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

Nailing down automatic circuit cutter's API #2168

Merged
merged 42 commits into from
Feb 25, 2022

Conversation

zeyueN
Copy link
Contributor

@zeyueN zeyueN commented Feb 4, 2022

Context:
When passing a tape to the automatic circuit cutter (KaHyPar), several hyperparameters (e.g. k, imbalance, etc.) need to be supplied. Depending on use cases, sensible default values should be derived based on known information (which usually comes at different points in time) such as the available devices and the circuits. However, power users should also be able to override and supply their own parameters. We thus must provide a unified interface that's general enough to allow these types of interactions.

Drawing similarities between distributing circuit fragments to devices and the classical problem of distributing computation tasks dags to multiple workers, the constraints coming from devices should be separated from the requirements coming from the computations. This separation is realized inside the added class CutStrategy, whose attributes contain constraints from devices and whose method get_cut_kwargs() takes circuit requirements at runtime and returns complete sets of cutting hyperparameters. In terms of UI, both the device constrains and the calling of get_cut_kwargs() could be done either manually by users or within decorators. dataclass was elected to make the implementation of the init function cleaner.

Description of the Change:
Adds:

  • a class CutStrategy that can be used as an interface to coordinate devices info, user constraints, and circuit requirements into partition hyperparameters ready for partitioning methods.

Possible Drawbacks:
Gate cut compatibility not fully explored

- helper function for deriving best default cut parameters
- 2 container dataclesses (CutConfig and CutSpec) for automatic cutter's input and output.
@zeyueN zeyueN self-assigned this Feb 4, 2022
@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2022

Hello. You may have forgotten to update the changelog!
Please edit doc/releases/changelog-dev.md with:

  • A one-to-two sentence description of the change. You may include a small working example for new features.
  • A link back to this PR.
  • Your name (or GitHub username) in the contributors section.

@codecov
Copy link

codecov bot commented Feb 4, 2022

Codecov Report

Merging #2168 (0f738cc) into master (60bdfd1) will increase coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff            @@
##           master    #2168    +/-   ##
========================================
  Coverage   99.26%   99.26%            
========================================
  Files         231      231            
  Lines       18228    18330   +102     
========================================
+ Hits        18094    18196   +102     
  Misses        134      134            
Impacted Files Coverage Δ
pennylane/transforms/__init__.py 100.00% <ø> (ø)
pennylane/transforms/qcut.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 60bdfd1...0f738cc. Read the comment docs.

Copy link
Contributor

@trbromley trbromley left a comment

Choose a reason for hiding this comment

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

Thanks @zeyueN!

pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
@trbromley
Copy link
Contributor

[sc-14728]

@zeyueN zeyueN marked this pull request as ready for review February 15, 2022 15:47
@zeyueN
Copy link
Contributor Author

zeyueN commented Feb 15, 2022

Edited the description. Promoting to real PR.

Copy link
Contributor

@trbromley trbromley left a comment

Choose a reason for hiding this comment

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

Thanks @zeyueN! I left a few suggestions/comments, but overall this looks great and happy to approve once these are resolved.

pennylane/transforms/qcut.py Show resolved Hide resolved
doc/releases/changelog-dev.md Show resolved Hide resolved
pennylane/transforms/qcut.py Show resolved Hide resolved
``devices`` is provided where it defaults to the maximum number of wires among
``devices``.
min_free_wires (int): Number of wires for the smallest available device.
Optional, defaults to ``max_device_wires``.
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be good to add a sentence explaining the implication of setting this number not to be default.

pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
Comment on lines 560 to 582
**Example**

.. code-block:: python

import pennylane as qml
from pennylane.transforms import qcut

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

with qml.tape.QuantumTape() as tape:
qml.RX(0.4, wires=0)
qml.RY(0.9, wires=0)
qml.CNOT(wires=[0, 1])
qml.expval(qml.PauliZ(1))

tape_dag = qcut.tape_to_graph(tape)

Deriving kwargs for a given circuit and feed it to a custom partitioner along with an extra
custom parameter:
>>> cut_strategy = qcut.CutStrategy(devices=dev)
>>> cut_kwargs = cut_strategy.get_cut_kwargs(tape_dag)
>>> cut_kwargs.update({'extra_param': 0})
>>> my_partitioner(tape_dag, **cut_kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good to move the example below args and returns

Comment on lines 577 to 582
Deriving kwargs for a given circuit and feed it to a custom partitioner along with an extra
custom parameter:
>>> cut_strategy = qcut.CutStrategy(devices=dev)
>>> cut_kwargs = cut_strategy.get_cut_kwargs(tape_dag)
>>> cut_kwargs.update({'extra_param': 0})
>>> my_partitioner(tape_dag, **cut_kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps this section is the only bit we need.

Comment on lines 595 to 596
List[Dict[str, Any]]: A list of minimal set of kwargs being passed to a graph
partitioner method.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you might have answered in previous comments, but why a list? Also it seems at odds with the example:

        >>> cut_kwargs = cut_strategy.get_cut_kwargs(tape_dag)
        >>> cut_kwargs.update({'extra_param': 0})
        >>> my_partitioner(tape_dag, **cut_kwargs)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The returned list contains all potential cut kwargs to explore, i.e. different ks and imbalances. You are right the example is wrong, have fixed it.

Comment on lines 638 to 639
assert isinstance(max_wires_by_fragment, (list, tuple))
assert all(isinstance(i, int) and i > 0 for i in max_wires_by_fragment)
Copy link
Contributor

Choose a reason for hiding this comment

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

Are asserts the best choice for user validation? It's more length by if-raise seems more natural.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

switched.

pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
):
"""Deriving cutting constraints from given devices and parameters."""

self.max_free_wires = self.max_free_wires or self.min_free_wires
Copy link
Contributor

@anthayes92 anthayes92 Feb 18, 2022

Choose a reason for hiding this comment

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

I don't really understand what this is doing, why is there the or self.min_free_wires here?

Couldn't we just have these values set as in lines 617 and 618?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is me wanting to be slightly more forgiving before checking if one of devices and max_free_wires is provided, where if neither are provided but min_free_wires are, then I interpret max_free_wires to be the same as min_free_wires. Unlikely situation, but if it happens this interpretation should be safe.

Copy link
Contributor

@anthayes92 anthayes92 Feb 23, 2022

Choose a reason for hiding this comment

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

Thanks this makes sense, though now I'm not sure I understand the docstring where for max_free_wires we have "Optional only when ``devices`` is provided" and vice versa for devices. Maybe I'm misunderstanding the use of Optional but this seems to suggest that at least one of the two must be passed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct that the docstring doesn't accurately describe this behaviour. This was intentional since this is sort of a "failsafe" behaviour which doesn't need to be advertised to users since it adds nothing functionally new and could potentially confuse people even more. In the end, I do want to force users to provide either devices or max_free_wires.

pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
Comment on lines 720 to 721
if max_wires_by_fragment is not None and max_gates_by_fragment is not None:
assert len(max_wires_by_fragment) == len(max_gates_by_fragment)
Copy link
Contributor

Choose a reason for hiding this comment

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

why do these need to be of equal length?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because the length implicitly signifies the number of fragments, i.e. k.

Comment on lines 697 to 698
assert free_wires >= avg_fragment_wires
assert free_gates >= avg_fragment_gates
Copy link
Contributor

Choose a reason for hiding this comment

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

I think free_wires can be a user defined variable (max_wires_by_fragment) so this might be well suited to raising an exception. Same for gates.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Also added a test since these raises won't be able to get triggered by user facing functions due to all the checks before the function is invoked.

zeyueN and others added 9 commits February 22, 2022 01:54
Co-authored-by: anthayes92 <34694788+anthayes92@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: anthayes92 <34694788+anthayes92@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
@zeyueN
Copy link
Contributor Author

zeyueN commented Feb 22, 2022

@trbromley I think I've addressed all the comments and ready for review.

The documentation check workflow is failing for some reason. Any idea how to fix?

@trbromley trbromley self-requested a review February 22, 2022 17:55
Copy link
Contributor

@trbromley trbromley left a comment

Choose a reason for hiding this comment

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

Thanks @zeyueN, just a few more comments.

doc/releases/changelog-dev.md Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
(not isinstance(d, qml.Device) for d in devices)
):
raise ValueError(
f"Argument `devices` contain at least one `Device` instance, got {type(devices)}."
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 the right error message? We raise an error if:

  • Devices is not a list of tuple, or
  • Any element in device is not a qml.Device

Maybe the error message should just read:

Argument `devices` must be a list or tuple containing elements of type qml.Device

(Also, should we just check for Sequence rather than list/tuple?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya definitely screwed up that sentence, switched to your suggestion.

pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
Comment on lines 910 to 916
devices: InitVar[Sequence[qml.Device]] = None
max_free_wires: int = None
min_free_wires: int = None
num_fragments_probed: Union[int, Sequence[int]] = None
max_free_gates: int = None
min_free_gates: int = None
imbalance_tolerance: float = None
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the docs shows these attributes without docstrings:
image

Would it be possible to add quick one line explanations? E.g.,

    imbalance_tolerance: float = None
    #: The global maximum allowed imbalance for all partition trials

(I'm not completely sure on the UI to get this to show up in the docs so might need some trial and error)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, the comment should be above the line.
image

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome, thanks for figuring out the right orientation for the comment!

Copy link
Contributor

@trbromley trbromley left a comment

Choose a reason for hiding this comment

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

Awesome, looks great - thanks @zeyueN! I left some final comments, but otherwise good to go! Approved 🟢

pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
pennylane/transforms/qcut.py Outdated Show resolved Hide resolved
zeyueN and others added 3 commits February 24, 2022 18:27
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
@zeyueN zeyueN merged commit 1c0a57c into master Feb 25, 2022
@zeyueN zeyueN deleted the sc-14145-decide-upon-optimizer-api branch February 25, 2022 00:29
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

Successfully merging this pull request may close these issues.

3 participants