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

QPY serialization fails for circuits with IfElse instructions #7583

Closed
kdk opened this issue Jan 26, 2022 · 5 comments · Fixed by #7584
Closed

QPY serialization fails for circuits with IfElse instructions #7583

kdk opened this issue Jan 26, 2022 · 5 comments · Fixed by #7584
Labels
bug Something isn't working mod: qpy Related to QPY serialization stable backport potential The bug might be minimal and/or import enough to be port to stable

Comments

@kdk
Copy link
Member

kdk commented Jan 26, 2022

Environment

  • Qiskit Terra version: 0.20.0.dev0+01913f4
  • Python version: 3.7
  • Operating system: Mac OSX 11.6.1

What is happening?

QPY serialization fails when attempting to serialize a circuit containing an IfElseOp.

How can we reproduce the issue?

import qiskit as qk

qc = qk.QuantumCircuit(2, 2)
qc.h(0)
qc.measure(0, 0)

with qc.if_test((qc.clbits[0], True)):
    qc.x(1)
    
qc.measure(1,1)

from qiskit.circuit import qpy_serialization as qpy
import tempfile

with tempfile.TemporaryFile() as fp:
    qpy.dump(qc, fp)
    out = qpy.load(fp)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/97/vkqk0hs17r5dnlc8dv5nmfxh0000gn/T/ipykernel_34451/736355381.py in <module>
      3 
      4 with tempfile.TemporaryFile() as fp:
----> 5     qpy.dump(qc, fp)
      6     out = qpy.load(fp)

~/q/qiskit-terra/qiskit/circuit/qpy_serialization.py in dump(circuits, file_obj)
   1423     file_obj.write(header)
   1424     for circuit in circuits:
-> 1425         _write_circuit(file_obj, circuit)
   1426 
   1427 

~/q/qiskit-terra/qiskit/circuit/qpy_serialization.py in _write_circuit(file_obj, circuit)
   1493     index_map["c"] = clbit_indices
   1494     for instruction in circuit.data:
-> 1495         _write_instruction(instruction_buffer, instruction, custom_instructions, index_map)
   1496     file_obj.write(struct.pack(CUSTOM_DEFINITION_HEADER_PACK, len(custom_instructions)))
   1497 

~/q/qiskit-terra/qiskit/circuit/qpy_serialization.py in _write_instruction(file_obj, instruction_tuple, custom_instructions, index_map)
   1223         else:
   1224             raise TypeError(
-> 1225                 f"Invalid parameter type {instruction_tuple[0]} for gate {type(param)},"
   1226             )
   1227         instruction_param_raw = struct.pack(INSTRUCTION_PARAM_PACK, type_key.encode("utf8"), size)

TypeError: Invalid parameter type Instruction(name='if_else', num_qubits=1, num_clbits=1, params=[<qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x13c510750>, None]) for gate <class 'qiskit.circuit.quantumcircuit.QuantumCircuit'>,

What should happen?

The circuit should be able to be serialized and de-serialized.

Any suggestions?

No response

@kdk kdk added bug Something isn't working stable backport potential The bug might be minimal and/or import enough to be port to stable mod: qpy Related to QPY serialization labels Jan 26, 2022
@mtreinish
Copy link
Member

mtreinish commented Jan 26, 2022

So there are 2 things we need to fix here. The first is to add qiskit.circuit.controlflow to the module lookup path for terra objects here: https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/qpy_serialization.py#L1122-L1125 and https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/qpy_serialization.py#L976-L983 so that qpy know about the control flow instructions. Otherwise it will try to treat the control flow instruction objects as custom instructions and make them instances of Instruction instead of the actual control flow object. The other piece of this is to allow circuits as a valid parameter type for instructions, here: https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/qpy_serialization.py#L1183-L1222 and https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/qpy_serialization.py#L934-L956 and we can just use https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/qpy_serialization.py#L1428 with a BytesIO buffer for the payload.

The only potential (mostly because I'm not as familiar with the control flow instructions) complication I can think of is if IfElseOp(**params) doesn't work we'll need to add a custom handling for it here: https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/qpy_serialization.py#L961-L991 in the deserializer. But, if we need more information from the object than what is serialized by default we'll probably need to define a custom serialization format for it (similar to what we did for the PauliEvolutionGate in #7374) so that we have all the necessary information in the qpy data to reconstruct the operation on the deserialization side.

@jakelishman
Copy link
Member

IfElseOp(**params) will fail, because the first positional argument is the condition, which is stored separately on Instruction, not in params. The same goes for WhileLoopOp, but ForLoopOp is in that form.

I guess we need to add an arbitrary serialisation hook here. I think we want to avoid any possible arbitrary _de_serialisation hooks, so we could ask for a method that will return a 2-tuple (args, kwargs) that can be passed to the initialiser to reconstruct the state of the object, and then recursively serialise those?

@mtreinish
Copy link
Member

Well we've had to do deserialization conditions before for instruction types which behave differently for weird reasons. Barrier and Initialize are the only two currently. We also serialize the condition attribute currently. So assuming the condition attribute is the same tuple of (register/bit, value) that other instructions have we wouldn't need to change anything on the serialization side and we can just add conditions like we do for barrier and initialize today. I think this might be preferable just because it's a smaller change for a backport and doesn't require defining a new type for the custom instruction table.

I think for the ever growing qpy v4 todo list (which already includes a global parameter table and pulse support) in 0.20.0 allowing an instruction class to define it's own (args, kwargs) for the constructor with something like a __qpy_args__() method for serialization is a good thing to do.

@jakelishman
Copy link
Member

Having the condition is fine, the problem is that the constructor of IfElseOp and WhileLoopOp take condition as their first positional argument, and don't put condition in the params attribute. The condition is the same tuple, but you do something like

my_if = IfElseOp((creg, 1), true_body, false_body)
assert len(my_if.params) == 2
assert my_if.params[0] is true_body
assert my_if.params[1] is false_body

We can do it, but it'll need special handling for IfElseOp and WhileLoopOp when reconstructing the instances, to pass the condition positionally first (they error if there's no condition).

Also, can qpy currently serialise a parameter of None? IfElseOp can have None as its second parameter if there isn't a false body.

@mtreinish
Copy link
Member

Right, I just meant we have a place already for this kind of thing where we have to do that kind of custom deserialization construction. Since we had to do it for Barrier (which does Barrier(num_qubits) which isn't a parameter) and Initialize (which does Initialize(params) instead of Initialize(*params). So for 0.19.4 I think if we do something like:

if gate_name in {"IfElseOp", "WhileLoopOp"}:
    gate_class(condition, *params)

around here: https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/qpy_serialization.py#L986-L992 that fits in with an existing pattern and won't be too intrusive and seems like the simple path for a backport fix.

As for None, that's a good call we don't support that yet either. That's trivial to add as a supported param type, we can add that at the same time we do QuantumCircuit

mtreinish added a commit to mtreinish/qiskit-core that referenced this issue Jan 26, 2022
This commit fixes support for using qpy serialization with circuits that
contained control flow instructions. Previously, if you attempted to
serialize a circuit that contained control flow instructions it would
error as qpy didn't support embedding the circuit blocks as parameters
for the instructions. This has been fixed and other missing pieces for
fully reconstructing circuits with control flow were added. This
requires bumping the version string to v4 because to accurately
reconstruct control flow circuits we needed to be able to represent
registers that were only partially present in a circuit fully. V4 also
adds a representation for ranges which didn't exist in earlier versions.

Fixes Qiskit#7583
@mergify mergify bot closed this as completed in #7584 Feb 2, 2022
mergify bot added a commit that referenced this issue Feb 2, 2022
* Fix QPY support for control flow instructions

This commit fixes support for using qpy serialization with circuits that
contained control flow instructions. Previously, if you attempted to
serialize a circuit that contained control flow instructions it would
error as qpy didn't support embedding the circuit blocks as parameters
for the instructions. This has been fixed and other missing pieces for
fully reconstructing circuits with control flow were added. This
requires bumping the version string to v4 because to accurately
reconstruct control flow circuits we needed to be able to represent
registers that were only partially present in a circuit fully. V4 also
adds a representation for ranges which didn't exist in earlier versions.

Fixes #7583

* Fix handling of standalone register bits in circuit

This commit fixes the failing tests by correcting the handling of
standalone register bits that are part of a circuit (without their
register). Previosuly, if the circuit was solely composed of register
bits but the circuit didn't contain that register, qpy deserialization
would incorrectly add the register to the circuit. This corrects this by
having the deserialization function treat a register that's not in
circuit in the same manner as if it were an out of order register and
add each bit one at a time.

* Add test for for loop with iterator

* Add backwards compat tests for control flow

* Finish release notes

* Fix docs build

* Apply suggestions from code review

Co-authored-by: Jake Lishman <jake@binhbar.com>

* Add handling for ContinueLoopOp too

* Cleanup note about register index array type change

* Use unique doc reference names for qpy module

* Update qpy version 3 ref in release note

* Switch tuple to support any qpy type instead of just ints

This commit changes the tuple formatting to work with any parameter type
that qpy currently supports, not just ints which is what for loop
currently uses a tuple for. This should provide some future proofing as
it enables tuples of other types, which is being considered in the
future, to work without modification.

* Update another broken sphinx ref

* Fix last dangling broken sphinx ref

* Fix yet another dangling sphinx ref

* Fix failing test

* Set fixed Python hash seed in qpy compat tests

This commit updates the qpy compat test runner script to set a fixed
hash seed. The control flow builders internally use sets which means
that to get consistent ordering between python processes we need to
ensure the hash seeds are the same. Without this the argument order
might differ between creating the circuit for qpy serialization and
reconstructing the circuit in another process by deserializing that
qpy data. Setting a fixed hash seed between processes ensures this type
of failure won't happen as the set order will be consistent between
multiple runs of Python.

* Update qiskit/circuit/qpy_serialization.py

Co-authored-by: Jake Lishman <jake@binhbar.com>

* Fix docstring for TUPLE payload

This commit fixes an oversight in earlier commits where the
documentation for the TUPLE payload wasn't actually finished wasn't
really coherent. This commit finishes the documentation to explain how a
tuple is serialized in qpy.

* Add test case using nested tuples

* Directly construct WhileLoopOp in qpy compat tests

In an earlier commit we set a fixed (but still randomly generated) python
hash seed to force multiple invocations of python to use the same hash
seed. This was done in order to fix a non-deterministic failure we were
seeing with the while loop circuit. The while loop context builder uses
a set internally and is sensitive to argument ordering. However, just
hash seed randomization doesn't fix the issue in practice because the
difference isn't between set order strictly, but also that set order
matches the circuit insertion order for what gets written in QPY. To
address this and make the tests work stably this commit switches away
from using the builder interface and instead manually constructs the
while loop and appends it to the circuit with a fixed argument order.
This should hopefully make the compat tests run consistently and fix the
occasional failures we're seeing in CI.

* Fix lint

* Add test case for serializing an empty tuple instruction parameter

* Avoid builder interface for qpy compat tests

Just as was changed for the while loop generator function in the qpy
compat tests this commit updates the other control flow example circuits
to not use the builder interface. The same non-determinism in the
ordering of arguments applies to all the control flow instructions. So
to avoid spurious failures we should explicitly set a arg order and not
use the builder interface.

Co-authored-by: Jake Lishman <jake@binhbar.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
mergify bot pushed a commit that referenced this issue Feb 2, 2022
* Fix QPY support for control flow instructions

This commit fixes support for using qpy serialization with circuits that
contained control flow instructions. Previously, if you attempted to
serialize a circuit that contained control flow instructions it would
error as qpy didn't support embedding the circuit blocks as parameters
for the instructions. This has been fixed and other missing pieces for
fully reconstructing circuits with control flow were added. This
requires bumping the version string to v4 because to accurately
reconstruct control flow circuits we needed to be able to represent
registers that were only partially present in a circuit fully. V4 also
adds a representation for ranges which didn't exist in earlier versions.

Fixes #7583

* Fix handling of standalone register bits in circuit

This commit fixes the failing tests by correcting the handling of
standalone register bits that are part of a circuit (without their
register). Previosuly, if the circuit was solely composed of register
bits but the circuit didn't contain that register, qpy deserialization
would incorrectly add the register to the circuit. This corrects this by
having the deserialization function treat a register that's not in
circuit in the same manner as if it were an out of order register and
add each bit one at a time.

* Add test for for loop with iterator

* Add backwards compat tests for control flow

* Finish release notes

* Fix docs build

* Apply suggestions from code review

Co-authored-by: Jake Lishman <jake@binhbar.com>

* Add handling for ContinueLoopOp too

* Cleanup note about register index array type change

* Use unique doc reference names for qpy module

* Update qpy version 3 ref in release note

* Switch tuple to support any qpy type instead of just ints

This commit changes the tuple formatting to work with any parameter type
that qpy currently supports, not just ints which is what for loop
currently uses a tuple for. This should provide some future proofing as
it enables tuples of other types, which is being considered in the
future, to work without modification.

* Update another broken sphinx ref

* Fix last dangling broken sphinx ref

* Fix yet another dangling sphinx ref

* Fix failing test

* Set fixed Python hash seed in qpy compat tests

This commit updates the qpy compat test runner script to set a fixed
hash seed. The control flow builders internally use sets which means
that to get consistent ordering between python processes we need to
ensure the hash seeds are the same. Without this the argument order
might differ between creating the circuit for qpy serialization and
reconstructing the circuit in another process by deserializing that
qpy data. Setting a fixed hash seed between processes ensures this type
of failure won't happen as the set order will be consistent between
multiple runs of Python.

* Update qiskit/circuit/qpy_serialization.py

Co-authored-by: Jake Lishman <jake@binhbar.com>

* Fix docstring for TUPLE payload

This commit fixes an oversight in earlier commits where the
documentation for the TUPLE payload wasn't actually finished wasn't
really coherent. This commit finishes the documentation to explain how a
tuple is serialized in qpy.

* Add test case using nested tuples

* Directly construct WhileLoopOp in qpy compat tests

In an earlier commit we set a fixed (but still randomly generated) python
hash seed to force multiple invocations of python to use the same hash
seed. This was done in order to fix a non-deterministic failure we were
seeing with the while loop circuit. The while loop context builder uses
a set internally and is sensitive to argument ordering. However, just
hash seed randomization doesn't fix the issue in practice because the
difference isn't between set order strictly, but also that set order
matches the circuit insertion order for what gets written in QPY. To
address this and make the tests work stably this commit switches away
from using the builder interface and instead manually constructs the
while loop and appends it to the circuit with a fixed argument order.
This should hopefully make the compat tests run consistently and fix the
occasional failures we're seeing in CI.

* Fix lint

* Add test case for serializing an empty tuple instruction parameter

* Avoid builder interface for qpy compat tests

Just as was changed for the while loop generator function in the qpy
compat tests this commit updates the other control flow example circuits
to not use the builder interface. The same non-determinism in the
ordering of arguments applies to all the control flow instructions. So
to avoid spurious failures we should explicitly set a arg order and not
use the builder interface.

Co-authored-by: Jake Lishman <jake@binhbar.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 05cbc44)
mergify bot added a commit that referenced this issue Feb 2, 2022
* Fix QPY support for control flow instructions

This commit fixes support for using qpy serialization with circuits that
contained control flow instructions. Previously, if you attempted to
serialize a circuit that contained control flow instructions it would
error as qpy didn't support embedding the circuit blocks as parameters
for the instructions. This has been fixed and other missing pieces for
fully reconstructing circuits with control flow were added. This
requires bumping the version string to v4 because to accurately
reconstruct control flow circuits we needed to be able to represent
registers that were only partially present in a circuit fully. V4 also
adds a representation for ranges which didn't exist in earlier versions.

Fixes #7583

* Fix handling of standalone register bits in circuit

This commit fixes the failing tests by correcting the handling of
standalone register bits that are part of a circuit (without their
register). Previosuly, if the circuit was solely composed of register
bits but the circuit didn't contain that register, qpy deserialization
would incorrectly add the register to the circuit. This corrects this by
having the deserialization function treat a register that's not in
circuit in the same manner as if it were an out of order register and
add each bit one at a time.

* Add test for for loop with iterator

* Add backwards compat tests for control flow

* Finish release notes

* Fix docs build

* Apply suggestions from code review

Co-authored-by: Jake Lishman <jake@binhbar.com>

* Add handling for ContinueLoopOp too

* Cleanup note about register index array type change

* Use unique doc reference names for qpy module

* Update qpy version 3 ref in release note

* Switch tuple to support any qpy type instead of just ints

This commit changes the tuple formatting to work with any parameter type
that qpy currently supports, not just ints which is what for loop
currently uses a tuple for. This should provide some future proofing as
it enables tuples of other types, which is being considered in the
future, to work without modification.

* Update another broken sphinx ref

* Fix last dangling broken sphinx ref

* Fix yet another dangling sphinx ref

* Fix failing test

* Set fixed Python hash seed in qpy compat tests

This commit updates the qpy compat test runner script to set a fixed
hash seed. The control flow builders internally use sets which means
that to get consistent ordering between python processes we need to
ensure the hash seeds are the same. Without this the argument order
might differ between creating the circuit for qpy serialization and
reconstructing the circuit in another process by deserializing that
qpy data. Setting a fixed hash seed between processes ensures this type
of failure won't happen as the set order will be consistent between
multiple runs of Python.

* Update qiskit/circuit/qpy_serialization.py

Co-authored-by: Jake Lishman <jake@binhbar.com>

* Fix docstring for TUPLE payload

This commit fixes an oversight in earlier commits where the
documentation for the TUPLE payload wasn't actually finished wasn't
really coherent. This commit finishes the documentation to explain how a
tuple is serialized in qpy.

* Add test case using nested tuples

* Directly construct WhileLoopOp in qpy compat tests

In an earlier commit we set a fixed (but still randomly generated) python
hash seed to force multiple invocations of python to use the same hash
seed. This was done in order to fix a non-deterministic failure we were
seeing with the while loop circuit. The while loop context builder uses
a set internally and is sensitive to argument ordering. However, just
hash seed randomization doesn't fix the issue in practice because the
difference isn't between set order strictly, but also that set order
matches the circuit insertion order for what gets written in QPY. To
address this and make the tests work stably this commit switches away
from using the builder interface and instead manually constructs the
while loop and appends it to the circuit with a fixed argument order.
This should hopefully make the compat tests run consistently and fix the
occasional failures we're seeing in CI.

* Fix lint

* Add test case for serializing an empty tuple instruction parameter

* Avoid builder interface for qpy compat tests

Just as was changed for the while loop generator function in the qpy
compat tests this commit updates the other control flow example circuits
to not use the builder interface. The same non-determinism in the
ordering of arguments applies to all the control flow instructions. So
to avoid spurious failures we should explicitly set a arg order and not
use the builder interface.

Co-authored-by: Jake Lishman <jake@binhbar.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 05cbc44)

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working mod: qpy Related to QPY serialization stable backport potential The bug might be minimal and/or import enough to be port to stable
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants