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

[RFC] Improve handling of nested control structures #602

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

dime10
Copy link
Contributor

@dime10 dime10 commented Jan 26, 2024

Looking for feedback on the following:

After merging #599, Lightning obtained support for the BlockEncode operator. However, even though Lightning in principle supports arbitrary control wires on all supported operators, it didn't automatically gain support for C(BlockEncode). The reason is that this is treated as a separate gate altogether, and many operators are additionally added in their controlled form to the list of supported ops, which seems redundant. To make matters worse, operations of the form C(C(...)) are still not supported.

The following patch is just one way we could handle controlled operations in a more generic fashion. qml.simplify is applied to any Controlled instance before processing it in order to flatten any nested control structures. Additionally, the supports_operation method is overridden to strip any C( prefixes from gate names to determine support status.

Note that nested control structures can easily arise when users write functions that call other functions while adding qml.ctrl into the mix.

Benefits:

  • no redundant C(...) style specifications in the list of operations
  • much faster processing of certain operations:
    For instance, C(C(SWAP)) would previously need to be decomposed into many gates in order to simulate it, when it could just be applied in a single step. See the following benchmark for how this affects simulation time:
    Screenshot 2024-01-25 at 7 09 38 PM
    -> more than 10x improvement
  • supported gates that cannot be decomposed (such as BlockEncode) are automatically also supported in their controlled version

[sc-55549]

Copy link
Contributor

Hello. You may have forgotten to update the changelog!
Please edit .github/CHANGELOG.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.

@dime10 dime10 requested review from vincentmr and mlxd January 26, 2024 00:16
Copy link

codecov bot commented Jan 26, 2024

Codecov Report

Attention: 22 lines in your changes are missing coverage. Please review.

Comparison is base (2716864) 98.52% compared to head (b8d91c4) 96.71%.

Files Patch % Lines
...ylane_lightning/lightning_qubit/lightning_qubit.py 4.34% 22 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #602      +/-   ##
==========================================
- Coverage   98.52%   96.71%   -1.82%     
==========================================
  Files         168      169       +1     
  Lines       24566    24321     -245     
==========================================
- Hits        24204    23521     -683     
- Misses        362      800     +438     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@vincentmr
Copy link
Contributor

I really like the direction this is taking :) Also, wouldn't it be nice if, for example, CNOT, Toffoli, MultiControlledX were just aliases for C(PauliX)? This way we would not need special treatment for all these gates.

@dime10
Copy link
Contributor Author

dime10 commented Jan 26, 2024

Also, wouldn't it be nice if, for example, CNOT, Toffoli, MultiControlledX were just aliases for C(PauliX)? This way we would not need special treatment for all these gates.

100%, luckily I think @astralcai is already working on ensuring all controlled-x type operations have a canonical representation.

while operation[0:2] == "C(":
operation = operation[2:-1]

return super().supports_operation(operation)
Copy link
Contributor

@sergei-mironov sergei-mironov Feb 21, 2024

Choose a reason for hiding this comment

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

@dime10 could you please clarify how e.g. C(CX) gate would be handled? From my understanding, Lighting does support CX but does not natively support the controlled version of this gate.

UPD: I understand that PL would decompose gates like C(CX), but can we rely on this fact here, in the PL-lightning?

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems PennyLane will spit out C(CX) as a Toffoli, if it has a single control, and MultiControlledX otherwise. This is handled by the Python module and the C++ layer. The latter treats CNOT and Toffoli gates separately, but all MultiControlledX are understood as C(PauliX), which are natively supported.

Copy link
Contributor

Choose a reason for hiding this comment

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

A slight caveat here: a qml.ctrl(op, wire) where op is a CNOT will spit out Toffoli, but if the operation is instantiated using the constructor of Controlled like Controlled(op, wire), then it doesn't magically becomes a Toffoli, in which case you will need to call op.simplify() to flatten the nested control structure.

@@ -117,27 +117,6 @@
"CRX",
"CRY",
"CRZ",
"C(PauliX)",
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 a question, I am not asking for immediate changes in this PR)

The set of supported gates (in Lighthing at least) is defined here and in C++ (and I would say that the complete spec would also include threading model and the number of qubits). Is the Python list below - a duplicate of these C++ enums?
Could we use the device API to read the supported gates somehow? Maybe, we can autogenerate Python sources from the C++ ones if we want to have this information in compile time without running C funcitons?

Copy link
Contributor

Choose a reason for hiding this comment

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

The problem is we do not have a tight and robust mechanism to communicate natively supported gates. The enums are just that, enums. So they are not lists of the supported gates, just gates that might be supported on some device. What dictates which gates are decomposed are the allowed_operations dictionaries, but these are manually maintained and might not include gates that are natively supported (usually goes uncaught), and vice versa (usually fails some test). Some gates like OrbitalRotation are listed in allowed_operations but are not natively supported and do not crash any test. This is because gates that do not have a specialized implementation are applied via matrices. In the case of OrbitalRotation, this is fine because the number of wire is fixed at 4, a reasonable number. For other gates however, like QFT, this can cause issues since the number of wire is unlimited, possibly requiring the generation of a large dense matrix at the Python layer (filling out the memory).

@astralcai
Copy link
Contributor

astralcai commented Feb 21, 2024

In the current master of pennylane, all controlled operations inherit from Controlled, so you should be able to replace all string comparisons here with isinstance checks. For CNOT, Toffoli, and MultiControlledX, you do not need special handling anymore, because they are all instances of Controlled, and they all have a base property that is a PauliX.

Comment on lines 351 to 354
if operation.name == "MultiControlledX":
basename = "PauliX"
control_values = [bool(int(i)) for i in operation.hyperparameters["control_values"]]
target_wires = list(set(self.wires.indices(operation.wires)) - set(control_wires))
Copy link
Contributor

Choose a reason for hiding this comment

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

This special case is not necessary anymore. The MultiControlledX now inherits from Controlled, you can access its base like operation.base, its control values like operation.control_values just like you would with any other operator.

Comment on lines +366 to +375
elif basename == "CNOT":
basename = "PauliX"
control_values += [True]
control_wires += operation.base.wires[0]
target_wires = [operation.base.wires[1]]
elif basename == "Toffoli":
basename = "PauliX"
control_values += [True, True]
control_wires += operation.base.wires[0:2]
target_wires = [operation.base.wires[2]]
Copy link
Contributor

@astralcai astralcai Feb 21, 2024

Choose a reason for hiding this comment

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

I believe after my PR, in the current master of pennylane, if you call operation.simplify(), it would flatten any nested controlled structures to a multi-controlled structure. You can test it out and let me know if anything doesn't work. This would allow you to get rid of these special treatment.

Copy link
Contributor

Choose a reason for hiding this comment

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

>>> import pennylane as qml
>>> from pennylane.ops import Controlled
>>> op = Controlled(qml.CNOT([0,1],2))
>>> op.simplify()
Toffoli(wires=[2, 0, 1])

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.

4 participants