-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Annotation serialisation framework #14439
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
Conversation
d465b75
to
a09da46
Compare
Pull Request Test Coverage Report for Build 15451537032Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
9ef71a7
to
e0ce5f0
Compare
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 looks pretty solid to me.
The one concern I have that ties in closely to the control flow oxidation stuff I'm working on is the Rust-side representation of annotations. We're going to need to use a Py<PyAny>
to represent these, and since you've got them as fields of BoxOp
as opposed to params
, they'll end up roughly here, directly on the ControlFlow
operation enum: OperationRef::ControlFlow(ControlFlow::Box { annotations, ... })
. At the moment, ControlFlow
derives PartialEq
, but with a Py<PyAny>
inside, we'd have to implement it manually and acquire the GIL (if we want it implemented at all).
If you stick it into params
, that shouldn't be an issue since we already compare those with the Python
token. I've got a ControlFlowView<'a>
enum FWIW that ties together what we store in the operation (ControlFlow
) and the params into a unified view:
pub enum ControlFlowView<'a, T> {
Box(Option<&'a Duration>, &'a T),
BreakLoop,
ContinueLoop,
ForLoop {
indexset: &'a [usize],
loop_param: Option<&'a PyObject>,
body: &'a T,
},
IfElse {
condition: &'a Condition,
true_body: &'a T,
false_body: Option<&'a T>,
},
Switch {
target: &'a Target,
cases_specifier: Vec<(&'a Vec<CaseSpecifier>, &'a T)>,
},
While {
condition: &'a Condition,
body: &'a T,
},
}
Separately, I'm curious how you envision annotations being exposed to Rust in the future. Any thoughts there?
I'll have another look once you've got the tests and remaining documentation TODOs sorted.
qubits, | ||
clbits, | ||
copy=False, | ||
) | ||
if body_or_annotations is ...: | ||
annotations = () if annotations is ... else annotations |
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.
Very minor, but should this be []
instead of ()
? Since I think we call list
on the annotations coming in to the BoxOp
.
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.
list
always creates a new list from its input, so it doesn't make much difference, and has a specialised fast path for conversion from tuple. The empty tuple is a singleton in CPython, so actually this is probably slightly faster because it avoids an intermediate allocation.
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.
also, I probably originally used ()
as the kwarg default (since it's immutable and safe, unlike []
), then realised that I needed to be able to tell if the object had been passed or not, and ()
is a valid value
namespace: the namespace that this serializer was accessed under. This may be an | ||
ancestor of the annotation. |
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.
Just curious, what would the expected use case for this be? Would an implementation decide to return NotImplemented
in some case based on this, or would we expect this to be part of the payload they write out?
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 idea of namespaces having .
-separated identifiers is just straight from the OQ3 spec, and then I leant into it in the rest of the design. It's nice, especially in conjunction with the serialisation state in QPY, because you can have a single serialiser object that can handle multiple different types of annotation, and use a shared state for all of them. It gives you a framework for having multiple Annotation
subclasses that share some higher logical state (like references to pulse channels or noise models, maybe), so you only want a single serialiser, and the namespace lets you distinguish things.
The NotImplemented
thing came from when I was intending to have a catch-all serialiser for QPY that looked things up using globally-configured OpenQASM 3 ones, and so the exception is managed as a proper type state during serialisation/deserialisation, which will make it rather cheaper to handle when moving down to Rust. I didn't want that to be a special case, so I just said it's available at all levels of the lookup - I can imagine a situation where you might have a "specialised" serialiser that handles only some entries in a dotted namespace, and you want a general serialiser for the whole namespace as a backup.
"""Use the custom serializers to construct an annotation object.""" | ||
for namespace in iter_namespaces(annotation.namespace): | ||
if (serializer := self.annotation_serializers.get(namespace, None)) is not None and ( | ||
payload := serializer.dump(annotation) |
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.
Are there any checks we should do on the payload to make sure it's supported by QASM? Disclaimer: I haven't looked at the QASM spec to see what's 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.
It just has to be a single line of valid utf-8 text. Python should enforce valid utf-8 by nature of the return value being str
, and I just didn't include the check for \n
since imo that'd be something that should live in the test suites of the custom serialisers.
struct INSTRUCTION; | ||
uint8_t name[name_size]; | ||
uint8_t label[label_size]; | ||
uint8_t register[conditional_reg_name_size]; (1) |
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.
Should this (1)
be after struct INSTRUCTION
instead?
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'd intended it to be a note read as part of understanding the line it's actually on - I can say from just having used the note myself during debugging that it at least made sense to me haha. I can see the argument for it being on the struct INSTRUCTION
line if you prefer.
if (serializer := self.potential_serializers.get(namespace, None)) is not None: | ||
if (out := serializer.dump_annotation(annotation)) is not NotImplemented: | ||
del self.potential_serializers[namespace] | ||
index = len(self.serializers) |
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.
Should we have size checking on the number of serializers to make sure we don't exceed the width of a u32
? I don't think we do that for anything else in QPY though, anyway.
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.
that's like 4 billion or something - I think you'd have larger problems if you were exceeding that
Longer term, I want In Rust, I will probably want to hide the list of annotations in the sort of "extra attributes" indirection we used to have for As to representation of I don't have the full Rust implementation fleshed out - it's very similar to the problem about how we'll represent custom gates in a way that isn't Python-specific, so e.g. Rust (including Qiskit itself) can put temporary custom objects into the circuit. I have rough ideas, but for the time being, I'm just throwing it into the "sort out with the other custom stuff" bucket, since annotations are needed by the primitives teams in the short term. |
e0ce5f0
to
383a676
Compare
This adds the basics of support for a custom annotation framework to Qiskit. An `Annotation` is a piece of custom-defined information (from Qiskit's perspective; it's most likely to be transpiler-pass defined, even downstream) that is localised to a particular instruction or scope. Currently, the only supported location for use of annotations is `box`. The meaning of the `Annotation` may currently be completely custom. We expect in the future to have a coarse set of validity ranges that annotations associate themselves with, to allow the transpiler to enforce that annotations are not violated by compiler passes that do not understand them. For now, it's up to the user not to call compiler passes that invalidate annotations. Annotations are closely related to the OpenQASM 3 concept of the same name. Serialisation to and from OpenQASM 3 and QPY is supported by additional user-defined classes, whose interfaces are defined in `qiskit.circuit.annotation`. QPY version 15 is introduced, which makes it a collaborative serialisation format, with well-defined slots in the output stream for custom serialisers to store their data, including state. OpenQASM 3 annotation serialisers must be stateless, since that language does not provide (nor intend) the same arbitrary co-operation. QPY has this additional support for statefulness because we anticipate larger amounts of data being transferred in annotations in QPY format for subsequent processing.
383a676
to
6339ff5
Compare
Ok, that should be the last time I force push to this branch - I'll just do everything additively from now. The major changes in the last force push were to modify how the QPY serialiser subclasses work - previously they were passing a namespace to all four of the methods, but actually, the way the interface is designed, it was only possible for a single look-up key to be associated with any one instance, so half of the passing was irrelevant. As a note: the current implementation makes a new annotation-state context for each subcircuit; we don't share the annotation state when we enter a |
Qiskit/qiskit-qasm3-import#50 adds an implementation of annotated boxes, and provides an API that this library can then wrap. I tested that PR against this branch. |
This does a small amount of version negotiation with `qiskit_qasm3_import`, which in turn checks the version of Qiskit.
Changes within PARAM_EXPR_ELEM_V13 | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
The struct itself is unchanged. However, for a ``PARAM_EXPR_ELEM_V13`` representing a | ||
:meth:`.ParameterExpression.subs` call (with ``op_code = 15``, and therefore ``lhs_type = 'p'`` and | ||
``rhs_type = 'n'``), the trailing :ref:`qpy_mapping` now maps keys of the raw bytes of the | ||
:class:`.Parameter` UUIDs to the substituted values. Previously (in QPY versions 13 and 14), this | ||
mapping stored the parameter names as the keys. | ||
|
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 looks much better than the original wording. It's also nice to have more changes that justify a QPY version change, instead of a tiny detail
One or more of the following people are relevant to this code:
|
from qiskit.circuit import annotation | ||
from test import QiskitTestCase # pylint: disable=wrong-import-order | ||
|
||
# Most of the tests for this module are elsewhere, such as for OpenQASM 3 serialisation being in the |
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.
# Most of the tests for this module are elsewhere, such as for OpenQASM 3 serialisation being in the | |
# Most of the tests for this module are elsewhere, such as for OpenQASM 3 serialization being in the |
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.
Thanks Abby - this particular one is just a code comment, and you're fighting a losing battle trying to localise those (I do my best in public docs, but there's a limit when it's just me "talking" in comments).
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 hear that! Copyediting is a constant losing battle, so I'm used to that :D Forgive me when I made such edits, but the American spelling per IBM Style guidance is branded in my brain.
@@ -3139,3 +3139,102 @@ def test_old_alias_classical_registers_option(self): | |||
] | |||
) | |||
self.assertEqual(dumps_experimental(qc, allow_aliasing=True), expected_qasm) | |||
|
|||
def test_annotations(self): | |||
"""Test that the annotation-serialisation framework works.""" |
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.
"""Test that the annotation-serialisation framework works.""" | |
"""Test that the annotation-serialization framework works.""" |
triggered_not_implemented = False | ||
|
||
class Serializer(annotation.QPYSerializer): | ||
# The idea with this family of serialisers is that we say the "value" we expect to see |
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 idea with this family of serialisers is that we say the "value" we expect to see | |
# The idea with this family of serializers is that we say the "value" we expect to see |
Abby: thanks for the effort with the localisation. In general, code comments (starting I try my best with the spelling when I'm writing public-facing documentation, but it's a (small) effort for me that I just don't bother with when it's not for publication - most of our test docstrings and code comments are barely even grammatically correct... If I were you, I wouldn't waste time looking at those. Comments on the public ones (docstrings in |
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 looks good to me, thanks! Just one minor comment in the tests.
Longer term, I want annotation to be a property of PackedInstruction not a special behaviour of box
...
In Rust, I will probably want to hide the list of annotations in the sort of "extra attributes" indirection we used to have for condition/unit/duration/label, and then got reduced down just to label, because we expect the use of it to be rare relative to instructions.
Ah alright, that sounds good then. I would've imagined the same regarding how we'd store them on PackedInstruction
.
The other context you've provided on the future of this in Rust is helpful.
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.
LGTM!
* Add `Annotation` base class and `BoxOp` support This adds the basics of support for a custom annotation framework to Qiskit. An `Annotation` is a piece of custom-defined information (from Qiskit's perspective; it's most likely to be transpiler-pass defined, even downstream) that is localised to a particular instruction or scope. Currently, the only supported location for use of annotations is `box`. The meaning of the `Annotation` may currently be completely custom. We expect in the future to have a coarse set of validity ranges that annotations associate themselves with, to allow the transpiler to enforce that annotations are not violated by compiler passes that do not understand them. For now, it's up to the user not to call compiler passes that invalidate annotations. Annotations are closely related to the OpenQASM 3 concept of the same name. Serialisation to and from OpenQASM 3 and QPY is supported by additional user-defined classes, whose interfaces are defined in `qiskit.circuit.annotation`. QPY version 15 is introduced, which makes it a collaborative serialisation format, with well-defined slots in the output stream for custom serialisers to store their data, including state. OpenQASM 3 annotation serialisers must be stateless, since that language does not provide (nor intend) the same arbitrary co-operation. QPY has this additional support for statefulness because we anticipate larger amounts of data being transferred in annotations in QPY format for subsequent processing. * Complete documentation of subclassing * Fix circuit equality of boxes * Add support for annotations in `qasm3.load` API This does a small amount of version negotiation with `qiskit_qasm3_import`, which in turn checks the version of Qiskit. * Include comment on equality * Add OpenQASM 3 import/export tests * Fix previous parameter-expression QPY documentation * Support `BoxOp` in `GenericBackendV2` * Fix annotation handling in `BoxOp.replace_blocks` * Add transpilation integration test * Fix GenericBackendV2 test * Add `iter_namespaces` test * Add QPY tests * Add release note * Fix parameter documentation * Make test logic errors exceptions
This is a roll-up of the current description of the annotation framework. I'll be back later today to finish it up. All of the hard work and design work is done, I just have to finish writing a bit more docs, write the QPY bit of the implementation (which will be small, like the OQ3 implementation, because all the complexity is pushed to users), and the tests (which will spill).
Summary
Implementation list:
Annotation
documentation I just noticed are still thereqiskit-qasm3-import
, and isn't so urgent)Details and comments