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

Pulse Compiler - Scheduling pass #11981

Merged
merged 27 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7cd1396
Basic implementation
TsafrirA Feb 20, 2024
a35abed
Add align right, align sequential (sequence+schedule)
TsafrirA Feb 21, 2024
592aba6
Add draw, flatten
TsafrirA Mar 4, 2024
8c83957
Corrections
TsafrirA Mar 5, 2024
b670356
Split into separate IR PR (temporary remove tests which rely on passes)
TsafrirA Mar 5, 2024
edf3749
Merge remote-tracking branch 'upstream/feature/pulse-ir' into OnlyIR2
TsafrirA Mar 6, 2024
52158f6
Update qiskit/pulse/ir/ir.py
TsafrirA Mar 6, 2024
1f0e13c
Corrections.
TsafrirA Mar 6, 2024
8c19fbf
Add to do.
TsafrirA Mar 6, 2024
4b91f55
Merge branch 'feature/pulse-ir' into OnlyIR2
TsafrirA Mar 7, 2024
21dc703
Fixes
TsafrirA Mar 7, 2024
13393b4
Disable lint
TsafrirA Mar 8, 2024
c828ab5
MapMixedFrame + SetSequence passes
TsafrirA Mar 9, 2024
f363271
Merge remote-tracking branch 'upstream/feature/pulse-ir' into Sequenc…
TsafrirA Mar 9, 2024
50ec916
Doc fixes
TsafrirA Mar 10, 2024
8052bfa
Add SchedulePass, restore tests depending on scheduling.
TsafrirA Mar 10, 2024
5fe6c78
doc fix
TsafrirA Mar 10, 2024
e185741
Corrections
TsafrirA Mar 11, 2024
01803ed
add todo
TsafrirA Mar 11, 2024
0a45805
Merge branch 'SequencePass' into SchedulingPass
TsafrirA Mar 11, 2024
5bc3df4
Move schedule logic to pass.
TsafrirA Mar 11, 2024
251a18d
Corrections.
TsafrirA Mar 12, 2024
94b4ab8
Merge remote-tracking branch 'upstream/feature/pulse-ir' into Schedul…
TsafrirA Mar 12, 2024
0d71eeb
blank line
TsafrirA Mar 12, 2024
e696bde
Update qiskit/pulse/compiler/passes/schedule.py
TsafrirA Mar 13, 2024
a629f18
Corrections
TsafrirA Mar 13, 2024
5c6b021
Minor fixes
TsafrirA Mar 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion qiskit/pulse/compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
"""Pass-based Qiskit pulse program compiler."""

from .passmanager import BlockTranspiler, BlockToIrCompiler
from .passes import MapMixedFrame, SetSequence
from .passes import MapMixedFrame, SetSequence, SetSchedule
1 change: 1 addition & 0 deletions qiskit/pulse/compiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@

from .map_mixed_frames import MapMixedFrame
from .set_sequence import SetSequence
from .schedule import SetSchedule
189 changes: 189 additions & 0 deletions qiskit/pulse/compiler/passes/schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""A scheduling pass for Qiskit PulseIR compilation."""

from __future__ import annotations
from functools import singledispatchmethod
from collections import defaultdict
from rustworkx import PyDAG, topological_sort, number_weakly_connected_components

from qiskit.pulse.compiler.basepasses import TransformationPass
from qiskit.pulse.ir import SequenceIR
from qiskit.pulse.transforms import AlignmentKind, AlignLeft, AlignRight, AlignSequential
from qiskit.pulse.exceptions import PulseCompilerError


class SetSchedule(TransformationPass):
"""Concretely schedule ``SequenceIR`` object.

The pass traverses the ``SequenceIR``, and recursively sets initial time for every
node in the sequence (and sub-sequences). The scheduling is done according to the
alignment strategy, and requires that the ``sequence`` property is already sequenced,
typically with the pass :class:`~qiskit.pulse.compiler.passes.SetSequence`."""

def __init__(self):
"""Create new SetSchedule pass"""
super().__init__(target=None)

def run(
self,
passmanager_ir: SequenceIR,
) -> SequenceIR:

self._schedule_recursion(passmanager_ir)
return passmanager_ir

def _schedule_recursion(self, prog: SequenceIR) -> None:
"""Recursively schedule the IR.

Nested IR objects must be scheduled first, so we traverse the IR,
and recursively schedule the IR objects.
After all nested IR objects are scheduled, we apply the scheduling strategy to the
current object.

Arguments:
prog: The IR object to be scheduled.
"""
for elem in prog.elements():
if isinstance(elem, SequenceIR):
self._schedule_recursion(elem)

self._schedule_single_program(prog.alignment, prog.sequence, prog.time_table)

@singledispatchmethod
def _schedule_single_program(
self, alignment: AlignmentKind, sequence: PyDAG, time_table: defaultdict
) -> None:
"""Concretely schedule the IR object.

The ``time_table`` argument is mutated to include the initial time of each element of
``sequence``, according to the structure of ``sequence`` and the alignment.
The function assumes that nested IR objects are already scheduled.

``sequence`` is assumed to have the following structure - node 0 marks the beginning of the
sequence, while node 1 marks the end of it. All branches of the graph originate from node 0
and end at node 1.

Arguments:
alignment: The alignment of the IR object.
sequence: The sequence of the IR object.
time_table: The time_table of the IR object.
"""
raise NotImplementedError

# pylint: disable=unused-argument
@_schedule_single_program.register(AlignLeft)
@_schedule_single_program.register(AlignSequential)
def _schedule_asap(
self, alignment: AlignmentKind, sequence: PyDAG, time_table: defaultdict
) -> None:
"""Concretely schedule the IR object, aligning to the left.

The ``time_table`` argument is mutated to include the initial time of each element of
``sequence``, according to the structure of ``sequence`` and aligning to the left.
The function assumes that nested IR objects are already scheduled.

``sequence`` is assumed to have the following structure - node 0 marks the beginning of the
sequence, while node 1 marks the end of it. All branches of the graph originate from node 0
and end at node 1.

Arguments:
alignment: The alignment of the IR object.
sequence: The sequence of the IR object.
time_table: The time_table of the IR object.

Raises:
PulseCompilerError: If the sequence is not sequenced as expected.
"""
nodes = topological_sort(sequence)
if number_weakly_connected_components(sequence) != 1 or nodes[0] != 0 or nodes[-1] != 1:
raise PulseCompilerError(
"The pulse program is not sequenced as expected. "
"Insert SetSequence pass in advance of SchedulePass."
)

for node_index in nodes:
if node_index in (0, 1):
# in,out nodes
continue
preds = sequence.predecessor_indices(node_index)
if preds == [0]:
time_table[node_index] = 0
else:
time_table[node_index] = max(
time_table[pred] + sequence.get_node_data(pred).duration for pred in preds
)

# pylint: disable=unused-argument
@_schedule_single_program.register(AlignRight)
def _schedule_alap(
self, alignment: AlignmentKind, sequence: PyDAG, time_table: defaultdict
) -> None:
"""Concretely schedule the IR object, aligning to the right.

The ``time_table`` argument is mutated to include the initial time of each element of
``sequence``, according to the structure of ``sequence`` and aligning to the right.
The function assumes that nested IR objects are already scheduled.

``sequence`` is assumed to have the following structure - node 0 marks the beginning of the
sequence, while node 1 marks the end of it. All branches of the graph originate from node 0
and end at node 1.

Arguments:
alignment: The alignment of the IR object.
sequence: The sequence of the IR object.
time_table: The time_table of the IR object.

Raises:
PulseCompilerError: If the sequence is not sequenced as expected.
"""
# We reverse the sequence, schedule to the left, then reverse the timings.
reversed_sequence = sequence.copy()
reversed_sequence.reverse()
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved

nodes = topological_sort(reversed_sequence)

if number_weakly_connected_components(sequence) != 1 or nodes[0] != 1 or nodes[-1] != 0:
raise PulseCompilerError(
"The pulse program is not sequenced as expected. "
"Insert SetSequence pass in advance of SchedulePass."
)

for node_index in nodes:
if node_index in (0, 1):
# in,out nodes
continue
preds = reversed_sequence.predecessor_indices(node_index)
if preds == [1]:
time_table[node_index] = 0
else:
time_table[node_index] = max(
time_table[pred] + sequence.get_node_data(pred).duration for pred in preds
)

total_duration = max(
time_table[i] + sequence.get_node_data(i).duration
for i in reversed_sequence.predecessor_indices(0)
)

for node in sequence.node_indices():
if node not in (0, 1):
time_table[node] = (
total_duration - time_table[node] - sequence.get_node_data(node).duration
)

def __hash__(self):
return hash((self.__class__.__name__,))

def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__
25 changes: 22 additions & 3 deletions qiskit/pulse/ir/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ def sequence(self) -> PyDAG:
"""Return the DAG sequence of the SequenceIR"""
return self._sequence

@property
def time_table(self) -> defaultdict:
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious why this needs to be a defaultdict? Since the scheduler logic naively assumes all previous nodes are scheduled, it's safer to raise a key error (or particular compiler error) instead of silently substituting t0=0.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the _time_table attribute is a defaultdict because we use it for initial_time, duration etc.

Copy link
Contributor

@nkanazawa1989 nkanazawa1989 Mar 13, 2024

Choose a reason for hiding this comment

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

Can we use internal _time_table for them and typecast to dict when the property returns?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you meant to do return dict(self._time_table) - that would return a different object and make _time_table it hard to mutate the base line dictionary. Did you have something else in mind?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's good point. Let's return same object as is.

"""Return the timetable of the SequenceIR"""
return self._time_table

def append(self, element: SequenceIR | Instruction) -> int:
"""Append element to the SequenceIR

Expand Down Expand Up @@ -217,11 +222,19 @@ def _draw_nodes(n):
def flatten(self, inplace: bool = False) -> SequenceIR:
"""Recursively flatten the SequenceIR.

The flattening process includes breaking up nested IRs until only instructions remain.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we provide this functionality as a pass? Seems like no existing pass requires flatten. I'm okey with visualizing a graph with nested (unflatten) sequences, if this is only for visualization purpose. Some target might require flatten if it doesn't have notion of context, so having the flatten pass still makes sense to me.

Copy link
Contributor

@nkanazawa1989 nkanazawa1989 Mar 13, 2024

Choose a reason for hiding this comment

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

For the visualization purpose we can add a dedicated visualization function to the qiskit.visualization module, and call the flatten pass internally depending on the argument, e.g.

def visualize_sequence_ir(sequence_ir: SequenceIR, flat_nested: bool = True):

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am not sure if this is something that should be a pass or a built-in method. I tend towards the latter because that's an operation which would probably be a stand alone - you want to visualize, or you want to flatten before sending to the backend. I don't think the user\developer should bother with a pass manager for something like that.

In other words - if the most likely scenario is a pass manager with only one pass, I think that's a good indication that it shouldn't be a pass.

Copy link
Contributor

@nkanazawa1989 nkanazawa1989 Mar 13, 2024

Choose a reason for hiding this comment

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

In case you submit the IR to backend, you know target code and whether flatten is required or not. For example, if you output Schedule like payload flatten is required, but something like qe-compiler Dialect or OpenPulse code, they have context and we can keep nested context. In this sense, a pass for flatten seems reasonable to me.

In my thoughts, only Transform pass can touch the graph structure. IR should NOT modify the structure of IR itself. Having this method obscures the responsibility, i.e. SRP

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That makes sense as well. I will split flatten out in a separate PR. (It wasn't introduced in this PR - only its tests)

The flattened object will contain all instructions, timing information, and the
complete sequence graph. However, the alignment of nested IRs will be lost. Because of
this, flattening an unscheduled IR is not allowed.

Args:
inplace: If ``True`` flatten the object itself. If ``False`` return a flattened copy.

Returns:
A flattened ``SequenceIR`` object.

Raises:
PulseError: If the IR (or nested IRs) are not scheduled.
"""
# TODO : Verify that the block\sub blocks are sequenced correctly.
if inplace:
Expand All @@ -230,7 +243,8 @@ def flatten(self, inplace: bool = False) -> SequenceIR:
block = copy.deepcopy(self)
block._sequence[0] = SequenceIR._InNode
block._sequence[1] = SequenceIR._OutNode

# TODO : Consider replacing the alignment to "NullAlignment", as the original alignment
# has no meaning.
# TODO : Create a dedicated half shallow copier.

def edge_map(_x, _y, _node):
Expand All @@ -240,18 +254,23 @@ def edge_map(_x, _y, _node):
return 1
return None

if any(
block.time_table[x] is None for x in block.sequence.node_indices() if x not in (0, 1)
):
raise PulseError("Can not flatten unscheduled IR")

for ind in block.sequence.node_indices():
if isinstance(sub_block := block.sequence.get_node_data(ind), SequenceIR):
sub_block.flatten(inplace=True)
initial_time = block._time_table[ind]
initial_time = block.time_table[ind]
nodes_mapping = block._sequence.substitute_node_with_subgraph(
ind, sub_block.sequence, lambda x, y, _: edge_map(x, y, ind)
)
if initial_time is not None:
for old_node in nodes_mapping.keys():
if old_node not in (0, 1):
block._time_table[nodes_mapping[old_node]] = (
initial_time + sub_block._time_table[old_node]
initial_time + sub_block.time_table[old_node]
)

del block._time_table[ind]
Expand Down
Loading