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

Fix ASAP/ALAP scheduling pass #7655

Merged
merged 11 commits into from
Feb 22, 2022
163 changes: 80 additions & 83 deletions qiskit/transpiler/passes/scheduling/alap.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,20 @@
# that they have been altered from the originals.

"""ALAP Scheduling."""
import itertools
from collections import defaultdict
from typing import List

from qiskit.circuit import Delay, Measure
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.circuit import Delay, Qubit, Measure, Gate
from qiskit.dagcircuit import DAGCircuit
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion

from .base_scheduler import BaseScheduler

class ALAPSchedule(TransformationPass):
"""ALAP Scheduling pass, which schedules the **stop** time of instructions as late as possible.

For circuits with instructions writing or reading clbits (e.g. measurements, conditional gates),
the scheduler assumes clbits I/O operations take no time, ``measure`` locks clbits to be written
at its end and ``c_if`` locks clbits to be read at its beginning.
class ALAPSchedule(BaseScheduler):
"""ALAP Scheduling pass, which schedules the **stop** time of instructions as late as possible.

Notes:
The ALAP scheduler may not schedule a circuit exactly the same as any real backend does
when the circuit contains control flows (e.g. conditional instructions).
See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseScheduler` for the
detailed behavior of the control flow operation, i.e. ``c_if``.
"""

def __init__(self, durations):
"""ALAPSchedule initializer.

Args:
durations (InstructionDurations): Durations of instructions to be used in scheduling
"""
super().__init__()
self.durations = durations
# ensure op node durations are attached and in consistent unit
self.requires.append(TimeUnitConversion(durations))

def run(self, dag):
"""Run the ALAPSchedule pass on `dag`.

Expand All @@ -68,71 +47,89 @@ def run(self, dag):
for creg in dag.cregs.values():
new_dag.add_creg(creg)

qubit_time_available = defaultdict(int)
clbit_readable = defaultdict(int)
clbit_writeable = defaultdict(int)

def pad_with_delays(qubits: List[int], until, unit) -> None:
"""Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``."""
for q in qubits:
if qubit_time_available[q] < until:
idle_duration = until - qubit_time_available[q]
new_dag.apply_operation_front(Delay(idle_duration, unit), [q], [])

idle_before = {q: 0 for q in dag.qubits + dag.clbits}
bit_indices = {bit: index for index, bit in enumerate(dag.qubits)}
for node in reversed(list(dag.topological_op_nodes())):
# validate node.op.duration
if node.op.duration is None:
indices = [bit_indices[qarg] for qarg in node.qargs]
if dag.has_calibration_for(node):
node.op.duration = dag.calibrations[node.op.name][
(tuple(indices), tuple(float(p) for p in node.op.params))
].duration

if node.op.duration is None:
raise TranspilerError(
f"Duration of {node.op.name} on qubits {indices} is not found."
)
if isinstance(node.op.duration, ParameterExpression):
indices = [bit_indices[qarg] for qarg in node.qargs]
raise TranspilerError(
f"Parameterized duration ({node.op.duration}) "
f"of {node.op.name} on qubits {indices} is not bounded."
)
# choose appropriate clbit available time depending on op
clbit_time_available = (
clbit_writeable if isinstance(node.op, Measure) else clbit_readable
)
# correction to change clbit start time to qubit start time
delta = 0 if isinstance(node.op, Measure) else node.op.duration
# must wait for op.condition_bits as well as node.cargs
start_time = max(
itertools.chain(
(qubit_time_available[q] for q in node.qargs),
(clbit_time_available[c] - delta for c in node.cargs + node.op.condition_bits),
)
)

pad_with_delays(node.qargs, until=start_time, unit=time_unit)
op_duration = self._get_node_duration(node, bit_indices, dag)

# compute t0, t1: instruction interval, note that
# t0: start time of instruction
# t1: end time of instruction

# since this is alap scheduling, node is scheduled in reversed topological ordering
# and nodes are packed from the very end of the circuit.
# the physical meaning of t0 and t1 is flipped here.
if isinstance(node.op, Measure):
# clbit time is always right (alap) justified
t0 = max(idle_before[bit] for bit in node.qargs + node.cargs)
t1 = t0 + op_duration
#
# |t1 = t0 + duration
# Q ░░░░░▒▒▒▒▒▒▒▒▒▒▒
# C ░░░░░░░░░▒▒▒▒▒▒▒
# |t0 + (duration - clbit_write_latency)
#
for clbit in node.cargs:
idle_before[clbit] = t0 + (op_duration - self.clbit_write_latency)
elif isinstance(node.op, Gate):
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
t0q = max(idle_before[q] for q in node.qargs)
if node.op.condition_bits:
# conditional is bit tricky due to conditional_latency
t0c = max(idle_before[c] for c in node.op.condition_bits)
# Assume following case (t0c > t0q):
#
# |t0q
# Q ░░░░░░░░░░░░░▒▒▒
# C ░░░░░░░░▒▒▒▒▒▒▒▒
# |t0c
#
# In this case, there is no actual clbit read before gate.
#
# |t0q' = t0c - conditional_latency
# Q ░░░░░░░░▒▒▒░░▒▒▒
# C ░░░░░░▒▒▒▒▒▒▒▒▒▒
# |t1c' = t0c + conditional_latency
#
# rather than naively doing
#
# |t1q' = t0c + duration
# Q ░░░░░▒▒▒░░░░░▒▒▒
# C ░░▒▒░░░░▒▒▒▒▒▒▒▒
# |t1c' = t0c + duration + conditional_latency
#
t0 = max(t0q, t0c - op_duration)
t1 = t0 + op_duration
for clbit in node.op.condition_bits:
idle_before[clbit] = t1 + self.conditional_latency
else:
t0 = t0q
t1 = t0 + op_duration
else:
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
# It happens to be Barrier
t0 = max(idle_before[bit] for bit in node.qargs + node.cargs)
t1 = t0 + op_duration

for bit in node.qargs:
delta = t0 - idle_before[bit]
if delta > 0:
new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [])
idle_before[bit] = t1

new_dag.apply_operation_front(node.op, node.qargs, node.cargs)

stop_time = start_time + node.op.duration
# update time table
for q in node.qargs:
qubit_time_available[q] = stop_time
for c in node.cargs: # measure
clbit_writeable[c] = clbit_readable[c] = start_time
for c in node.op.condition_bits: # conditional op
clbit_writeable[c] = max(stop_time, clbit_writeable[c])

working_qubits = qubit_time_available.keys()
circuit_duration = max(qubit_time_available[q] for q in working_qubits)
pad_with_delays(new_dag.qubits, until=circuit_duration, unit=time_unit)
circuit_duration = max(idle_before.values())
for bit, before in idle_before.items():
delta = circuit_duration - before
if not (delta > 0 and isinstance(bit, Qubit)):
continue
new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [])

new_dag.name = dag.name
new_dag.metadata = dag.metadata
new_dag.calibrations = dag.calibrations

# set circuit duration and unit to indicate it is scheduled
new_dag.duration = circuit_duration
new_dag.unit = time_unit

return new_dag
174 changes: 91 additions & 83 deletions qiskit/transpiler/passes/scheduling/asap.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,20 @@
# that they have been altered from the originals.

"""ASAP Scheduling."""
import itertools
from collections import defaultdict
from typing import List

from qiskit.circuit import Delay, Measure
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.circuit import Delay, Qubit, Measure, Gate
from qiskit.dagcircuit import DAGCircuit
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion

from .base_scheduler import BaseScheduler

class ASAPSchedule(TransformationPass):
"""ASAP Scheduling pass, which schedules the start time of instructions as early as possible..

For circuits with instructions writing or reading clbits (e.g. measurements, conditional gates),
the scheduler assumes clbits I/O operations take no time, ``measure`` locks clbits to be written
at its end and ``c_if`` locks clbits to be read at its beginning.
class ASAPSchedule(BaseScheduler):
"""ASAP Scheduling pass, which schedules the start time of instructions as early as possible..

Notes:
The ASAP scheduler may not schedule a circuit exactly the same as any real backend does
when the circuit contains control flows (e.g. conditional instructions).
See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseScheduler` for the
detailed behavior of the control flow operation, i.e. ``c_if``.
"""

def __init__(self, durations):
"""ASAPSchedule initializer.

Args:
durations (InstructionDurations): Durations of instructions to be used in scheduling
"""
super().__init__()
self.durations = durations
# ensure op node durations are attached and in consistent unit
self.requires.append(TimeUnitConversion(durations))

def run(self, dag):
"""Run the ASAPSchedule pass on `dag`.

Expand All @@ -69,70 +48,99 @@ def run(self, dag):
for creg in dag.cregs.values():
new_dag.add_creg(creg)

qubit_time_available = defaultdict(int)
clbit_readable = defaultdict(int)
clbit_writeable = defaultdict(int)

def pad_with_delays(qubits: List[int], until, unit) -> None:
"""Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``."""
for q in qubits:
if qubit_time_available[q] < until:
idle_duration = until - qubit_time_available[q]
new_dag.apply_operation_back(Delay(idle_duration, unit), [q])

idle_after = {q: 0 for q in dag.qubits + dag.clbits}
bit_indices = {q: index for index, q in enumerate(dag.qubits)}
for node in dag.topological_op_nodes():
# validate node.op.duration
if node.op.duration is None:
indices = [bit_indices[qarg] for qarg in node.qargs]
if dag.has_calibration_for(node):
node.op.duration = dag.calibrations[node.op.name][
(tuple(indices), tuple(float(p) for p in node.op.params))
].duration

if node.op.duration is None:
raise TranspilerError(
f"Duration of {node.op.name} on qubits {indices} is not found."
)
if isinstance(node.op.duration, ParameterExpression):
indices = [bit_indices[qarg] for qarg in node.qargs]
raise TranspilerError(
f"Parameterized duration ({node.op.duration}) "
f"of {node.op.name} on qubits {indices} is not bounded."
)
# choose appropriate clbit available time depending on op
clbit_time_available = (
clbit_writeable if isinstance(node.op, Measure) else clbit_readable
)
# correction to change clbit start time to qubit start time
delta = node.op.duration if isinstance(node.op, Measure) else 0
# must wait for op.condition_bits as well as node.cargs
start_time = max(
itertools.chain(
(qubit_time_available[q] for q in node.qargs),
(clbit_time_available[c] - delta for c in node.cargs + node.op.condition_bits),
)
)

pad_with_delays(node.qargs, until=start_time, unit=time_unit)
op_duration = self._get_node_duration(node, bit_indices, dag)

# compute t0, t1: instruction interval, note that
# t0: start time of instruction
# t1: end time of instruction
if isinstance(node.op, Measure):
# measure instruction handling is bit tricky due to clbit_write_latency
t0q = max(idle_after[q] for q in node.qargs)
t0c = max(idle_after[c] for c in node.cargs)
# Assume following case (t0c > t0q)
#
# |t0q
# Q ▒▒▒▒░░░░░░░░░░░░
# C ▒▒▒▒▒▒▒▒░░░░░░░░
# |t0c
#
# In this case, there is no actual clbit access until clbit_write_latency.
# The node t0 can be push backward by this amount.
#
# |t0q' = t0c - clbit_write_latency
# Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒
# C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
# |t0c' = t0c
#
# rather than naively doing
#
# |t0q' = t0c
# Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒
# C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒
# |t0c' = t0c + clbit_write_latency
#
t0 = max(t0q, t0c - self.clbit_write_latency)
t1 = t0 + op_duration
for clbit in node.cargs:
idle_after[clbit] = t1
elif isinstance(node.op, Gate):
t0q = max(idle_after[q] for q in node.qargs)
if node.op.condition_bits:
# Conditional gate
t0c = max(idle_after[bit] for bit in node.op.condition_bits)
if t0q > t0c:
# This is situation something like below
#
# |t0q
# Q ▒▒▒▒▒▒▒▒▒░░
# C ▒▒▒░░░░░░░░
# |t0c
#
# In this case, you can insert readout access before tq0
#
# |t0q
# Q ▒▒▒▒▒▒▒▒▒▒▒
# C ▒▒▒░░░▒▒░░░
# |t0q - conditional_latency
#
t0c = max(t0q - self.conditional_latency, t0c)
t1c = t0c + self.conditional_latency
for bit in node.op.condition_bits:
# Lock clbit until state is read
idle_after[bit] = t1c
# It starts after register read access
t0 = max(t0q, t1c)
else:
t0 = t0q
t1 = t0 + op_duration
else:
# It happens to be Barrier
t0 = max(idle_after[bit] for bit in node.qargs + node.cargs)
t1 = t0 + op_duration

# Add delay to qubit wire
for bit in node.qargs:
delta = t0 - idle_after[bit]
if delta > 0 and isinstance(bit, Qubit):
new_dag.apply_operation_back(Delay(delta, time_unit), [bit], [])
idle_after[bit] = t1

new_dag.apply_operation_back(node.op, node.qargs, node.cargs)

stop_time = start_time + node.op.duration
# update time table
for q in node.qargs:
qubit_time_available[q] = stop_time
for c in node.cargs: # measure
clbit_writeable[c] = clbit_readable[c] = stop_time
for c in node.op.condition_bits: # conditional op
clbit_writeable[c] = max(start_time, clbit_writeable[c])

working_qubits = qubit_time_available.keys()
circuit_duration = max(qubit_time_available[q] for q in working_qubits)
pad_with_delays(new_dag.qubits, until=circuit_duration, unit=time_unit)
circuit_duration = max(idle_after.values())
for bit, after in idle_after.items():
delta = circuit_duration - after
if not (delta > 0 and isinstance(bit, Qubit)):
continue
new_dag.apply_operation_back(Delay(delta, time_unit), [bit], [])

new_dag.name = dag.name
new_dag.metadata = dag.metadata
new_dag.calibrations = dag.calibrations
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved

# set circuit duration and unit to indicate it is scheduled
new_dag.duration = circuit_duration
new_dag.unit = time_unit
Expand Down
Loading