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

Add device and gradient expansions to the new batch-execution pipeline #1651

Merged
merged 72 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
b2c4baf
Bug fixes for batch execution
josh146 Sep 10, 2021
317514b
more tests
josh146 Sep 10, 2021
a937d4d
more tests
josh146 Sep 10, 2021
c6dc629
more tests
josh146 Sep 10, 2021
0a5756a
more tests
josh146 Sep 10, 2021
490f106
more tests
josh146 Sep 10, 2021
fa10491
changelog
josh146 Sep 10, 2021
d70f0c8
Add metric tensor
josh146 Sep 10, 2021
e0b45f5
fixes
josh146 Sep 10, 2021
523b765
add test
josh146 Sep 10, 2021
f8af2d2
Merge branch 'master' into batch-bug-fixes
josh146 Sep 10, 2021
754bbe7
Integrate batch execution into a QNode
josh146 Sep 10, 2021
bb6626c
update
josh146 Sep 10, 2021
377d8f3
update
josh146 Sep 10, 2021
7c400cc
remove xfail
josh146 Sep 10, 2021
57f83a0
Merge branch 'master' into batch-bug-fixes
josh146 Sep 10, 2021
0852693
Merge branch 'master' into batch-bug-fixes
josh146 Sep 13, 2021
abc543a
more tests
josh146 Sep 13, 2021
852f2fe
Merge branch 'master' into batch-bug-fixes
josh146 Sep 13, 2021
2a3c67b
rever
josh146 Sep 13, 2021
4e54a26
Add more tests
josh146 Sep 13, 2021
0129cb7
revert
josh146 Sep 13, 2021
5d1f0b3
Merge branch 'batch-qnode' into batch-qnode-interfaces
josh146 Sep 13, 2021
a79c827
fix
josh146 Sep 13, 2021
63f2990
Merge branch 'batch-bug-fixes' into batch-metric-tensor
josh146 Sep 13, 2021
74a8df0
Merge branch 'batch-metric-tensor' into batch-qnode
josh146 Sep 13, 2021
1d35677
Merge branch 'batch-qnode' into batch-qnode-interfaces
josh146 Sep 13, 2021
59b9d7d
fix
josh146 Sep 13, 2021
31b0ff3
Merge branch 'batch-qnode' into batch-qnode-interfaces
josh146 Sep 13, 2021
23f8433
fix
josh146 Sep 13, 2021
cd8b00c
fix cross references
josh146 Sep 13, 2021
524724f
Merge branch 'batch-qnode' into batch-qnode-interfaces
josh146 Sep 13, 2021
21c0386
fix
josh146 Sep 13, 2021
3d84bb3
fix
josh146 Sep 13, 2021
c84cf98
Merge branch 'batch-qnode' into batch-qnode-interfaces
josh146 Sep 13, 2021
b445870
fix
josh146 Sep 13, 2021
eaf7381
Merge branch 'batch-qnode' into batch-qnode-interfaces
josh146 Sep 13, 2021
547a286
Add support for device and gradient expansions
josh146 Sep 14, 2021
440a46a
fixes
josh146 Sep 14, 2021
d8d3b66
more fixes
josh146 Sep 14, 2021
316a16f
more
josh146 Sep 14, 2021
9b32238
add tests
josh146 Sep 14, 2021
4a94acf
more
josh146 Sep 14, 2021
4bdf092
more
josh146 Sep 14, 2021
f0e5c3c
more
josh146 Sep 14, 2021
4c671dc
more
josh146 Sep 14, 2021
4308af5
comment
josh146 Sep 14, 2021
c35b5cb
more tests
josh146 Sep 14, 2021
1e812d7
more tests
josh146 Sep 15, 2021
bc57b00
merge master
josh146 Sep 15, 2021
016857a
Merge branch 'batch-qnode' into batch-qnode-interfaces
josh146 Sep 15, 2021
102d6ce
Merge branch 'batch-qnode-interfaces' into batch-qnode-expand
josh146 Sep 15, 2021
a9b26ca
more tests
josh146 Sep 15, 2021
063745b
more tests
josh146 Sep 15, 2021
79b55d2
better suppotr
josh146 Sep 17, 2021
19c3fe6
merge master
josh146 Sep 21, 2021
4d54374
update changelog
josh146 Sep 21, 2021
0da517f
linting
josh146 Sep 21, 2021
8d0880d
changelog
josh146 Sep 21, 2021
141671a
another test
josh146 Sep 21, 2021
d46fb3a
Merge branch 'master' into batch-qnode-expand
josh146 Sep 21, 2021
a5f6424
Apply suggestions from code review
josh146 Sep 21, 2021
af9c4c4
Apply suggestions from code review
josh146 Sep 22, 2021
94de146
Apply suggestions from code review
josh146 Sep 22, 2021
61383be
Apply suggestions from code review
josh146 Sep 22, 2021
4478944
Merge branch 'master' into batch-qnode-expand
josh146 Sep 22, 2021
d78f5a4
Update tests/interfaces/test_batch_torch_qnode.py
josh146 Sep 22, 2021
8b40dfa
Merge branch 'master' into batch-qnode-expand
josh146 Sep 23, 2021
46d01bb
Merge branch 'master' into batch-qnode-expand
josh146 Sep 23, 2021
6bdcd21
add grad_method=None to all templates
josh146 Sep 23, 2021
320b468
fix
josh146 Sep 23, 2021
bb640fe
fix
josh146 Sep 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
`qml.beta.QNode`, and `@qml.beta.qnode`.
[(#1642)](https://github.com/PennyLaneAI/pennylane/pull/1642)
[(#1646)](https://github.com/PennyLaneAI/pennylane/pull/1646)
[(#1651)](https://github.com/PennyLaneAI/pennylane/pull/1651)

It differs from the standard QNode in several ways:

Expand All @@ -106,21 +107,52 @@
significant performance improvement when executing the QNode on remote
quantum hardware.

- When decomposing the circuit, the default decomposition strategy will prioritize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when do decompositions generally occur? e.g if I was running a simple optimisation with a PL circuit, at what level does this happen on hadware/simulator?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • expansion_strategy="device": decomposition happens in the QNode, during construction, by querying the device for its supported gate set. This is beneficial in terms of overhead (since the decomposition only happens once), but results in future quantum transforms/compilations working with a potentially very big/deep circuit.

  • expansion_strategy="gradient": decomposition happens in the QNode, during construction, by querying the gradient transform. Typically, the decomposed circuit will not be as deep as the device-decomposed one, since a lot of complex unitaries have gradient rules defined. Later on, further decompositions may be required on the device to get the circuit down to native gate sets.

    This is beneficial in terms of a reduction in quantum resources, at the expense of moving the device decomposition down to every evaluation of the device (so additional classical overhead).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is beneficial in terms of a reduction in quantum resources, at the expense of moving the device decomposition down to every evaluation of the device (so additional classical overhead).

Seems to be yet another benefit for caching parametric circuits to reuse device translations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, precisely 💯 I would even argue, this is only fully solved by parametric compilation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Followup question: suppose I do something like

@qml.compile()
@qml.qnode(dev, expansion_strategy="device")
def some_qnode():
    # stuff

(or alternatively the gradient strategy). When does decomposition happen currently vs. in this new PR w.r.t. the compilation transform? As you suggest @josh146 we would want compilation to happen before either the device or gradient strategy so that compilation it is acting on a smaller circuit rather than the full-depth expanded one, and consequently leading to a smaller circuit that gets expanded / gradient transformed. (That said, it's possible that a decomposition leads to optimizations in the compilation pipeline that might not otherwise be recognized...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation @josh146, that makes things very clear! The gradient transform continues to impress me!

Copy link
Member Author

@josh146 josh146 Sep 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glassnotes, correct me if wrong, but compile() is a qfunc transform, not a QNode transform? So the following order is needed:

@qml.qnode(dev, expansion_strategy="device")
@qml.compile()
def some_qnode():
    # stuff

and just based on the ordering, the compile transform would always occur prior to the QNode's expansions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you're 100% correct, my bad 🤦‍♀️

decompositions that result in the smallest number of parametrized operations
required to satisfy the differentiation method. Additional decompositions required
to satisfy the native gate set of the quantum device will be performed later, by the
device at execution time. While this may lead to a slight increase in classical processing,
it significantly reduces the number of circuit evaluations needed to compute
gradients of complex unitaries.

In an upcoming release, this QNode will replace the existing one. If you come across any bugs
while using this QNode, please let us know via a [bug
report](https://github.com/PennyLaneAI/pennylane/issues/new?assignees=&labels=bug+%3Abug%3A&template=bug_report.yml&title=%5BBUG%5D)
on our GitHub bug tracker.

Currently, this beta QNode does not support the following features:

- Circuit decompositions
- Non-mutability via the `mutable` keyword argument
- Viewing specifications with `qml.specs`
- The `reversible` QNode differentiation method
- The ability to specify a `dtype` when using PyTorch and TensorFlow.

It is also not tested with the `qml.qnn` module.

* Two new methods were added to the Device API, allowing PennyLane devices
increased control over circuit decompositions.
[(#1651)](https://github.com/PennyLaneAI/pennylane/pull/1651)

- `Device.expand_fn(tape) -> tape`: expands a tape such that it is supported by the device. By
default, performs the standard device-specific gate set decomposition done in the default
QNode. Devices may overwrite this method in order to define their own decomposition logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can a user overwrite this logic?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is a good point 🤔

At the moment yes, but it looks a bit hacky. You could do something like this:

>>> def my_custom_expand_fn(tape, **kwargs):
...     print("hello")
...     return tape
>>> qnode.device.expand_fn = my_custom_expand_fn
>>> qnode(0.5)
hello
tensor(0.87758256, requires_grad=True)

Hmmm 🤔 Do you think this will be useful?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glassnotes I think @anthayes92 might be on to something re: custom decompositions....

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, we could even support something like how you currently 'register' QNode execution wrappers while writing a batch transform

dev = qml.device("default.qubit", wires=2)

@dev.custom_expand
def my_expansion_function(tape, **kwargs):
    ...
    return tape

# from now on, the custom expansion is called whenever
# the device is executed.

This is more powerful (too powerful?) compared to a dictionary of gates to custom decompositions. But I still have some question marks:

  1. Should this replace the device expansion?

  2. If it doesn't, does it come before the device expansion? This way unsupported gates are finally decomposed down to device native gates. Or should it come after the device expansion? Execution would then fail if the custom decomp results in an unsupported gate.

  3. Rather than changing the device, does it make more sense to pass a custom decomposition to the QNode? E.g.,

    @qml.qnode(dev, expansion_strategy=my_custom_expansion)
    
    # or
    
    @existing_qnode.register_expansion
    def my_custom_expansion(...):

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more powerful (too powerful?) compared to a dictionary of gates to custom decompositions.

How would custom decompositions be specified in these cases?

What if we did something like this, which combines a few of the ideas floating around:

custom_decomps = {qml.Hadamard : h_func, qml.CNOT : cnot_func}

def custom_expand(tape, custom_decomps):
    # applies custom decompositions 

qnode.device.set_expand_fn(custom_expand)

but where the device itself does some sort of internal validation of the decompositions?

def set_expand_fn(custom_decomps):
    for op, decomp in custom_decomps.items():
       # Ensure all the operations in the decomposition are valid for the device
       ...
       # Register the new decompositions to the operators
       if decomp_is_valid:
           op.register_new_decomposition(decomp)

If we do this kind of validation, it ensures that we can apply the expansion after the gradient tapes have already been constructed, but with the guarantee that they'll still run on the device.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like @glassnotes suggestion of having expansion after the gradient tapes have already been constructed! So would the logic here look like: if custom gates are unsupported then decompose to device native gates, so that this is where the guarantee they'll still run on the device comes from?


Note that the numerical result after applying this method should remain unchanged; PennyLane
will assume that the expanded tape returns exactly the same value as the original tape when
executed.

- `Device.batch_transform(tape) -> (tapes, processing_fn)`: pre-processes the tape in the case
josh146 marked this conversation as resolved.
Show resolved Hide resolved
where the device needs to generate multiple circuits to execute from the input circuit. The
requirement of a post-processing function makes this distinct to the `expand_fn` method above.

By default, this method applies the transform

.. math:: \left\langle \sum_i c_i h_i\right\rangle -> \sum_i c_i \left\langle h_i \right\rangle

if `expval(H)` is present on devices that do not natively support Hamiltonians with
non-commuting terms.


<h3>Improvements</h3>

* The tests for qubit operations are split into multiple files.
Expand Down
91 changes: 91 additions & 0 deletions pennylane/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,97 @@ def gradients(self, circuits, method="jacobian", **kwargs):
gradient_method = getattr(self, method)
return [gradient_method(circuit, **kwargs) for circuit in circuits]

def expand_fn(self, circuit, max_expansion=10):
"""Method for expanding or decomposing an input circuit.
This method should be overwritten if custom expansion logic is
required.

By default, this method expands the tape if:

- nested tapes are present,
- any operations are not supported on the device, or
- multiple observables are measured on the same wire.

Args:
circuit (.QuantumTape): the circuit to expand.
max_expansion (int): The number of times the circuit should be
expanded. Expansion occurs when an operation or measurement is not
supported, and results in a gate decomposition. If any operations
in the decomposition remain unsupported by the device, another
expansion occurs.

Returns:
.QuantumTape: The expanded/decomposed circuit, such that the device
will natively support all operations.
"""
obs_on_same_wire = len(
circuit._obs_sharing_wires # pylint: disable=protected-access
) > 0 and not self.supports_observable("Hamiltonian")

ops_not_supported = any(
isinstance(op, qml.tape.QuantumTape) # nested tapes must be expanded
or not self.supports_operation(op.name) # unsupported ops must be expanded
for op in circuit.operations
)

if ops_not_supported or obs_on_same_wire:
circuit = circuit.expand(
depth=max_expansion,
stop_at=lambda obj: not isinstance(obj, qml.tape.QuantumTape)
and self.supports_operation(obj.name),
)

return circuit

def batch_transform(self, circuit):
"""Apply a differentiable batch transform for preprocessing a circuit
prior to execution. This method is called directly by the QNode, and
should be overwritten if the device requires a transform that
generates multiple circuits prior to execution.

By default, this method contains logic for generating multiple
circuits, one per term, of a circuit that terminates in ``expval(H)``,
if the underlying device does not support Hamiltonian expectation values,
or if the device requires finite-shots.
josh146 marked this conversation as resolved.
Show resolved Hide resolved

.. warning::

This method will be tracked by autodifferentiation libraries,
such as Autograd, JAX, TensorFlow, and Torch. Please make sure
to use ``qml.math`` for autodiff-agnostic tensor processing
if required.

Args:
circuit (.QuantumTape): the circuit to preprocess

Returns:
tuple[Sequence[.QuantumTape], callable]: Returns a tuple containing
the sequence of circuits to be executed, and a post-processing function
to be applied to the list of evaluated circuit results.
"""

# If the observable contains a Hamiltonian and the device does not
# support Hamiltonians, or if the simulation uses finite shots,
# split tape into multiple tapes of diagonalizable known observables.
# In future, this logic should be moved to the device
josh146 marked this conversation as resolved.
Show resolved Hide resolved
# to allow for more efficient batch execution.
josh146 marked this conversation as resolved.
Show resolved Hide resolved
supports_hamiltonian = self.supports_observable("Hamiltonian")
finite_shots = self.shots is not None

hamiltonian_in_obs = "Hamiltonian" in [obs.name for obs in circuit.observables]

if hamiltonian_in_obs and (not supports_hamiltonian or finite_shots):
try:
return qml.transforms.hamiltonian_expand(circuit, group=False)

except ValueError as e:
raise ValueError(
"Can only return the expectation of a single Hamiltonian observable"
) from e

# otherwise, return an identity transform
return [circuit], lambda res: res[0]

@property
def op_queue(self):
"""The operation queue to be applied.
Expand Down
39 changes: 36 additions & 3 deletions pennylane/beta/qnode.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ class QNode:

Currently, this beta QNode does not support the following features:

- Circuit decompositions
- Non-mutability via the ``mutable`` keyword argument
- Viewing specifications with ``qml.specs``
- The ``reversible`` QNode differentiation method
- The ability to specify a ``dtype`` when using PyTorch and TensorFlow.

It is also not tested with the :mod:`~.qnn` module.

Expand Down Expand Up @@ -123,6 +124,19 @@ class QNode:

* ``None``: QNode cannot be differentiated. Works the same as ``interface=None``.

expansion_strategy (str): The strategy to use when circuit expansions or decompositions
are required.

- ``gradient``: The QNode will attempt to decompose
the internal circuit such that all circuit operations are supported by the gradient
method. Further decompositions required for device execution are performed by the
device prior to circuit execution.

- ``device``: The QNode will attempt to decompose the internal circuit
such that all circuit operations are natively supported by the device.

The ``gradient`` strategy typically results in a reduction in quantum device evaluations
required during optimization, at the expense of an increase in classical pre-processing.
josh146 marked this conversation as resolved.
Show resolved Hide resolved
max_expansion (int): The number of times the internal circuit should be expanded when
executed on a device. Expansion occurs when an operation or measurement is not
supported, and results in a gate decomposition. If any operations in the decomposition
Expand Down Expand Up @@ -174,6 +188,7 @@ def __init__(
device,
interface="autograd",
diff_method="best",
expansion_strategy="gradient",
max_expansion=10,
mode="best",
cache=True,
Expand Down Expand Up @@ -208,6 +223,7 @@ def __init__(
self.device = device
self._interface = interface
self.diff_method = diff_method
self.expansion_strategy = expansion_strategy
self.max_expansion = max_expansion

# execution keyword arguments
Expand All @@ -216,8 +232,12 @@ def __init__(
"cache": cache,
"cachesize": cachesize,
"max_diff": max_diff,
"max_expansion": max_expansion,
}

if self.expansion_strategy == "device":
self.execute_kwargs["expand_fn"] = None

# internal data attributes
self._tape = None
self._qfunc_output = None
Expand Down Expand Up @@ -518,6 +538,14 @@ def construct(self, args, kwargs):
"Operator {} must act on all wires".format(obj.name)
)

if self.expansion_strategy == "device":
self._tape = self.device.expand_fn(self.tape, max_expansion=self.max_expansion)

# If the gradient function is a transform, expand the tape so that
# all operations are supported by the transform.
if isinstance(self.gradient_fn, qml.gradients.gradient_transform):
self._tape = self.gradient_fn.expand_fn(self._tape)

def __call__(self, *args, **kwargs):
override_shots = False

Expand All @@ -540,15 +568,20 @@ def __call__(self, *args, **kwargs):
# construct the tape
self.construct(args, kwargs)

# preprocess the tapes by applying any device-specific transforms
tapes, processing_fn = self.device.batch_transform(self.tape)

res = qml.execute(
[self.tape],
tapes,
device=self.device,
gradient_fn=self.gradient_fn,
interface=self.interface,
gradient_kwargs=self.gradient_kwargs,
override_shots=override_shots,
**self.execute_kwargs,
)[0]
)

res = processing_fn(res)

if override_shots is not False:
# restore the initialization gradient function
Expand Down
5 changes: 4 additions & 1 deletion pennylane/gradients/gradient_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ def gradient_expand(tape, depth=10):
and ((supported_op(obj) and trainable_op(obj)) or not trainable_op(obj))
)

return tape.expand(depth=depth, stop_at=stop_cond)
new_tape = tape.expand(depth=depth, stop_at=stop_cond)
params = new_tape.get_parameters(trainable_only=False)
new_tape.trainable_params = qml.math.get_trainable_indices(params)
Comment on lines +52 to +53
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out these two lines are required to solve a bug I discovered while writing the tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this related to the Unwrap issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really hesitate to say this, but I often don't fully understand the trainable_params setting 😬 Each autodiff framework is different, it is affected by expansion, by higher order derivatives, etc.

So a lot of the current logic is simply guided by 'this causes the tests to pass, for all interfaces, for all QNode variations, for all order derivatives, for all differentiation methods'.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So a lot of the current logic is simply guided by 'this causes the tests to pass, for all interfaces, for all QNode variations, for all order derivatives, for all differentiation methods'.

This is how I feel any time I have to write interface tests 😓

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok, could this be something to think about addressing for the upcoming planning? Or is it the case that since it only affects devs and we can generally muddle though that it's lower priority

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we all feel it 😿

return new_tape

return tape

Expand Down
36 changes: 32 additions & 4 deletions pennylane/interfaces/batch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def set_shots(device, shots):
device.shots = original_shots


def cache_execute(fn, cache, pass_kwargs=False, return_tuple=True):
def cache_execute(fn, cache, pass_kwargs=False, return_tuple=True, expand_fn=None):
"""Decorator that adds caching to a function that executes
multiple tapes on a device.

Expand Down Expand Up @@ -106,6 +106,12 @@ def cache_execute(fn, cache, pass_kwargs=False, return_tuple=True):
function: a wrapped version of the execution function ``fn`` with caching
support
"""
if expand_fn is not None:
original_fn = fn

def fn(tapes, **kwargs): # pylint: disable=function-redefined
tapes = [expand_fn(tape) for tape in tapes]
return original_fn(tapes, **kwargs)

@wraps(fn)
def wrapper(tapes, **kwargs):
Expand Down Expand Up @@ -189,6 +195,8 @@ def execute(
cachesize=10000,
max_diff=2,
override_shots=False,
expand_fn="device",
max_expansion=10,
):
"""Execute a batch of tapes on a device in an autodifferentiable-compatible manner.

Expand Down Expand Up @@ -217,6 +225,14 @@ def execute(
the maximum number of derivatives to support. Increasing this value allows
for higher order derivatives to be extracted, at the cost of additional
(classical) computational overhead during the backwards pass.
expand_fn (function): Tape expansion function to be called prior to device execution.
Must have signature of the form ``expand_fn(tape, max_expansion)``, and return a
single :class:`~.QuantumTape`. If not provided, by default :meth:`Device.expand_fn`
is called.
max_expansion (int): The number of times the internal circuit should be expanded when
executed on a device. Expansion occurs when an operation or measurement is not
supported, and results in a gate decomposition. If any operations in the decomposition
remain unsupported by the device, another expansion occurs.

Returns:
list[list[float]]: A nested list of tape results. Each element in
Expand Down Expand Up @@ -284,21 +300,33 @@ def cost_fn(params, x):

batch_execute = set_shots(device, override_shots)(device.batch_execute)

if expand_fn == "device":
expand_fn = lambda tape: device.expand_fn(tape, max_expansion=max_expansion)

if gradient_fn is None:
with qml.tape.Unwrap(*tapes):
res = cache_execute(batch_execute, cache, return_tuple=False)(tapes)
res = cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(
tapes
)

return res

if gradient_fn == "backprop" or interface is None:
return cache_execute(batch_execute, cache, return_tuple=False)(tapes)
return cache_execute(batch_execute, cache, return_tuple=False, expand_fn=expand_fn)(tapes)

# the default execution function is batch_execute
execute_fn = cache_execute(batch_execute, cache)
execute_fn = cache_execute(batch_execute, cache, expand_fn=expand_fn)

if gradient_fn == "device":
# gradient function is a device method

# Expand all tapes as per the device's expand function here.
# We must do this now, prior to the interface, to ensure that
# decompositions with parameter processing is tracked by the
# autodiff frameworks.
for i, tape in enumerate(tapes):
tapes[i] = expand_fn(tape)

if mode in ("forward", "best"):
# replace the forward execution function to return
# both results and gradients
Expand Down
2 changes: 1 addition & 1 deletion pennylane/interfaces/batch/autograd.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def grad_fn(dy):

# Generate and execute the required gradient tapes
if _n == max_diff:
with qml.tape.Unwrap(*tapes):
with qml.tape.Unwrap(*tapes, set_trainable=False):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bug I discovered. I think by now, almost all cases of qml.tape.Unwrap() require set_trainable=False to work!!

In a new PR, I will remove the set_trainable logic from Unwrap, since it seems to be not needed/not required.

vjp_tapes, processing_fn = qml.gradients.batch_vjp(
tapes,
dy,
Expand Down
2 changes: 1 addition & 1 deletion pennylane/interfaces/batch/torch.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def backward(ctx, *dy):
# The derivative order is at the maximum. Compute the VJP
# in a non-differentiable manner to reduce overhead.

with qml.tape.Unwrap(*ctx.tapes):
with qml.tape.Unwrap(*ctx.tapes, set_trainable=False):
vjp_tapes, processing_fn = qml.gradients.batch_vjp(
ctx.tapes,
dy,
Expand Down
1 change: 1 addition & 0 deletions pennylane/templates/embeddings/amplitude.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def circuit(f=None):
num_params = 1
num_wires = AnyWires
par_domain = "A"
grad_method = None

def __init__(
self, features, wires, pad_with=None, normalize=False, pad=None, do_queue=True, id=None
Expand Down
1 change: 1 addition & 0 deletions pennylane/templates/embeddings/angle.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class AngleEmbedding(Operation):
num_params = 1
num_wires = AnyWires
par_domain = "A"
grad_method = None

def __init__(self, features, wires, rotation="X", do_queue=True, id=None):

Expand Down
1 change: 1 addition & 0 deletions pennylane/templates/layers/basic_entangler.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def circuit(weights):
num_params = 1
num_wires = AnyWires
par_domain = "A"
grad_method = None

def __init__(self, weights, wires=None, rotation=None, do_queue=True, id=None):

Expand Down
Loading