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 SetSequence pass #11980

Merged
merged 18 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
1 change: 1 addition & 0 deletions qiskit/pulse/compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
"""Pass-based Qiskit pulse program compiler."""

from .passmanager import BlockTranspiler, BlockToIrCompiler
from .passes import MapMixedFrame, SetSequence
3 changes: 3 additions & 0 deletions qiskit/pulse/compiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
# that they have been altered from the originals.

"""Built-in pulse compile passes."""

from .map_mixed_frames import MapMixedFrame
from .set_sequence import SetSequence
59 changes: 59 additions & 0 deletions qiskit/pulse/compiler/passes/map_mixed_frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 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.

"""MixedFrames mapping analysis pass"""

from __future__ import annotations
from collections import defaultdict

from qiskit.pulse.compiler.basepasses import AnalysisPass
from qiskit.pulse.ir import SequenceIR
from qiskit.pulse.model import MixedFrame


class MapMixedFrame(AnalysisPass):
"""Map the dependencies of all ``MixedFrame``s on ``PulseTaraget`` and ``Frame``.
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved

The pass recursively scans the ``SequenceIR``, identifies all ``MixedFrame``s and
tracks the dependencies of them on ``PulseTarget`` and ``Frame``. The analysis result
is added as a dictionary to the property set under key "mixed_frames_mapping". The
added dictionary is keyed on every ``PulseTarget`` and ``Frame`` in ``SequenceIR``
with the value being a set of all ``MixedFrame``s associated with the key.
"""

def __init__(self):
"""Create new MapMixedFrames pass"""
super().__init__(target=None)
Copy link
Contributor

Choose a reason for hiding this comment

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

This violates typehint (though type is not important in Python). Do you want to remove Target from the base passes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As I commented on the original compiler PR, we don't always need Target. I think we can make it optional.

self.mixed_frames_mapping = defaultdict(set)

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

self._analyze_mixed_frames_in_sequence(passmanager_ir)
self.property_set["mixed_frames_mapping"] = self.mixed_frames_mapping
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self._analyze_mixed_frames_in_sequence(passmanager_ir)
self.property_set["mixed_frames_mapping"] = self.mixed_frames_mapping
mixed_frames_mapping = defaultdict(set)
self._analyze_mixed_frames_in_sequence(passmanager_ir, mixed_frames_mapping)
self.property_set["mixed_frames_mapping"] = mixed_frames_mapping

Can we implement the logic like this? Statefull pass sometimes causes edge cases. For example, in the current implementation some unexpected things could happen if you run the same pass manager instance twice for different inputs.

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 issue of re-running a pass is something that bothered me too. Do we override existing data? Raise an error? There's a "global" decision to be made here, because it's applicable to all passes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For the current pass, I modified it per your suggestion.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is definitely a demand for rerunning the pass manager, for example #11784. I prefer pass that doesn't have state, except for the user provided arguments.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In that case, should we remove all edges from the sequence before sequencing?


def _analyze_mixed_frames_in_sequence(self, prog: SequenceIR) -> None:
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
"""A helper function to recurse through the sequence while mapping mixed frame dependency"""
for elm in prog.elements():
# Sub Block
if isinstance(elm, SequenceIR):
self._analyze_mixed_frames_in_sequence(elm)
# Pulse Instruction
else:
if isinstance(inst_target := elm.inst_target, MixedFrame):
self.mixed_frames_mapping[inst_target.frame].add(inst_target)
self.mixed_frames_mapping[inst_target.pulse_target].add(inst_target)
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved

def __hash__(self):
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
return hash((self.__class__.__name__,))
53 changes: 53 additions & 0 deletions qiskit/pulse/compiler/passes/set_sequence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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.

"""Sequencing pass for Qiskit PulseIR compilation."""

from __future__ import annotations

from qiskit.pulse.compiler.basepasses import TransformationPass
from qiskit.pulse.ir import SequenceIR


class SetSequence(TransformationPass):
"""Sets the sequence of a ``SequenceIR`` object.

The pass traverses the ``SequenceIR``, recursively sets the sequence, by adding edges to
the ``sequence`` property. Sequencing is done according to the alignment strategy.

For parallel alignment types, the pass depends on the results of the analysis pass
:class:`~qiskit.pulse.compiler.passes.MapMixedFrame`.
"""

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

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

self._set_sequence_recursion(passmanager_ir)
return passmanager_ir

def _set_sequence_recursion(self, prog: SequenceIR) -> None:
"""A helper function to recurse through the sequence"""
prog.alignment.set_sequence(
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
prog.sequence, mixed_frames_mapping=self.property_set["mixed_frames_mapping"]
)
for elm in prog.elements():
if isinstance(elm, SequenceIR):
self._set_sequence_recursion(elm)

def __hash__(self):
return hash((self.__class__.__name__,))
2 changes: 2 additions & 0 deletions qiskit/pulse/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
AlignRight,
AlignSequential,
AlignmentKind,
ParallelAlignment,
SequentialAlignment,
)

from .base_transforms import target_qobj_transform
Expand Down
151 changes: 126 additions & 25 deletions qiskit/pulse/transforms/alignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
from __future__ import annotations
import abc
from typing import Callable, Tuple
from rustworkx import PyDAG

import numpy as np

from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType
from qiskit.pulse.exceptions import PulseError
from qiskit.pulse.schedule import Schedule, ScheduleComponent
from qiskit.pulse.utils import instruction_duration_validation
from qiskit.pulse.model import MixedFrame


class AlignmentKind(abc.ABC):
Expand All @@ -44,6 +46,22 @@ def align(self, schedule: Schedule) -> Schedule:
"""
pass

@abc.abstractmethod
def set_sequence(self, sequence: PyDAG, **kwargs) -> None:
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
"""Set the sequence of the IR program according to the policy.

The ``sequence`` is mutated to include all the edges
connecting the elements in the sequence.

Only top-level elements are sequences. If sub-sequences are nested,
nested sequences are not recursively set.

Args:
sequence: The graph object to be sequenced.
kwargs: Keyword arguments needed for some subclasses.
"""
pass

@property
@abc.abstractmethod
def is_sequential(self) -> bool:
Expand Down Expand Up @@ -87,7 +105,110 @@ def __repr__(self):
return f"{self.__class__.__name__}({', '.join(self._context_params)})"


class AlignLeft(AlignmentKind):
class SequentialAlignment(AlignmentKind, abc.ABC):
"""Abstract base class for ``AlignmentKind`` which aligns instructions sequentially"""

def set_sequence(self, sequence: PyDAG, **kwargs) -> None:
"""Sets the sequence sequentially.

The ``sequence`` property of ``ir_program`` is mutated to include all the edges
connecting the elements of the sequence sequentially according to their order.

Only top-level elements are sequences. If sub-sequences are nested,
nested sequences are not recursively set.

Args:
sequence: The graph object to be sequenced.
kwargs: Included only to match the signature of the function with other subclasses.
"""
nodes = sequence.node_indices()
prev = 0
# The first two nodes are the in\out nodes.
for ind in nodes[2:]:
sequence.add_edge(prev, ind, None)
prev = ind
sequence.add_edge(prev, 1, None)

@property
def is_sequential(self) -> bool:
return True


class ParallelAlignment(AlignmentKind, abc.ABC):
"""Abstract base class for ``AlignmentKind`` which aligns instructions in parallel"""

def set_sequence(self, sequence: PyDAG, **kwargs) -> None:
"""Sets the sequence in parallel.

The ``sequence`` property of ``ir_program`` is mutated to include all the edges
connecting the elements of the sequence in parallel.

The function requires a ``mixed_frame_mapping`` dictionary - mapping all ``PulseTarget``
and ``Frame`` to the associated ``MixedFrame`` - to be passed to ``kwargs``. As part
of a pass manager work flow the dictionary is obtained via the pass
:class:`~qiskit.pulse.compiler.MapMixedFrames`.

Only top-level elements are sequenced. If sub-sequences are nested,
nested sequences are not recursively set.

Args:
sequence: The graph object to be sequenced.
kwargs: Expecting a keyword argument ``mixed_frame_mapping``.

Raises:
PulseError: if ``kwargs`` does not include a "mixed_frames_mapping" key.
"""
if "mixed_frames_mapping" not in kwargs.keys() or kwargs["mixed_frames_mapping"] is None:
raise PulseError(
"Expected a keyword argument mixed_frames_mapping with a"
" mapping of PulseTarget and Frame to MixedFrame"
)
mixed_frame_mapping = kwargs["mixed_frames_mapping"]

idle_after = {}
for ind in sequence.node_indices():
if ind in (0, 1):
# In, Out node
continue
node = sequence.get_node_data(ind)
node_mixed_frames = set()

# if isinstance(node, SequenceIR):
# inst_targets = node.inst_targets
# else:
# inst_targets = [node.inst_target]
if hasattr(node, "inst_targets"):
TsafrirA marked this conversation as resolved.
Show resolved Hide resolved
# SequenceIR object
inst_targets = node.inst_targets
else:
# Instruction object
inst_targets = [node.inst_target]

for inst_target in inst_targets:
if isinstance(inst_target, MixedFrame):
node_mixed_frames.add(inst_target)
else:
node_mixed_frames |= mixed_frame_mapping[inst_target]

pred_nodes = [
idle_after[mixed_frame]
for mixed_frame in node_mixed_frames
if mixed_frame in idle_after
]
if len(pred_nodes) == 0:
pred_nodes = [0]
for pred_node in pred_nodes:
sequence.add_edge(pred_node, ind, None)
for mixed_frame in node_mixed_frames:
idle_after[mixed_frame] = ind
sequence.add_edges_from_no_data([(ni, 1) for ni in idle_after.values()])

@property
def is_sequential(self) -> bool:
return False


class AlignLeft(ParallelAlignment):
"""Align instructions in as-soon-as-possible manner.

Instructions are placed at earliest available timeslots.
Expand All @@ -97,10 +218,6 @@ def __init__(self):
"""Create new left-justified context."""
super().__init__(context_params=())

@property
def is_sequential(self) -> bool:
return False

def align(self, schedule: Schedule) -> Schedule:
"""Reallocate instructions according to the policy.

Expand Down Expand Up @@ -154,7 +271,7 @@ def _push_left_append(this: Schedule, other: ScheduleComponent) -> Schedule:
return this.insert(insert_time, other, inplace=True)


class AlignRight(AlignmentKind):
class AlignRight(ParallelAlignment):
"""Align instructions in as-late-as-possible manner.

Instructions are placed at latest available timeslots.
Expand All @@ -164,10 +281,6 @@ def __init__(self):
"""Create new right-justified context."""
super().__init__(context_params=())

@property
def is_sequential(self) -> bool:
return False

def align(self, schedule: Schedule) -> Schedule:
"""Reallocate instructions according to the policy.

Expand Down Expand Up @@ -222,7 +335,7 @@ def _push_right_prepend(this: Schedule, other: ScheduleComponent) -> Schedule:
return this


class AlignSequential(AlignmentKind):
class AlignSequential(SequentialAlignment):
"""Align instructions sequentially.

Instructions played on different channels are also arranged in a sequence.
Expand All @@ -233,10 +346,6 @@ def __init__(self):
"""Create new sequential context."""
super().__init__(context_params=())

@property
def is_sequential(self) -> bool:
return True

def align(self, schedule: Schedule) -> Schedule:
"""Reallocate instructions according to the policy.

Expand All @@ -256,7 +365,7 @@ def align(self, schedule: Schedule) -> Schedule:
return aligned


class AlignEquispaced(AlignmentKind):
class AlignEquispaced(SequentialAlignment):
"""Align instructions with equispaced interval within a specified duration.

Instructions played on different channels are also arranged in a sequence.
Expand All @@ -274,10 +383,6 @@ def __init__(self, duration: int | ParameterExpression):
"""
super().__init__(context_params=(duration,))

@property
def is_sequential(self) -> bool:
return True

@property
def duration(self):
"""Return context duration."""
Expand Down Expand Up @@ -325,7 +430,7 @@ def align(self, schedule: Schedule) -> Schedule:
return aligned


class AlignFunc(AlignmentKind):
class AlignFunc(SequentialAlignment):
"""Allocate instructions at position specified by callback function.

The position is specified for each instruction of index ``j`` as a
Expand Down Expand Up @@ -362,10 +467,6 @@ def __init__(self, duration: int | ParameterExpression, func: Callable):
"""
super().__init__(context_params=(duration, func))

@property
def is_sequential(self) -> bool:
return True

@property
def duration(self):
"""Return context duration."""
Expand Down
13 changes: 13 additions & 0 deletions test/python/pulse/compiler_passes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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.

"""Qiskit pulse compiler tests."""
Loading