Skip to content

Latest commit

 

History

History
963 lines (797 loc) · 59.3 KB

Adaptive_Profile.md

File metadata and controls

963 lines (797 loc) · 59.3 KB

Adaptive Profile

This profile defines a subset of the QIR specification to support a coherent set of functionalities and capabilities that might be offered by a quantum backend. Like all profile specifications, this document is primarily intended for compiler backend authors as well as contributors to the targeting stage of the QIR compiler.

The adaptive profile specifies supersets of the base profile that enable control flow based on mid-circuit measurements and classical computations within coherence times. A backend can support this profile by supporting a minimum set of features beyond the base profile and can opt in for features beyond that.

To support the adaptive profile without any of its optional features, the following capabilities must be implemented:

  1. An adaptive profile program can execute a sequence of quantum instructions that transform the quantum state.
  2. A backend must support applying a measurement operation at any point in the exeuction of the program. Qubits not undergoing the measurement should not have their state affected as if they underwent the measurement. A QIS measurement function, quantum__qis__mz__body or some other measurement function, must be supported in the quantum instruction set of back-end executing an adaptive profile program. Additionally, anrt function to turn a measurement result into a boolean (__quantum__rt__read_result__body) must be in the instruction set.
  3. An adaptive profile program must produce one of the specified output schemas.
  4. Forward branching (the profile must support the br instruction assuming that the instruction only expresses non-loop control flow). Since support for the br instruction relies on the i1 type a small amount of instructions implementing classical logic on i1 types is also assumed. Conditional quantum operations in the instruction set that appear in the target basic blocks of a br instruction must be performed conditionally like in a purely classical LLVM program.

This means that at minimum, backends supporting adaptive profile programs should support mid-circuit measurement, turning measurements into booleans, and branching based on those booleans. Beyond this, a backend can opt into one or more of the following additional capabilities to extend minimal adaptive profile compliance with additional features. A maximal adaptive profile program adds all of the following capabilities. The extended possible adaptive profile capabilities a program can express are:

  1. Classical computations on integers, floating-point numbers, or fixed-point numbers.
  2. IR-defined functions and function calls.
  3. Backwards branching.
  4. Multiple target branching.
  5. Multipe return points.

Thus, any backend that supports capabilities 1-4 and as many of capabilities 5-9 as it desires is considered as supporting adaptive profile programs. An adaptive profile program must indicate what additional capabilities it uses via module flags. Additionally, backends can indicate how they support the various optional capabilities and any caveats on support for the capabilities that they do support via this document. Ideally, static analysis/verification tools should be able to understand what capabilities of the adaptive profile a backend is implementing and should run a verification pass to ensure adaptive profile programs that are using capabilities not supported by a backend are rejected with an informative message. More details about each of the aforementioned capabilities are outlined below.

Mandatory Capabilities

Bullet 1: Quantum transformations

The set of available instructions that transform the quantum state may vary depending on the targeted backend. The profile specification defines how to leverage and combine the available instructions to express a program, but does not dictate which quantum instructions may be used. Targeting a program to a specific backend requires choosing a suitable profile and quantum instruction set (QIS). Both can be chosen largely independently, though certain instruction sets may be incompatible with this (or other) profile(s). The section on the quantum instruction set defines the requirements for a QIS to be compatible with the adaptive profile. More information about the role of the QIS, recommendations for front- and backend providers, as well as the distinction between run-time functions and quantum instructions, can be found in this document.

Bullet 2: Measurements

The second requirement relieves a restriction from the base profile that qubits can only be measured at the end of the program and that no quantum operations can be performed on a measured qubit. The only requirement here is that all qubits must be measurable, but there are no restrictions on when these measurements are made, and there is no restriction necessitating that all qubits be measured. I.E. it's perfectly valid to measure a subset of qubits used in the program, not have all measured qubits in the output, and perform the measurements at different points. A function to turn measurement results into classical data, mid-circuit, is critical to using forward branching and conditional quantum execution. An example of such a function (@_quantum__rt__read_result__body) is below:

%0 = tail call i1 @__quantum__rt__read_result__body(%Result* null)
 br i1 %0, label %then, label %continue

Bullet 3: Program output

There is no change to output specifications with respect to the base profile, sans for the addition of error codes.

The specifications of QIR and all its profiles need to accurately reflect the program intent. This includes being able to define and customize the program output. The base profile and adaptive profile specifications hence require explicitly expressing which values/measurements are returned by the program and in which order. How to express this is defined in the section on output recording.

A suitable output schema can be generated in a post-processing step after the computation on the quantum processor itself has finished; customization of the program output hence does not require support on the QPU itself.

The defined output schemas provide different options for how a backend may express the computed value(s). The exact schema can be freely chosen by the backend and is identified by a string label in the produced schema. Each output schema contains sufficient information to allow quantum programming frameworks to generate a user-friendly presentation of the returned values in the requested order, such as, e.g., a histogram of all results when running the program multiple times.

If a program opts into a classical data type like in Bullet 5, then a program can return a value of this data point by using a return statement and then this error code must appear in the output schema. By default, an entry-point function has a single return statement, but by opting into Bullet 9 values can be returned in many places of an entry-point function.

Bullet 4: Forward Branching

The adaptive profile can allow for arbitrary forward branching with the restriction being that branch instructions cannot express programs that do not terminate. By default, support for backwards branching is not a requirement, though a backend can opt into Bullet 7 to support backwards branching. In the case that Bullet 7 is not opted into, then a simple static analysis can check for cycles in the control flow graph and reject any program with a cycle as invalid. Depending on the vector of capabilities defined by an adaptive profile program (like when Bullet 7 is opted into), proving non-termination with a static analysis may be impossible due to the capabilities forming a Turing-complete subset of LLVM. Therefore, it is a rule that a valid adaptive profile program should not contain non-terminating loops and that a frontend generating a program with a non-terminating loop is generating a program that is not compliant with the adaptive profile specification. However, since there is no static analysis that can prove termination then it is up to backends to enforce termination guarantees via a means of their choosing (for example a watchdog process with a timeout that will kill an executing adaptive profile program if it takes too much time). Different forms of conditional logic can be supported in the profile based on optional features, but at minimum, the simple br branch instruction must be supported.

Although forward branching can be useful when combined with purely classical operations within a quantum program, the real utility is being able to conditionally perform gates as part of things like repeat-until-success or quantum-error-correction routines in quantum programs.

Here is an example program that mixes mid-circuit measurement, a qis function to convert a measurement result to a boolean, and a branch instruction to control how gates are applied based on mid-circuit measurement.

  tail call void @__quantum__qis__h__body(%Qubit* null)
  tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* null)
  tail call void @__quantum__qis__reset__body(%Qubit* null)
  %0 = tail call i1 @__quantum__rt__read_result__body(%Result* null)
  br i1 %0, label %then, label %continue
then:                                   ; preds = %entry
  tail call void @__quantum__qis__x__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  br label %continue__1.i.i.i

continue:
...

Optional Capabilities

Bullet 5: Classical Computations

An adaptive profile program may include instructions or intrinsics for numeric and logical computations that don't involve allocating memory for aggregate data structures. These can include integer arithmetic calculations of a backend-specified width, floating point arithmetic calculations of a backend-specified width, logical operations on integers of a backend-specified width, or operations on fixed-point numbers via intrinsics. Front-ends can use any integer or floating point width when generating code, and it is the responsibility of a backend to provide a compile-time error message if the frontend uses an integer width greater than that which is supported.

When an adaptive profile program indicates that it is using a particular data type, then instruction set (qis) functions or classical (rt) functions that a backend may support can now be called. For example, consider that if a backend supports integer computations and random number generation, then an adaptive profile qir program may have code like the following to do randomized benchmarking:

%0 = call i64 @__quantum__rt__rand_range(i64 0, i64 2)
%1 = icmp eq i64 %0, 0
br i1 %1, label %zero_rand_sequence, label %one_rand_sequence

By combining mid-circuit measurements with instructions on classical data types, you can conditionally apply gates based on logic using multiple mid-circuit measurements and boolean computations:

  tail call void @__quantum__qis__h__body(%Qubit* null)
  tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* null)
  tail call void @__quantum__qis__reset__body(%Qubit* null)
  %0 = tail call i1 @__quantum__rt__read_result__body(%Result* null)
  tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Result* nonnull inttoptr (i64 1 to %Result*))
  tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  %1 = tail call i1 @__quantum__rt__read_result__body(%Result* nonnull inttoptr (i64 1 to %Result*))
  %2 = and i1 %0, %1
  br i1 %2, label %then, label %continue

then:                                   ; preds = %entry
  tail call void @__quantum__qis__x__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  br label %continue__1.i.i.i

continue:
...

An adaptive profile program must indicate which classical data types must be supported to execute it. This is done by setting the following module flags to indicate the widths of various classical types as values for the rightmost section of the module flag metadata, like in the table below. Any program like the previous one that uses instructions on classical data types must have the appropriate module flag set, or it is not a legal adaptive profile program. A module flag can indicate that the program uses multiple classical types by adding multiple "end" fields in the module flags, each an i32 representing a type's width. For example, !{i32 1, !"classical_ints", i32 16, i32 32} indicates that both 16-bit integers and 32-bit integers are used in the adaptive profile program. On the other hand, !{i32 1, !"classical_ints", i32 0} indicates that the program does not use classical integer computations.

LLVM Module Flag Context and Purpose
!{i32 1, !"classical_ints", i32 N} indicates whether the integer i1 and iN data types are used by the program
!{i32 1, !"classical_floats", i32 N} indicates whether a floating point fN data type is used by the program
!{i32 1, !"classical_fixed_points", i32 N} indicates whether fixed-point intrinsics on an iN type are used by the program

For each data type being supported, some corresponding instructions must be supported, as detailed in the table below. For each datatype, any caveats on the ability to support certain instructions or where major differences in the behavior of the backend's execution from the semantics of the LLVM instruction should be noted in this document:

An adaptive profile program using integers as a datatype with width N (for example the following module flag is defined: !{i32 1, !"classical_ints", i32 N}) can use the instructions from the table below on this iN type.

LLVM Instruction Context and Purpose Note
add Used to add two integers together.
sub Used to subtract two integers.
mul Used to multiply integers
udiv Used for unsigned division. Can cause real-time errors.
sdiv Used for signed division. Can cause real-time errors.
urem Used for unsigned remainder division. Can cause real-time errors.
srem Used for signed remainder division. Can cause real-time errors.
and Bitwise and of two integers.
or Bitwise or of two integers.
xor Bitwise xor of two integers.
shl a left bit shift on a register or number.
lshr Shifts a number the specified number of bits to the right. No sign extension.
ashr Shifts a number the specified number of bits to the right. Does sign extension.
icmp Performs signed or unsigned integer comparisons. Different options are: eq, ne, slt, sgt, sle, sge, ult, ugt, ule, uge.
zext zero extend an iM to an iN where N>M
sext signed zero extend an iM to an iN where N>M
select conditionally select the value in a register based on a boolean value
phi assign a value to a register based on control-flow

If an adaptive profile program has support for floating point computations on floats of width N (for example, computations on N-bit floats are supported !{i32 1, !"classical_floats", i32 N}), then the following instructions are supported.

LLVM Instruction Context and Purpose Note
fadd Used to add two floats together.
fsub Used to subtract floats integers.
fmul Used to multiply floats
fdiv Used for floating point division. Can cause real-time errors.

Finally, an adaptive profile program using fixed-point intrinsics on integers of width N (for example, a module flag is set as follows !{i32 1, !"classical_floats", i32 N}), can use the following intrinsics:

LLVM Intrinsic Context and Purpose Note
llvm.smul.fix.* Used to multiply two signed fixed point numbers.
llvm.smul.fix.sat* Same as above but clamps to min/max number in scale.
llvm.umul.fix.* Used to multiply two unsigned fixed point numbers.
llvm.umul.fix.sat* Same as above but clamps to min/max number in scale.
llvm.sdiv.fix.* Used to divide two signed fixed point numbers. Can cause real-time errors.
llvm.sdiv.fix.sat* Same as above but clamps to min/max number in scale.
llvm.udiv.fix.* Used to divide two unsigned fixed point numbers. Can cause real-time errors.
llvm.udiv.fix.sat* Same as above but clamps to min/max number in scale.

Bullet 6: IR-defined functions and function calls

An adaptive profile program may use IR-defined functions and function calls. For example, consider that with IR-defined functions if a backend has a Cnot gate in its instruction set, then a program that liberally uses Swap operations can define a function and call it as follows:

define void @swap(%Qubit* %arg1, %Qubit* %arg2) {
call void __quantum__qis__cnot__body(%Qubit* %arg1, %Qubit* %arg2)
call void __quantum__qis__cnot__body(%Qubit* %arg2, %Qubit* %arg1)
call void __quantum__qis__cnot__body(%Qubit* %arg1, %Qubit* %arg2)
}

define void @main() {
...
call void @swap(%Qubit* null, %Qubit* nonnull inttoptr (1 to %Qubit*))
...
}

Moreover, classical functions can be defined in the IR assuming that a backend has opted into supporting classical computations:

define i64 @triple(i64 %0) {
%1 = mul i64 %0, 3
ret i64 %1
}

define void @main() {
...
%0 = call void @triple(i64 2)
...
}

The only restriction on IR functions and definitions is that you cannot have dynamically allocated %Qubit* arguments, they must still be constant %Qubit* id's. An adaptive profile program using this feature must have a module flag set like the following: !{i32 1, !"IR_functions", i1 true}.

Bullet 7: Backwards branching

Opting into this capability releases the restriction on backwards branching so that a more compact representation of loops can be expressed in programs. Here is a program that implements a loop via a backwards branch that performs coinflips with a qubit and exits the program when the coin flip produces a 1. It is up to a backend to enforce that a program using backwards branching does not cause non-termination.

...
define void @simple_loop() local_unnamed_addr #0 {
entry:
  br label %loop
loop:
  call void @__quantum__qis__h__body(%Qubit* null)
  call void @__quantum__qis__mz__body(%Qubit* null, %Result* null)
  %0 = call i1 @__quantum__rt__read_result__body(%Result* null)
  br i1 %0, label %exit, label %loop
exit:
  ret void
}
...

An adaptive profile program using this feature must have a module flag set like the following: !{i32 1, !"backwards_branching", i1 true}.

Bullet 8: Multiple Target Branching

It can be desirable to support control constructs that indicate how a computation can lead to branching to one of many different control flow paths. Having such a construct exposed in the IR allows for more aggressive optimization considerations where it is easy to gather that gates being performed on the same qubits across different blocks can have no control flow dependencies. As such, a backend can opt into switch instruction support so that more aggressive static analysis and optimization are possible. To make such a construct useful, some amount of integer computation support (Bullet 5) must be supported. In the snippet below, we can imagine that mid-circuit measurement fed into classical computations producing %val and that each target block has conditional quantum operations.

 Implement a jump table:
switch i32 %val, label %otherwise [ i32 0, label %onzero
                                    i32 1, label %onone
                                    i32 2, label %ontwo ]

An adaptive profile program using this feature must have a module flag set like the following: !{i32 1, !"multiple_target_branching", i1 true}.

Bullet 9: Multiple Return Points

As an optional feature, adaptive profile programs can have multiple return points in the entry point function. For example, an adpative profile program can contain code like:

...
define i32 @simple_br() local_unnamed_addr #0 {
entry:
  br i1 label %error, label %exit
error:
  ret 2
exit:
  ret 0
}

An adaptive profile program using this feature must have a module flag set like the following: !{i32 1, !"multiple_return_points", i1 true}.

Program Structure

An adaptive profile-compliant program is defined in the form of a single LLVM bitcode file that contains the following:

  • the definitions of the opaque Qubit and Result types
  • global constants that store string labels needed for certain output schemas that may be ignored if the output schema does not make use of them
  • the entry point definition that contains the program logic
  • declarations of the QIS functions used by the program
  • declarations of run-time functions used for initialization and output recording
  • one or more attribute groups used to store information about the entry point, and optionally additional information about other function declarations
  • module flags that contain information that a compiler or backend may need to process the bitcode. These include module flags that indicate which features of the adaptive profile are used. A back end can list which module flags they support in the following document.

The human-readable LLVM IR for the bitcode can be obtained using standard LLVM tools. For clarity, this specification contains examples of the human-readable IR emitted by LLVM 13. While the bitcode representation is portable and usually backward compatible, there may be visual differences in the human-readable format depending on the LLVM version. These differences are irrelevant when using standard tools to load, manipulate, and/or execute bitcode.

The code below illustrates how a simple program looks within an adaptive profile representation:

; ModuleID = './QSharpVersion/qir/Example.ll'
source_filename = "./QSharpVersion/qir/Example.ll"

; declare fundamanetal quantum types supported by the profile

%Result = type opaque
%Qubit = type opaque

; constants for output labeling

@0 = internal constant [5 x i8] c"0_t0\00"
@1 = internal constant [5 x i8] c"0_t1\00"

; Entry point for teleport chain program utilizaing the minimal adaptive profile + bullet 8

define void @TeleportChain__DemonstrateTeleportationUsingPresharedEntanglement() local_unnamed_addr #0 {
entry:
  tail call void @__quantum__qis__h__body(%Qubit* null)
  tail call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*), %Qubit* nonnull inttoptr (i64 4 to %Qubit*))
  tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*))
  tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*), %Qubit* nonnull inttoptr (i64 5 to %Qubit*))
  tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Result* null)
  tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  %0 = tail call i1 @__quantum__qis__read_result__body(%Result* null)
  br i1 %0, label %then0__1.i.i.i, label %continue__1.i.i.i

; conditional quantum gate (only one in this block, but many can appear and the full quantum instruction set should be usable)
then0__1.i.i.i:                                   ; preds = %entry
  tail call void @__quantum__qis__z__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*))
  br label %continue__1.i.i.i

continue__1.i.i.i:                                ; preds = %then0__1.i.i.i, %entry
  tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*), %Result* nonnull inttoptr (i64 1 to %Result*))
  tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  %1 = tail call i1 @__quantum__qis__read_result__body(%Result* nonnull inttoptr (i64 1 to %Result*))
  br i1 %1, label %then0__2.i.i.i, label %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.2.exit.i

then0__2.i.i.i:                                   ; preds = %continue__1.i.i.i
  tail call void @__quantum__qis__x__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*))
  br label %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.2.exit.i

TeleportChain__TeleportQubitUsingPresharedEntanglement__body.2.exit.i: ; preds = %then0__2.i.i.i, %continue__1.i.i.i
  tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*), %Qubit* nonnull inttoptr (i64 3 to %Qubit*))
  tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*))
  tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*), %Result* nonnull inttoptr (i64 2 to %Result*))
  tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*))
  %2 = tail call i1 @__quantum__qis__read_result__body(%Result* nonnull inttoptr (i64 2 to %Result*))
  br i1 %2, label %then0__1.i.i1.i, label %continue__1.i.i2.i

then0__1.i.i1.i:                                  ; preds = %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.2.exit.i
  tail call void @__quantum__qis__z__body(%Qubit* nonnull inttoptr (i64 5 to %Qubit*))
  br label %continue__1.i.i2.i

continue__1.i.i2.i:                               ; preds = %then0__1.i.i1.i, %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.2.exit.i
  tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*), %Result* nonnull inttoptr (i64 3 to %Result*))
  tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*))
  %3 = tail call i1 @__quantum__qis__read_result__body(%Result* nonnull inttoptr (i64 3 to %Result*))
  br i1 %3, label %then0__2.i.i3.i, label %TeleportChain__DemonstrateTeleportationUsingPresharedEntanglement__body.1.exit

then0__2.i.i3.i:                                  ; preds = %continue__1.i.i2.i
  tail call void @__quantum__qis__x__body(%Qubit* nonnull inttoptr (i64 5 to %Qubit*))
  br label %TeleportChain__DemonstrateTeleportationUsingPresharedEntanglement__body.1.exit

TeleportChain__DemonstrateTeleportationUsingPresharedEntanglement__body.1.exit: ; preds = %continue__1.i.i2.i, %then0__2.i.i3.i
  tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* nonnull inttoptr (i64 4 to %Result*))
  tail call void @__quantum__qis__reset__body(%Qubit* null)
  tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 5 to %Qubit*), %Result* nonnull inttoptr (i64 5 to %Result*))
  tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 5 to %Qubit*))
  call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 4 to %Result*), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @0, i32 0, i32 0))
  call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 5 to %Result*), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @1, i32 0, i32 0))
  ret void
}

declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) local_unnamed_addr

declare void @__quantum__qis__h__body(%Qubit*) local_unnamed_addr

declare void @__quantum__qis__x__body(%Qubit*) local_unnamed_addr

declare void @__quantum__qis__z__body(%Qubit*) local_unnamed_addr

declare void @__quantum__qis__reset__body(%Qubit*) local_unnamed_addr

declare void @__quantum__rt__result_record_output(%Result*, i8*)

declare void @__quantum__qis__mz__body(%Qubit*, %Result*)

declare i1 @__quantum__qis__read_result__body(%Result*)

; attributes

attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="6" "required_num_results"="6" }

; module flags

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10}

!0 = !{i32 1, !"qir_major_version", i32 1}
!1 = !{i32 7, !"qir_minor_version", i32 0}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
!4 = !{i32 1, !"classical_ints", i32 0} ; ...
!5 = !{i32 1, !"classical_floats", i32 0}
!6 = !{i32 1, !"classical_fixed_points", i32 0}
!7 = !{i32 1, !"IR_functions", i1 false}
!8 = !{i32 1, !"backwards_branching", i1 false}
!9 = !{i32 1, !"multiple_target_branching", i1 false}
!10 = !{i32 1, !"multiple_return_points", i1 false}

The program performs gate teleportation, and it uses conditional single qubit gates and mid-circuit measurements to effect control flow.

For the sake of clarity, the code above does not contain any debug symbols. Debug symbols contain information that is used by a debugger to relate failures during execution back to the original source code. While we expect this to be primarily useful for execution on simulators, debug symbols are both easy to ignore and may be useful to generate helpful error messages for compilation failures that happen only late in the process. We hence see no reason to disallow them from occurring in the bitcode but will not detail their use any further. We defer to existing resources for more information about how to generate and use debug symbols.

Entry Point Definition

The bitcode contains the definition of the LLVM function that should be invoked when the program is executed, referred to as "entry point" in the rest of this profile specification. The name of this function may be chosen freely, as long as it is a valid global identifier according to the LLVM standard. The entry point is identified by a custom function attribute; the section on attributes defines which attributes must be attached to the entry point function.

An entry point function may not take any parameters and must either return void in case the program has no failure conditions or must return an exit code in the form of a 64-bit integer. In the case that a shot fails either because the entry point function returns an integer value indicating that the program caught an error or because an instruction caused an uncaught error (for example a div instruction fails due to division by 0), then the output format should indicate that the shot failed with output for the shot appearing as ERROR Code: ival where ival is the error code from the adaptive profile program or ERROR message in the case that an uncaught failure occurs, and the message is chosen by the backend.

The adaptive profile program makes no restrictions on the structure of basic blocks within the entry point function, other than that a block cannot jump to a previously encountered block in the control flow graph unless a backend opts into Bullet 8. By default adaptive profile programs limit branching to only express forward branching and nested conditionality. Additionally, the only functions that can be called by default in the entry block are qis or rt functions defined in the instruction set and profile. This restriction is removed if a backend opts into Bullet 6 which allows for IR-defined functions.

Quantum Instruction Set

For a quantum instruction set to be fully compatible with the adaptive profile, it must satisfy the following three requirements:

  • All functions must return void, iN, or fN types (i1 and i64 for example) and can only take in %Qubit*, %Result*, iN, or fN types as arguments. Additionally, arguments to the quantum instruction set cannot have dynamically computed arguments by default. They can however refer to static global values that are defined by linking (for example call @__quantum__qis__rz__body(double @angle, %Qubit* null) where @double is a static global who gets a new rotation angle linked at each shot). Dynamically computed arguments can be supplied to instruction set functions when adaptive profile programs use a classical data type from Bullet 5. Functions that measure qubits must take the qubit pointer(s) as well as the result pointer(s) as arguments.

  • Functions that perform a measurement of one or more qubit(s) must be marked with a custom function attribute named irreversible. The use of attributes in general is outlined in the corresponding section.

For more information about the relationship between a profile specification and the quantum instruction set, we refer to the paragraph on Bullet 1 in the introduction of this document. For more information about how and when the QIS is resolved, as well as recommendations for front- and backend developers, we refer to the document on compilation stages and targeting.

Classical Instructions

The following instructions are the LLVM instructions that were permitted within a base profile-compliant program:

LLVM Instruction Context and Purpose Rules for Usage
call Used within a basic block to invoke any one of the declared QIS functions and run-time functions. May optionally be preceded by a tail marker.
br Used to branch from one block to another in the entry point function. The branching must be unconditional and occurs as the final instruction of a block to jump to the next one.
ret Used to return the exit code of the program. Must occur (only) as the last instruction of the final block in the entry point.
inttoptr Used to cast an i64 integer value to either a %Qubit* or a %Result*. May be used as part of a function call only.
getelementptr inbounds Used to create an i8* to pass a constant string for the purpose of labeling an output value. May be used as part of a call to an output recording function only.

The adaptive profile extends the base profile with additional instructions such as br (Bullet 4) and the additional opt-in instructions or intrinsics in (Bullet 5)

See also the section on data types and values for more information about the creation and usage of LLVM values.

Run-Time Functions

The following run-time functions must be supported by all backends:

Function Signature Description
__quantum__rt__initialize void(i8*) Initializes the execution environment. Sets all qubits to a zero-state if they are not dynamically managed.
__quantum__rt__tuple_record_output void(i64,i8*) Inserts a marker in the generated output that indicates the start of a tuple and how many tuple elements it has. The second parameter defines a string label for the tuple. Depending on the output schema, the label is included in the output or omitted.
__quantum__rt__array_record_output void(i64,i8*) Inserts a marker in the generated output that indicates the start of an array and how many array elements it has. The second parameter defines a string label for the array. Depending on the output schema, the label is included in the output or omitted.
__quantum__rt__result_record_output void(%Result*,i8*) Adds a measurement result to the generated output. The second parameter defines a string label for the result value. Depending on the output schema, the label is included in the output or omitted.
__quantum__rt__bool_record_output void(i1,i8*) Adds a boolean value to the generated output. The second parameter defines a string label for the result value. Depending on the output schema, the label is included in the output or omitted.

The following output recording functions can appear if you opt into supporting real-time integer calculations on an integer or fixed-point iN type (the profile compliant program sets the following module flag !{i32 1, !"classical_ints", i32 N} or !{i32 1, !"classical_fixed_points", i32 N}).

Function Signature Description
__quantum__rt__iN_record_output void(i64,i8*) Adds an integer result to the generated output. The second parameter defines a string label for the result value. Depending on the output schema, the label is included in the output or omitted.

The following output recording functions can appear if you opt into supporting real-time floating point computations on an fN type (the profile compliant program sets the following module flag !{i32 1, !"classical_ints", f32 N}).

Function Signature Description
__quantum__rt__fN_record_output void(f64,i8*) Adds a double precision floating point value result to the generated output. The second parameter defines a string label for the result value. Depending on the output schema, the label is included in the output or omitted.

Additionally, a backend can provide more rt functions that can be used by adaptive profile programs in accordance with the classical data types that it supports.

Initialization

A workstream to specify how to initialize the execution environment is currently in progress. As part of that workstream, this paragraph and the listed initialization function(s) will be updated.

Output Recording

The program output of a quantum application is defined by a sequence of calls to run-time functions that record the values produced by the computation, specifically calls to the run-time functions ending in record_output listed in the tables above. In the case of the adaptive profile, these calls are contained within the final block of the entry point function, i.e. the block that terminates in a return instruction. In the case that conditional data needs to be returned, then phi instructions are expected to be used to move conditional data into the final block of the entry-point function.

For all output recording functions, the i8* argument must be a non-null pointer to a global constant that contains a null-terminated string. A backend may ignore that argument if it guarantees that the order of the recorded output matches the order defined by the entry point. Conversely, certain output schemas do not require the recorded output to be listed in a particular order. For those schemas, the i8* argument serves as a label that permits the compiler or tool that generated the labels to reconstruct the order intended by the program. Compiler frontends must always generate these labels in such a way that the bitcode does not depend on the output schema; while choosing how to best label the program output is up to the frontend, the choice of output schema, on the other hand, is up to the backend. A backend may reject a program as invalid or fail execution if a label is missing.

Both the labeling schema and the output schema are identified by a metadata entry in the produced output. For the output schema, that identifier matches the one listed in the corresponding specification. The identifier for the labeling schema, on the other hand, is defined by the value of the "output_labeling_schema" attribute attached to the entry point.

Data Types and Values

Within the adaptive profile, defining local variables is supported via reading mid-circuit measurements via (Bullet 2) or by classical instructions and calls (Bullet 5) if a backend supports these features. This implies the following:

  • Call arguments can be constant values, inttoptr casts, getelementptr instructions that are inlined into a call instruction, or classical registers.
  • backends can have various numeric data types used in their instructions if they opt in to using the classical types specified in Bullet 5.

Constants of any type are permitted as part of a function call. What data types occur in the program hence depends on what QIS functions are used in addition to the run-time functions for initialization and output recording. Constant values of type i64 in particular may be used as part of calls to output recording functions; see the section on output recording for more details.

The %Qubit* and %Result* data types must be supported by all backends. Qubits and results can occur only as arguments in function calls and are represented as a pointer of type %Qubit* and %Result* respectively, where the pointer itself identifies the qubit or result value rather than a memory location where the value is stored: a 64-bit integer constant is cast to the appropriate pointer type. A more detailed elaboration on the purpose of this representation is given in the next subsection. The integer constant that is cast must be in the interval [0, numQubits) for %Qubit* and [0, numResults) for %Result*, where numQubits and numResults are the required number of qubits and results defined by the corresponding entry point attributes. Since backends may look at the values of the required_num_qubits and required_num_results attributes to determine whether a program can be executed, it is recommended to index qubits and results consecutively so that there are no unused values within these ranges.

Qubit and Result Usage

Qubits and result values are represented as opaque pointers in the bitcode, which may only ever be dereferenced as part of a run-time function implementation. In general, the QIR specification distinguishes between two kinds of pointers for representing a qubit or result value, as explained in more detail here, and either one, though not both, may be used throughout a bitcode file. A module flag in the bitcode indicates which kinds of pointers are used to represent qubits and result values.

The first kind of pointer points to a valid memory location that is managed dynamically during program execution, meaning the necessary memory is allocated and freed by the run-time. The second kind of pointer merely identifies a qubit or result value by a constant integer encoded in the pointer itself. To be compliant with the adaptive profile specification, the program must not make use of dynamic qubit or result management, meaning it must use only the second kind of pointer; qubits and results must be identified by a constant integer value that is bitcast to a pointer to match the expected type. How such an integer value is interpreted and specifically how it relates to hardware resources is ultimately up to the executing backend.

Additionally, the adaptive profile imposes the following restrictions on qubit and result usage:

  • Qubits must not be used after they have been passed as arguments to a function that performs an irreversible action. Such functions are marked with the irreversible attribute in their declaration.

  • Results can only be used either as writeonly arguments, as arguments to output recording functions, or as arguments to the measurement result to boolean conversion function (Bullet 2). We refer to the LLVM documentation regarding how to use the writeonly attribute.

Attributes

The following custom attributes must be attached to the entry point function:

  • An attribute named "entry_point" identifying the function as the starting point of a quantum program
  • An attribute named "qir_profiles" with the value "adaptive_profile" identifying the profile the entry point has been compiled for
  • An attribute named "output_labeling_schema" with an arbitrary string value that identifies the schema used by a compiler frontend that produced the IR to label the recorded output
  • An attribute named "required_num_qubits" indicating the number of qubits used by the entry point
  • An attribute named "required_num_results" indicating the maximal number of measurement results that need to be stored while executing the entry point function

Optionally, additional attributes may be attached to the entry point. Any custom function attributes attached to the entry point should be reflected as metadata in the program output; this includes both mandatory and optional attributes but not parameter attributes or return value attributes. This in particular implies that the labeling schema used in the recorded output can be identified by looking at the metadata in the produced output. See the specification of the output schemas for more information about how metadata is represented in the output schema.

Custom function attributes will show up as part of an attribute group in the IR. Attribute groups are numbered in such a way that they can be easily referenced by multiple function definitions or global variables. Consumers of adaptive profile-compliant programs must not rely on a particular numbering but instead, look for the function to which an attribute with the name "entry_point" is attached to determine which one to invoke when the program is launched.

Both the "entry_point" attribute and the "output_labeling_schema" attribute can only be attached to a function definition; they are invalid on a function that is declared but not defined. For the adaptive profile, this implies that they can occur only in one place.

Within the restrictions imposed by the adaptive profile, the number of qubits that are needed to execute the quantum program must be known at compile time. This number is captured in the form of the "required_num_qubits" attribute attached to the entry point. The value of the attribute must be the string representation of a non-negative 64-bit integer constant.

Similarly, the number of measurement results that need to be stored when executing the entry point function is captured by the "required_num_results" attribute.

Beyond the entry point-specific requirements related to attributes, custom attributes may optionally be attached to any of the declared functions. The irreversible attribute in particular impacts how the program logic in the entry point is structured, as described in the section about the entry point definition. Furthermore, the following LLVM attributes may be used according to their intended purpose on function declarations and call sites: inlinehint, nofree, norecurse, readnone, readonly, writeonly, and argmemonly.

Module Flags Metadata

The following module flags must be added to the QIR bitcode:

  • a flag with the string identifier "qir_major_version" that contains a constant value of type i32
  • a flag with the string identifier "qir_minor_version" that contains a constant value of type i32
  • a flag with the string identifier "dynamic_qubit_management" that contains a constant true or false value of type i1
  • a flag with the string identifier "dynamic_result_management" that contains a constant true or false value of type i1
  • A flag named "qubit_resetting" with a boolean i1 value indicating if the program uses reset operations on qubits.
  • A flag named "classical_ints" with a boolean i1 value indicating if the program uses classical computations on integers.
  • A flag named "classical_floats" with a boolean i1 value indicating if the program uses classical computations on floating point values.
  • A flag named "classical_fixed_points" with a boolean i1 value indicating if the program uses reset operations on fixed point values.
  • A flag named "IR_functions" with a boolean i1 value indicating if the program uses user defined functions and function calls.
  • A flag named "backwards_branching" with a boolean i1 value indicating if the program uses branch instructions that causes "backwards" jumps in the control flow.
  • A flag named "multiple_target_branching" with a boolean i1 value indicating if the program uses the switch instruction in llvm.

These flags are attached as llvm.module.flags metadata to the module. They can be queried using the standard LLVM tools and follow the LLVM specification in behavior and purpose. Since module flags impact whether different modules can be merged and how, additional module flags may be added to the bitcode only if their behavior is set to Warning, Append, AppendUnique, Max, or Min. It is at the discretion of the maintainers for various components in the QIR stack to discard module flags that are not explicitly required or listed as optional flags in the QIR specification.

Specification Version

The required flags "qir_major_version" and "qir_minor_version" identify the major and minor versions of the specification that the QIR bitcode adheres to.

  • Since the QIR specification may introduce breaking changes when updating to a new major version, the behavior of the "qir_major_version" flag must be set to Error; merging two modules that adhere to different major versions must fail.

  • The QIR specification is intended to be backward compatible within the same major version but may introduce additional features as part of newer minor versions. The behavior of the "qir_minor_version" flag must hence be Max so that merging two modules compiled for different minor versions results in a module that adheres to the newer of the two versions.

Memory Management

Each bitcode file contains the information whether pointers of type %Qubit* and %Result* point to a valid memory location, or whether a pointer merely encodes an integer constant that identifies which qubit or result the value refers to. This information is represented in the form of the two module flags named "dynamic_qubit_management" and "dynamic_result_management". Within the same bitcode module, there can never be a mixture of the two different kinds of pointers. The behavior of both module flags correspondingly must be set to Error. As detailed in the section on qubit and result usage, an adaptive profile-compliant program must not make use of dynamic qubit or result management. The value of both module flags hence must be set to false.

For i1, i64, f64, ... values created by mid-circuit measurement, extern functions, or classical computations the assumption is that while a %Result* may point to a valid memory location in RAM or some other memory pool, by default, instructions performed on virtual registers with these data types correspond to these values being stored in integer or floating registers when an instruction is executed. Before a virtual register is used in an instruction, there is no assumption that the value in the virtual register always corresponds to a physical register. For example, when considering register coloring, the virtual register, %0, in the QIR program may refer to a value stored in RAM for most of its lifetime before being loaded into a register when an instruction operates on %0. backends should specify any constraints on classical compute support on this page.

Error Messages

Two forms of error messages can occur as a result of the submission of adaptive profile programs to a backend:

  1. Compile-time error messages.
  2. Run-time error messages.

The compile-time error messages can occur when a backend doesn't support some of the optional features from Bullets 5-9. If upon inspecting a module flag, the backend determines that the adaptive profile program uses features not supported by the backend, then a compile-time error message should be provided. Additionally, if there are specific limitations on the support of certain features, like not supporting a particular instruction in Bullet 5, then the backend should return an error message indicating the type of instruction that was not supported.

The run-time error messages can occur when opting into features such as the classical computations in Bullets 5. An adaptive profile program that undergoes a real-time classical error (for example unchecked division by zero) has undefined behavior, and a backend is free to execute and appropriate behavior. Programs can also check computations and provide error code by returning a value supported by a classical data type in a program, assuming a classical type specified in Bullet 5 is supported.

LLVM 15 Opaque Pointers

The transition of LLVM in LLVM 15 and on means that the %Result* and %Qubit* representations of qubits and measurement results will no longer be valid. Establishing a baseline LLVM version is not the point of this workstream. After discussions around LLVM 15 and on support resolve, this spec will be updated on how to indicate an adaptive profile program pre-LLVM 15 and for LLVM 15 and on. The changes to these pointer representations are orthogonal to the concerns of the adaptive profile other than that the signature of the measurement instruction must necessarily change to represent how measurement results are actually converted into i1 values. See the discussion on this upgrade.