-
Notifications
You must be signed in to change notification settings - Fork 39
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
base: master
Are you sure you want to change the base?
Conversation
Hello. You may have forgotten to update the changelog!
|
Codecov ReportAttention: Patch coverage is
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. |
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. |
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 enum
s are just that, enum
s. 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).
In the current master of pennylane, all controlled operations inherit from |
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)) |
There was a problem hiding this comment.
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.
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]] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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])
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 forC(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 formC(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 anyControlled
instance before processing it in order to flatten any nested control structures. Additionally, thesupports_operation
method is overridden to strip anyC(
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:
C(...)
style specifications in the list of operationsFor 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:-> more than 10x improvement
BlockEncode
) are automatically also supported in their controlled version[sc-55549]