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

Support extern gate declarations #507

Open
jlapeyre opened this issue Jan 13, 2024 · 16 comments
Open

Support extern gate declarations #507

jlapeyre opened this issue Jan 13, 2024 · 16 comments
Labels
enhance/change spec Semantic changes to language, not clarification

Comments

@jlapeyre
Copy link
Contributor

jlapeyre commented Jan 13, 2024

There is no way to declare a gate without defining it.
In particular, there is no extern for gates.

EDIT: The discussion of stdgates.qasm stdgates.inc here is a red herring, because it is required to be supported in a precise way. This issue is relevant for other, non-standard, gate libraries.

stdgates.inc is expected to contain definitions. For example Qiskit's stdgates.inc contains definitions in the terms of the two built in gates. When importing into Qiskit, there is, in the typical case, no need to parse the definitions. A declaration will do.

At the moment, it seems gate rz(t) q {}, is good enough. But it has to be interpreted specially by Qiskit, rather than as a gate that does nothing.

If this were C, you would be required to include extern declarations and then link to a library. But details of linking and the library are not part of the language. For example #include <stdio.h> does not require explicit linking in gcc. (I am not sure that the last two sentences are complete and/or correct) So I think can allow extern for gates and then your compiler/transpiler toolchain is responsible for ensuring that definitions are found.

See also

@jlapeyre
Copy link
Contributor Author

@hodgestar pointed out that the relation of such an extern declaration to gate and defcal definitions is perhaps problematic. At a minimum, it needs clarification.

@jlapeyre jlapeyre added enhance/change spec Semantic changes to language, not clarification enhancement default GH label and removed enhancement default GH label labels Jan 24, 2024
@jlapeyre
Copy link
Contributor Author

implementations (of parsers, compilers ?) should behave as if "stdgates.qasm" were present, and is if it contained a precise collection of declarations. So the present issue is not relevant for include "stdgates.qasm";.

However, some backend might provide other gates, but for some reason (there are several possible) does not want to expose their definitions in an API. In this case, we need an organized way for the compiler to allow valid use of myspecialgate(r, s, t) q0, q1. That is, a generic frontend has to handle this without knowing anything about the backend.

In this case, extern for gate declarations is still useful. Using something like

gate myspecialgate(r, s, t) q0, q1 {}

is a fragile hack that confuses two concepts.

  • For example, a user may be debugging or developing and comments out the body of a gate that is used frequently. If we want to allow this (and the spec does allow it) then a frontend can't know whether this is meant to be just a declaration or is also a definition. It will have to create a gate object, including an empty body, which is heavier than just recording the name and number of parameters.

  • Another example. This makes optimizations that occur before whatever level has knowledge of myspecialgate. More generally, in many places code would be required to treat gates with empty bodies specially.

@jwoehr
Copy link
Collaborator

jwoehr commented Mar 22, 2024

An extern keyword isn't needed (extern means something different in C ... a simple prototype doesn't need a keyword) if it is decided that a declaration of the form:
gate myspecialgate(r, s, t) q0, q1;
is a prototype ... I think?!
So all you need is a linking regimen for the final definition of the prototyped gate.

@jlapeyre
Copy link
Contributor Author

My main concern is that there be an unambiguous way to spell it. At first glance, it looks like

gate myspecialgate(r, s, t) q0, q1;

is indeed a good choice. We should think carefully about whether to require extern, or if omitting it is allowed, but has different semantics (or leave room for differing semantics in the future). I don't much like the idea of making extern optional as it is for C functions.

I imagine that how linking works will depend on the details of particular tool chains.

@jwoehr
Copy link
Collaborator

jwoehr commented Mar 22, 2024

Good points, @jlapeyre

I would point out that extern has a somewhat different (overloaded, as usual) meaning in C, with functions referring to code bodies defined in a different language (hence with a different calling convention).

A gate definition in OpenQASM 3 is somewhat like a function declaration in C. It seems to me semantically unambiguous to treat gate declarations that lack definition bodies like C function prototypes. Or am I missing the point?

@jlapeyre
Copy link
Contributor Author

jlapeyre commented Mar 23, 2024

It seems semantically unambiguous to me too. I'm just not super confident that I am not overlooking something.

I was trying to think of any possible conflicting future change... maybe something like extern gate mygate q; means the gate is defined by the backend (or some qasm file that has in some sense has been compiled). And gate mygate q; is a forward declaration that means the gate is defined later in the compilation unit or in a later included file. It may not be a good example. In C, there is no distinction in these cases. At link time the linker follows some rules for searching for symbols and finds them wherever they are.

@hodgestar
Copy link
Contributor

@jlapeyre What are the use cases of such an undefined gate you have in mind?

A gate specifies a unitary operation to be performed, not how the gate should actually be implemented. So gates are from one point of view already all external functions that specify the "API" they provide (i.e. the unitary they are supposed to perform).

Is there a good use case for a gate whose behaviour is completely undefined? And why not just define the unitary?

I guess what is missing is a way to specify gates whose behaviour is either not unitary or not entirely specified by the unitary operation. For example, one cannot define a reset or measurement gate in OpenQASM.

I'm not sure whether OpenQASM should be extended to include this or whether this belongs in the pulse level language.

My own imagined use cases:

Perhaps extending gates in this way is a good idea for OpenQASM. E.g. It might be worthwhile to express extern gate active_reset q; or extern gate sequential_x q { x q; }. However, from a compilation perspective, these are really closest to extern defcal ... because the semantics of "the compiler may choose to make this unitary happen in whatever way it considers optimal" are gone.

@jlapeyre
Copy link
Contributor Author

I think we may be talking about two different things. Let me give an example of what I am talking about.

I want to run a circuit specified in OQ3 on a backend. Suppose for some reason I need, as an intermediate step, to represent the circuit as as a Qiskit QuantumCircuit object. Maybe the backend requires it, or maybe there are some passes in the Qiskit compiler that I really want to use. Or maybe I only want to run some compiler passes and examine the resulting circuit because I am researching how to construct and transform circuits.

Suppose I want to use a cx gate in my research. This is easy, I can write

include "stdgates.inc";
qubit q0;
qubit q1;
cx q0, q1;

What happens when I send this to Qiskit? It is approximately the following. Qiskit will use the new rust "parser" as the compiler frontend. The frontend is meant to work with many backends, so it doesn't know about Qiskit. When the frontend sees the include statement it does nothing more or less than make entries in its symbol table, one of which notes that there is a gate called cx that takes two qubit parameters and no classical parameters. Then when the frontend encounters the gate call, it checks the name and number and types of parameters, finds they are ok, and proceeds. The frontend passes the instruction cx q0, q1 as some kind of data structure. Qiskit sees a gate call from a gate with an integer ID. It looks up this number in the symbol table, finds that the gate is called "cx" and applies its representation of this gate in some way to a circuit, maybe a matrix, or just a symbol, or as a sequence of control pulses.

Now suppose instead that I did the following:

qubit q0;
qubit q1;
cx q0, q1;

In this case, long before Qiskit sees anything, the frontend will record an error saying that the gate cx is undefined because it tries to look up a gate symbol that doesn't exist. And there is more analysis that will be done by the frontend. If any errors are reported, Qiskit makes them available to the user and never even begins to build a circuit.

Now suppose there is some other gate in the Qiskit library that I want to use, say ISWAP. This is not in the OQ3 standard library. I send to Qiskit this:

include "stdgates.inc";
qubit q0;
qubit q1;
iswap q0, q1;

Recall that the frontend doesnt know anything about Qiskit. The frontend sees that iswap q0, q1 has the syntax of a gate call and tries to look up the symbol for iswap to verify that the user gave the correct number and types of arguments. It finds none and so records the error and sends errors and the circuit as a structure to Qiskit. Qiskit reports the errors to the user and never starts constructing a circuit.

The way that this is typically solved for compiler tool chains is to allow a declaration of the minimal properties of iswap in the QASM3 file. So I would write something like

gate iswap a, b;
qubit q0, q1;
iswap q0, q1;

And the OQ3 spec would say that this declares that the symbol iswap represents a gate that takes two qubit params and no classical params. This is all that is needed for the basic semantic analysis. The spec would further say that other parts of the toolchain or compiler are responsible deciding which particular definition to use.

If you really don't want gate declarations, then you have design a particular, ad hoc, beside-the-spec convention for a particular backend.

Here are some ideas

First, regardless of solution, even using declarations as proposed, in order to specify that we want to "link" to Qiskit's extended standard library, Qiskit defines a pragma or allows flags to its import function. Or maybe just decides that it is always linked by default.

  • Do something, whatever you want, in the QASM program to make is semantically valid, so no errors, and Qiskit will ignore what you did. So do supply some QASM definition of iswap, and Qiskit will ignore it. Or since it is ignored, just use the syntactially and semantically correct gate cx a, b {};. But this is also worse than a proper declaration:
    1. Requiring the semantic analysis to construct a gate is a heavier and slower process than reading a declaration.
    2. This precludes using efficient passes closer to the parser, such as gate cancellation, etc. Passes close to the frontend have to assume that gate cx a, b {}; is an actual definition; for example they may elide a call because it is evidentally a no-op. So Qiskit can't reliably use any part of a tool chain that is designed to lie between the frontend and the backend.

Following is a pretty bad solution, you can probably skip it.

  • In order to specify that we want to "link" to Qiskit's extended standard library, Qiskit defines a pragma or allows flags to its import function. Or maybe just decides that it is always linked by default. Then Qiskit filters through the error messages deciding which are real errors, and which are spurious because qiskit-library gates are not defined. Qiskit repeats some of the semantic analysis. It uses efficient symbols for gates that were defined in QASM, but if it finds an undefined gate symbol, it goes back to the error message, finds the byte range and retrieves the gate name, checks if it is in the standard libary and uses the strings rather than symbols. This complexity is handled everywhere in the importer. There are a number of further problems with this solution. To name one, a circuit with thousands of calls to an undeclared gate will generated thousands of errors.

This response has not been succinct, but I hope it sheds some light on the problem I see.

@hodgestar
Copy link
Contributor

@jlapeyre Thank you for the explansion! I am fine with long.

In C if one wanted to link to an external function one would supply a header with a forward declaration. In OpenQASM that head is currently the full gate definition. I.e. From the language perspective the unitary behaviour is currently part of the signature of the gate and the implementation is always somewhere else.

Currently gate X q {} defines X to be the identity gate.

I'm not entirely against having "extern" gates gate X q; that are opaque to the compiler, but it does seem a little odd that gate X q; or extern gate X q; is not the same as including a full gate definition from some unspecified include file.

In the end, what you're describing seems to be a request to the compiler to use an opaque get implementation that will be supplied later and currently that is closest to extern defcal ....

Perhaps in the end the question is: Do we add extern gate to OpenQASM 3 or extern defcal to OpenPulse or even OpenQASM?

@jlapeyre
Copy link
Contributor Author

Currently gate X q {} defines X to be the identity gate.

I agree this is the meaning according the spec. And I would prefer that it stay that way. Any other meaning would be a hack agreed on between the QASM programmer and a particular compiler. Currently we use this hack for the Qiskit importer. I also use it all over the place in tests of the frontend as well, because I need to express the protocol for a gate without its definition and I have no better way to do it.

But, at least for gates in the standard library, this hack will no longer be necessary in the frontend and importer because of the recent PR Qiskit/openqasm3_parser#185 . This PR is in line with the spec and is practical.

I think one difficulty is that OQ3 is intended to be used for a broad range of goals. It seems that being able control quantum hardware at a low level is essential to its main use case. But it's only part of its main use case, if I understand correctly. For example here:

// Defcal using frames from backend.inc enabling the calibration
// to "plugin" to the existing calibrations.
defcal Y90p $0 {
waveform y90p = drag(0.1-0.2im, 160dt, 40dt, 0.05);
play(q0_frame, y90p);
}
// Teach the compiler what the unitary of a Y90p is
gate Y90p q {
rz(-pi/2) q;
sx q;
}

I guess that the low-level description is supplied in case the backend wants to actually send this gate to specific hardware (and recently calibrated!) And the more abstract description is supplied in case, at some level above the pulse descriptions, the compiler wants to reason about the gate and account for it in optimizations.

But there are many valid use cases that do not need both of these definitions. Many users who don't know or care about calibrations might use OQ3 as an input format to Qiskit or some other compiler. Maybe they have a program that exports circuits as OQ3. If such a user wants to use gates in the standard library they don't need to supply a definition in OQ3, (this will be soon be made official in the OQ3 spec). They trust that Qiskit knows best how to represent the standard gate at all levels, for optimization, as well as pulses.

Here is a feature that I imagine might be very attractive to users; I expect someone might ask for it. (I am repeating what I wrote in a previous comment, but with a different angle) The user asks "I want to send an OQ3 circuit to Qiskit that uses a gate from Qiskit's library that is not in the OQ3 standard library. How can I do this?". It's important to note that this user does not know or care about gate calibrations.

I would implement this feature in the Qiskit importer as follows.

Implementation with no gate declarations without definitions allowed in OQ3

First, I would provide a flag in the importer that "links" the qiskit (extended) library. This request to link means the following. The importer reads statements as some data structure from the frontend. Every time it sees a gate call, it checks if it has seen this gate before (the gate is identified by an integer). If not, it checks the extended library. If it does not find the gate name there, it looks for a gate definition in a previously read statement from the frontend. If it does not find a definition, importing halts with an error. If it did find a definition, it caches the location for reuse the next time it sees a call to the same gate.

Second, I would instruct the user to include in their OQ3 file a definition for the gate like this

gate iswap a, b {}

I would explain to the user that they could put anything in the body that they want, but that the cleanest, and least wasteful of resources, is to leave the body empty. The user trusts that Qiskit knows how best to represent this gate in any particular situation. But the user must include a definition, otherwise semantic analysis in the frontend will fail.
Suppose we instead require that the user supply a real definition. The user can't guess about the best representation. Perhaps the best solution is for Qiskit to supply a file with definitions corresponding to its extended library. But, by far the best thing for the importer to do with these definitions is to ignore them. Qiskit already has the most suitable definitions internally, far from OQ3. It gains absolutely nothing by having definitions that came in via OQ3.

This would be a fairly workable solution. But here are some disadvantages.

  • The OQ3 code is not portable. Perhaps the compiler from another company also provides a library with the same gate iswap. But this company's rule is that you must include a gate definition with an annotation that specifies its library name. The spec does not require, but sort of suggests that a compiler should ignore unknown annotations. So I suppose a single source file might work for both compilers. But this is clearly not ideal.

  • Back to qiskit, say the user wants to use a gate in the extended library, and also another gate that they define in their OQ3 file. But the user does not realize that a gate with the same name as this second gate is in the extended library. According to the agreement above, Qiskit has to ignore the user's definition of the second gate. The user is mystified by the misbehavior of the compiler, because they don't realize that a different definition is being used.

Implementation with simple gate declarations allowed.

Now suppose that OQ3 does allow gate declarations with no definitions. I would implement access to the Qiskit extended library like this:

I would not need to instruct the user on how to write the OQ3 input file. Because this is now part of the spec. The user would would write for example

gate iswap a, b;

...

iswap q[0], q[3];

Alternatively Qiskit could provide an include file with these definitions. The include file would contain lines like gate iswap a, b;. Then the user would do this:

include "qiskit_extended.inc";
...
iswap q[0], q[3];

I would again instruct the user to set a provided flag to link the Qiskit extended library. But the linking would work differently. All the disadvantages in the implementation above would disappear, and there would be additional advantages. For example

  • The input file, or at least its format would be standard, and more portable. For the most portable code, and assuming that two backends have iswap, the user would explicitly write gate iswap a, b;.

  • A spelling error on the part of the user, say gate ISWAP a, b; would happily cause an error at link time. This error would not and could not occur in the first implementation. In that case gate ISWAP a, b {}; would silently be treated as an identity gate. (Or as whatever else the the user put in the body, including a valid, but non-optimal definition of iswap.)

  • This would be more efficient. The compiler would not have to waste resources compiling useless gate definitions.

  • A bug mentioned above would not occur. This is the case where the user defines a gate without realizing that it is also in the extended library. The linker would find two definitions (i.e. two resolutions) of the gate one in the OQ3 stream and one in the extended library, and raise an error.

@hodgestar
Copy link
Contributor

This all seems to have gotten way too complicated. Could you do:

include "qiskit_extended.inc";
...

and completely define the gates fully (i.e. specify the unitary they implement) in qiskit_extended.inc? And if you didn't want the user to have to use the include provide a compilation flag that says "include qiskit_extended.inc"?

@hodgestar
Copy link
Contributor

Aside 1: Defining a gate by just its name seems a bit fraught. There are already varying conventions around details of how a gate with a particular name is defined.

Aside 2: Are there any Qiskit gates that should just go into stdgates.inc?

Aside 3: Regarding the defcal Y90p $0 { ... }, gate Y90p q { ... } -- QuTiP pulse simulator engine already performs some optimizations where pulses are reordered using knowledge of the unitary they implement. This is quite a cool feature to be able to support.

@jlapeyre
Copy link
Contributor Author

Could you do: include "qiskit_extended.inc";
and completely define the gates fully (i.e. specify the unitary they implement) in qiskit_extended.inc?

Yes. That is more or less one of scenarios I mentioned above. It puts a burden on the user, a burden on the designer of the importer, and introduces a class of bugs. At the same time, it provides no advantages over allowing extern declarations. Also, since the supplied definitions are completely ignored, the optimization of this process (which the user will certainly do once they realize it works) leads to supplying empty definitions.

One might instead insist on the point and require the that user supply full gate definitions, and require that the importer (Qiskit, tket, etc.) does not discard these definitions but rather uses them. This still puts a burden on the user. The user has to choose a representation of the unitary, make sure it is coded correctly, and possibly optimize it. It doesn't put a burden on the importer (Qiskit, tket, ...) because they have to support gate definitions in any case. But now instead of being explicitly instructed to use the gate in the Qiskit library called iswap, the importer receives a gate that happens to be named iswap and that has a definition. Qiskit's (or another compiler's!) native iswap implementation may have several representations optimized for different purposes, as well as a collection of properties and templates that can be used for optimizations. The gate in the OQ3 code that is also named iswap enjoys none of these advantages. It can only be optimized to the extent that Qiskit can analyze any custom gate.

Defining a gate by just its name seems a bit fraught. There are already varying conventions around details of how a gate with a particular name is defined.

I agree 100%. I'm not in favor of OQ3 associating unitaries with gate names beyond what is already done for builtins and the standard library. In the present case, I declare a gate gate iswap a, b;, and then I instruct the importer to add its extended library to search path for undefined gates. I've read the library documentation so I know that iswap does what I want. Similarly, if I add a declaration void fft(int n, complex double *input, complex double *output) to my C code, it's my responsibility to make sure that my code and the library I link to assume the same normalization convention.

I might use a tool that manipulates circuits and exports OQ3. And the tool includes a gate called ISWAP that has the same unitary as Qiskit's iswap. In order to import into Qiskit, I need a facility in either the importer or the exporter that allows me to specify a translation dictionary. So I would say ISWAP is replaced by iswap and link to the extended library.

If I use a circuit-creation tool that exports SOMEGATE, and know that Qiskit supports a gate somegate that has the same unitary up to a phase, then a good solution might be.

include "qiskit_extended.inc";

gate SOMEGATE q {
    somegate(q);
    gphase(pi / 4);
}

And of course, again, tell the importer to use the extended library so that I know exactly what somegate means. I imagine I'll still get some or all of the optimizations that I would get if the two gates were actually the same.

Are there any Qiskit gates that should just go into stdgates.inc?

I don't know of any, but I haven't reviewed them either. Ideally before including another gate, I'd like to see that it is widely used by a number of compilers, Qiskit, tket, bloquade, what have you.

How urgent is this?

From my perspective, implementing extern gate declarations is not urgent. Being able to effectively have extern declarations for the standard library takes the pressure off. For other gates, I can continue to use the hack of empty definition wherever possible.

If OpenQASM 3 becomes more widely used as an exchange format, extern gate declarations will be forced on us. Further development of the rust front end and the Qiskit importer may also make the issue more important.

But since we will have to face this sooner or later, I think it's a good idea to start thinking about it now.

@hodgestar
Copy link
Contributor

hodgestar commented Apr 15, 2024

Summarizing my current view, partly for myself:

  • I would be in favour of extending OpenQASM 3 to support extern defcal ... or defcal without a body independently of the pulse level language. This provides a clean way to declare expected pulse level gate implementations without requiring the use of OpenPulse or any other pulse level language.
  • Compilers are then free to provide linking functionality that will provide such gates if they're not supplied.
  • We don't have namespacing for such library functions and includes, so we'll have to live with the possibility of clashes for the moment.
  • For gates, the gate body is not an implementation of the gate, but rather part of its specification. A gate as envisioned by the spec does not have an implementation and must be provided by some other means. I would be in favour of specifying opaque / extern gates via the opaque / extern defcal extension mentioned in my first bullet point.

@blakejohnson
Copy link
Contributor

I believe our intent when removing the opaque keyword in moving from OQ2 to OQ3 was that if a user wants to define a symbol for the parser to reason about (e.g. my_gate) without supplying its unitary definition, they would write something like:

defcal my_gate(angle param1) q1, q2 {}

I think the above discussion is about something else, though – namely, connecting to an external tool's gate library while avoiding some kind of unitary matching and instead wishing to match by label instead. I agree that for performance reasons a user may desire this behavior, but I don't see why new language semantics would be required to achieve it. Instead, having your importer (optionally) inject a

include "qiskit_gates.qasm";

so that those symbols are guaranteed to exist seems sufficient.

@levbishop
Copy link
Contributor

I think while in general the contents of defcal are defcalgrammar-specific, having a grammar-independent way to indicate external linkage is valuable. defcal ... ; with no body and extern defcal...; both seem reasonable, with my leaning toward the former. I think given only the parsing of the defcal-body is grammar-specific we can rule on this at the openqasm level and have it apply to all grammars.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhance/change spec Semantic changes to language, not clarification
Projects
None yet
Development

No branches or pull requests

5 participants