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

0012-Pulse-Compiler-and-IR #45

Merged
merged 10 commits into from
Aug 7, 2023
277 changes: 277 additions & 0 deletions ####-Pulse-Channels-IR-and-Compiler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
# Pulse Compiler & IR

| **Status** | **Proposed** |
|:------------------|:---------------------------------------------|
| **RFC #** | #### |
| **Authors** | Tsafrir Armon (tsafrir.armon@ibm.com), Naoki Kanazawa (knzwnao@jp.ibm.com) |
| **Deprecates** | - |
| **Submitted** | 2023-07-31 |
| **Updated** | YYYY-MM-DD |

## Summary
This RFC summarizes the proposal for new Pulse Compiler & IR. The introduction of the new compiler paves the way to the transition to frame aware model, which is also discussed.
The proposal is based on a series of discussions
within Qiskit Pulse's development team, and is brought here for the community to weigh in. The main changes proposed include:

- Introduce new pass based Pulse Compiler and the supporting Pulse IR (intermediate representation).
- Introduce new model to supplement the existing `Channel` model, that will allow writing backend-agnostic template pulse programs.

Comments are welcome.

## Motivation
Qiskit Pulse currently has no unified compilation pathway. Different tasks are handled by the different code segments. This not only creates code clutter which is hard to maintain,
but also limits the ability of vendors to adapt Qiskit Pulse, or adjust Qiskit Pulse to new HW developments. The circuit module of Qiskit already uses a pass based compilation process,
which gives the flexibility to add\change\adapt passes according to the changing needs.

A prime example for the need for a better compilation process, is given by the second part of this proposal - rework of the `Channel` model.

The legacy `Channel`s correspond to what the backend calls a mixed frame - a combination of specific HW port, and the frame (frequency and phase) needed to play pulses.
However, to specify a Channel one must be aware of the backend mapping. For example, the following pulse schedule is an ECR pulse for qubits 3 and 4:

```
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
with builder.build() as ecr_schedule_q3q4:
with align_sequential():
with align_left():
Play(GaussianSquare(...), ControlChannel(6))
Play(GaussianSquare(...), DriveChannel(4))
Play(Gaussian(...), DriveChannel(3))
with align_left():
Play(GaussianSquare(...), ControlChannel(6))
Play(GaussianSquare(...), DriveChannel(4))
```

Note how one can't make sense of the code, without knowing the mapping of the control channels.
Similarly, frame synchronization also has to be carried out manually and depends on the backend mapping.
This dependency on the backend prohibits the ability to write a template code, which in turn complicates modules like Qiskit Experiments.

Under the new proposal, the dependency on backend mapping will be removed, and Qiskit Pulse users (and particularly Qiskit Experiments users) will be able to write
backend-agnostic template programs, which could be used across devices. Additionally, custom frames will make it easier to experiment with qudit control.
However, supporting this change adds significant compilation needs, which are hard to implement in the current workflow.

## User Benefit
- Qiskit contributors as well as vendors will have a unified compilation code which will be simpler to adjust for future needs and changes.
- Qiskit Pulse users will have a clearer and more efficient way to write their Pulse programs.
- Qiskit Experiments users will be able to write more streamlined template experiments.
- Qiskit Pulse users will have easier access to qudit control.

## Design Proposal
The new pulse compiler will be a pass based compiler. Dedicated passes will perform every analysis,
transformation and validation operations needed to convert a pulse program into a desired output format. A detailed workflow will be described below, but the main tasks include
concretely scheduling `ScheduleBlock` programs, mapping logical qubits to physical ones, applying backend constraints, and converting to desired output format. To support this,
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
the new IR will consist of nested instruction blocks, which will be transformed by the compiler as it operates.

On the user facing interface, one can schematically split Qiskit Pulse into three layers -

- Pulse level (`SymbolicPulse`,`WaveForm`)
- Instruction level (`Play`,`Delay`...)
- Program level (`Schedule`,`ScheduleBlock`)
This proposal focuses on the instruction level. The main goal is to allow for clear and simple backend agnostic (but not architecture agnostic) pulse templates.
To do this, the legacy `Channel`s will be supplemented by `Frame`s and `LogicalElement`s.

No API breaking changes are needed. New and legacy options can coexist.

## Detailed Design
### Channel rework
To replace the legacy Channels we propose to specify instructions in terms of `LogicalElement`s and `Frame`s:

- `LogicalElement` - every type of element in the HW which the user can control ("play pulses on"). For example, a qubit is a `LogicalElement`, and not the specific port used to drive it. The most notable example of a logical element is of course the `Qubit`, but in the future one can imagine dedicated couplers, flux controls and so on.
- `Frame` - a combination of frequency and phase. Subclasses used to identify the frame with a backend default. Notable examples are `QubitFrame` and `MeasurementFrame` associated with the default driving and measurement frequencies (respectively) of a qubit.
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved

Using these objects, the above code takes the form:

```
with builder.build() as ecr_schedule_q3q4:
with align_sequential():
with align_left():
Play(GaussianSquare(...), Qubit(3),QubitFrame(4))
Play(GaussianSquare(...), Qubit(3),QubitFrame(3))
Play(Gaussian(...), Qubit(3),QubitFrame(3))
with align_left():
Play(GaussianSquare(...), Qubit(3),QubitFrame(4))
Play(GaussianSquare(...), Qubit(3),QubitFrame(3))
```

The new code not only allows to work with logical backend-agnostic qubits, but is also much clearer in conveying the actual actions in play (acting on qubit 3 with the frame of qubit 4 vs with the frame of qubit 3).

On top of the `LogicalElement` and the `Frame`, another layer of `MixedFrame` will be introduced.
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
The `MixedFrame` is simply a combination of a LogicalElement and a Frame, and in many ways is similar to the legacy `Channel`.
The difference being that a `MixedFrame` does not depend on the backend mapping and has clear relations with other `MixedFrames` as opposed to a `Channel` which hasn't.
Play\Delay instructions will be allowed to take one of `LogicalElement`+`Frame`, `MixedFrame` or `Channel` (for backwards compatibility).
Set\Shift Frequency\Phase will be allowed to take one of `Frame` (to be broadcast to every associated `MixedFrame`), `MixedFrame` (not to be broadcasted) or `Channel`.

It should be noted that custom `Frame` and `MixedFrame` will make it easier to control qudit experiments.

Classes hierarchy:

```
class LogicalElement(ABC):
@property
def index(self) -> Tuple[int, ...]
# index

@property
@abstractmethod
def name(self) -> str:
# opcode/string identifier

class Qubit(LogicalElement):

def __init__(self, index: int):
....

@property
def index(self) -> int
return self._index

@property
def name(self) -> str:
return f"Q{index}"

class Coupler(LogicalElement):

def __init__(self, index: Tuple[int, int]):
self._index = index

@property
def index(self) -> Tuple[int, int]
return self._index

@property
def name(self) -> str:
qubits = ",".join(self._index)
return f"Coupler({qubits})"

class Frame(ABC):

@property
def name(self) -> str:
# opcode/string identifier

class GenericFrame(Frame):
def __init__(self, name, frequency, phase):
self._name = name
self._frequency = frequency
self._phase = phase

@property
def name(self) -> str:
return self._name

@property
def frequency(self) -> float: # The initial frequency of the frame

@property
def phase(self) -> float: # The initial phase of the frame


class QubitFrame(Frame):
def __init__(self, index):
self._index = index

@property
def index(self) -> int
# qubit index

@property
def name(self) -> str:
return f"QFrame{self.index}"

class MeasurementFrame(Frame):
def __init__(self, index):
self._index = index

@property
def index(self) -> int
# qubit index

@property
def name(self) -> str:
return f"MFrame{self.index}"
```

### Pulse IR
The Pulse IR will provide the framework for the compiler to work on and perform the necessary compilation steps. The main components of the IR will be:

- IR Block - A block of instructions (or nested blocks) with an alignment context. This structure is similar to the structure of `ScheduleBlock` and needed to support one of the main tasks of the compiler - scheduling.
- IR Instruction - A pulse program instruction. Typically, it will be initialized without concrete timing, and will be scheduled during the compiler operation.
Copy link
Contributor

@nkanazawa1989 nkanazawa1989 Jul 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also define the members of these class. These would be something like

class PulseIR:
    def __init__(self):
        self.instructions # DAG, PulseIR can be nested into other instance? 
        self.metadata  # maybe alignment is part of metadata?

class GenericInstruction:
    def __init__(
        self,
        instruction_type: str, 
        duration: int, 
        logical_element: Optional[LogicalElement] = None, 
        frame: Optional[Frame] = None, 
        **operands,
    ):  
        self.t0 = None
        self.instruction = instruction_type  # opcode
        self.duration = duration
        self.logical_element = logical_element
        self.frame = frame
        self.operands = operands

Maybe we can use composite pattern to implement them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instructions will be grouped in the IR objects, so I think that will make a composite pattern a bit redundant here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be. A good point of using composite pattern is that we can remove branching or dispaching which could make code more readable (originally we have implemented Schedule and Instruction by using this pattern). Maybe this doesn't fit in here because PulseIR doesn't need time point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me like everything we can do with a composite pattern we can also do by acting on PulseIR objects? We can revisit this later to see if we can improve code readability.


Two instructions types are used:
- `GenericInstruction` - for play, delay, set\shift frequency\phase. Assignment of `LogicalElement` will be optional for set\shift frequency\phase and will determine whether or not the instruction will be
broadcaseted to all mixed frames associated with the frame or not.
- `AcquireInstruction` - for acquire instructions (due to the different elements involved).

```
class PulseIR:
def __init__(self):
self.instructions # List of instructions or nested PulseIR objects
self.alignment

class GenericInstruction:
def __init__(
self,
instruction_type: str,
duration: int,
logical_element: Optional[LogicalElement] = None,
frame: Optional[Frame] = None,
**operands,
):
self.t0 = None
self.instruction = instruction_type # opcode
self.duration = duration
self.logical_element = logical_element
self.frame = frame
self.operands = operands
```

### Compiler
The compiler will be built with a shared design of Qiskit's pass manager (based on PR #10474), and will thus be easily extendable to other tasks. The pass based approach allows for simple modification of the lowering process for vendors or Qiskit team.
A typical compiler call will look like:
```
payload = compile(my_pulse_prog, backend, target="pulse_qobj")
```
where the backend is provided to accomodate the compiled program to the backend constraints and mappings. An optional argument will be a mapping between logical and physical qubits, as under the new model all indices will be assumed logical. If no mapping is provided, the trivial one will be used.

The first step of every compiler run will be to initialize the IR of the pulse program. The IR will be initialized with no concrete timing, and the scheduling will be carried out next.

Next we highlight some of the tasks handled by the compiler.

#### Scheduling
- Canonicalization (transform pass) - If legacy `Channel`s were used, they will be mapped to `MixedFrame`s using the backend mapping. This is the only scenario where backend mapping will be needed for this stage.
- Identify frames and mixed frames (characterization pass) - `FrameInstruction`s will need to be broadcasted to every `MixedFrame` associated with that `Frame`. Therefore, we need identify every `MixedFrame`, `Frame` and the relations between them.
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
- Schedule each block -
- Starting from the lowest blocks, each block will be scheduled on its own.
- Set\shift frequency\phase instructions will be broadcasted when necessary, and the additional mixed frames will be added to the block.
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
- According to the alignment, the instructions will be scheduled within the block.
- Nested blocks will be shifted according to the alignment, but their internal timing will not be altered by parent block alignment.
- Apply backend scheduling constraints.
- Validate timing and constraints.
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved

#### Mapping to backend aware "channels"
For a target format consumed by a backend, the `Frame`s and `MixedFrame`s will have to be converted to backend aware "channels".
It remains to be seen if this conversion will be done on the frontend or the backend, but a scheme for frontend mapping could look like this.

- Consume the backend accepted channels (For example, "D0" is the driving mixed frame of qubit 0 while "CR12" is the cross resonance mixed frame of qubit 1 in the frame of qubit 2.).
- First, map `MixedFrame`s which natively map to the backend. For example, a `MixedFrame` associated with `Qubit(1)` and `QubitFrame(1)` is natively mapped to the backend's "D1" "channel" in the example above.
Similarly MixedFrame associate with `Qubit(1)` and `QubitFrame(2)` is natively mapped to the backend's "CR12" "channel".
- Next, map remaining `MixedFrames` into unused "channels".
- Lastly, provide the backend with `MixedFrame` which couldn't have been mapped by the frontend, in hope that the backend could support them. (This will obviously require dedicated backend support).

#### Conversion to output format
There is still ongoing discussion about desired output formats, but every choice of output format could be supported at the cost of creating a conversion mechanism from IR to that format.
One can imagine various tasks associated with this step:
- Conversion of `SymbolicPulse` to `Waveform` for non-backend-supported pulses.
- Creation of pulse dictionary to reduce payload memory foot print.

#### Optimizations
While pulse level programs are low level, one might consider optimization passes, like dynamical decoupling addition. The pass based compiler will allow such optimization passes to be created and applied.

## Alternative Approaches
This proposal combines two seemingly separate issues - the channel model rework and the introduction of Pulse IR and Compiler. It would be possible to do one without the other, but it seems like this is
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
a good opportunity to do both.

## Questions
- Naming - Names were not finalized. While not in the same namespace, some names here clash with other Qiskit modules (most notably `Qubit`), and it might be better to modify the names.
- IR instructions - The existing `Instruction` class provides similar functionality. Should we use it instead of introducing new IR Instruction classes?

## Future Extensions
- Frontend & backend coordination to support custom frames and mixed frames. This has the potential to better utilize HW control options (as discussed above).