From 02f54b60c747e94f3ce6ab80dac081b40fa62cfb Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 12 Feb 2022 06:47:17 +0900 Subject: [PATCH 1/9] fix scheduling pass This commit updates both ASAP and ALAP passes not to allow measurement instructions to simultaneously write in the same register. In addition, delay appended after end of circuit is removed since this instruction has no effect. --- qiskit/transpiler/passes/scheduling/alap.py | 71 ++--- qiskit/transpiler/passes/scheduling/asap.py | 64 ++--- .../python/transpiler/test_scheduling_pass.py | 267 ++++++++++++------ 3 files changed, 237 insertions(+), 165 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 4357f85940f7..d55fc9c83589 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -11,13 +11,9 @@ # 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 import Delay, Qubit from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit import DAGCircuit, DAGInNode from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion @@ -68,17 +64,7 @@ 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 @@ -99,40 +85,39 @@ def pad_with_delays(qubits: List[int], until, unit) -> None: 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) + this_t0 = max(idle_before[q] for q in node.qargs + node.cargs + node.op.condition_bits) + this_t1 = this_t0 + node.op.duration - new_dag.apply_operation_front(node.op, node.qargs, node.cargs) + for prev_node in dag.predecessors(node): + if isinstance(prev_node, DAGInNode): + continue + # Keep current bit status until operation complete + for blocked_bit in set(node.op.condition_bits) & set(prev_node.cargs): + idle_before[blocked_bit] = this_t1 - 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]) + for bit in node.qargs + node.cargs: + before = idle_before[bit] + delta = this_t0 - before + if before > 0 and delta > 0 and isinstance(bit, Qubit): + new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) + idle_before[bit] = this_t1 - 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) + new_dag.apply_operation_front(node.op, node.qargs, node.cargs) + + 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 diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index 5c0d08dda2eb..aadff8ab0560 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -11,13 +11,9 @@ # 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 import Delay, Qubit from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit import DAGCircuit, DAGOutNode from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion @@ -69,17 +65,7 @@ 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 @@ -100,39 +86,31 @@ def pad_with_delays(qubits: List[int], until, unit) -> None: 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) + this_t0 = max(idle_after[q] for q in node.qargs + node.cargs + node.op.condition_bits) + this_t1 = this_t0 + node.op.duration - new_dag.apply_operation_back(node.op, node.qargs, node.cargs) + for next_node in dag.successors(node): + if isinstance(next_node, DAGOutNode): + continue + # Keep current bit status until operation complete + for blocked_bit in set(node.op.condition_bits) & set(next_node.cargs): + idle_after[blocked_bit] = this_t1 + + for bit in node.qargs + node.cargs: + delta = this_t0 - idle_after[bit] + if delta > 0 and isinstance(bit, Qubit): + new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) + idle_after[bit] = this_t1 - 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]) + new_dag.apply_operation_back(node.op, node.qargs, node.cargs) - 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()) 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 diff --git a/test/python/transpiler/test_scheduling_pass.py b/test/python/transpiler/test_scheduling_pass.py index d63dff60d9dc..aa84e213032b 100644 --- a/test/python/transpiler/test_scheduling_pass.py +++ b/test/python/transpiler/test_scheduling_pass.py @@ -29,6 +29,7 @@ class TestSchedulingPass(QiskitTestCase): def test_alap_agree_with_reverse_asap_reverse(self): """Test if ALAP schedule agrees with doubly-reversed ASAP schedule.""" qc = QuantumCircuit(2) + qc.barrier() qc.h(0) qc.delay(500, 1) qc.cx(0, 1) @@ -64,14 +65,14 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): 0 └─────────┘ (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├ - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── - └─────────────────┘ ║ └─╥─┘ - ║ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0 = T ╞════ - 0 └─────────┘ + ┌─┐ + q_0: ───────────────────┤M├─────────── + ┌─────────────────┐└╥┘ ┌───┐ + q_1: ┤ Delay(1000[dt]) ├─╫────┤ X ├─── + └─────────────────┘ ║ └─╥─┘ + ║ ┌────╨────┐ + c: 1/════════════════════╩═╡ c_0=0x1 ╞ + 0 └─────────┘ """ qc = QuantumCircuit(2, 1) qc.measure(0, 0) @@ -83,7 +84,6 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): expected = QuantumCircuit(2, 1) expected.measure(0, 0) - expected.delay(200, 0) expected.delay(1000, 1) # x.c_if starts after measure expected.x(1).c_if(0, True) @@ -104,13 +104,13 @@ def test_measure_after_measure(self, schedule_pass): 0 0 (scheduled) - ┌───┐ ┌─┐ - q_0: ──────┤ X ├───────┤M├─── - ┌─────┴───┴──────┐└╥┘┌─┐ - q_1: ┤ Delay(200[dt]) ├─╫─┤M├ - └────────────────┘ ║ └╥┘ - c: 1/═══════════════════╩══╩═ - 0 0 + ┌───┐ ┌─┐ + q_0: ───────┤ X ├───────┤M├─── + ┌──────┴───┴──────┐└╥┘┌─┐ + q_1: ┤ Delay(1200[dt]) ├─╫─┤M├ + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩══╩═ + 0 0 """ qc = QuantumCircuit(2, 1) qc.x(0) @@ -124,7 +124,7 @@ def test_measure_after_measure(self, schedule_pass): expected = QuantumCircuit(2, 1) expected.x(0) expected.measure(0, 0) - expected.delay(200, 1) # 2nd measure starts at the same time as 1st measure starts + expected.delay(1200, 1) expected.measure(1, 0) self.assertEqual(expected, scheduled) @@ -146,16 +146,17 @@ def test_c_if_on_different_qubits(self, schedule_pass): 0 └─────────┘└─────────┘ (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────────────── - ├─────────────────┤ ║ └─╥─┘ ┌───┐ - q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── - └─────────────────┘ ║ ║ └─╥─┘ - ║ ┌────╨────┐ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0 = T ╞════╡ c_0 = T ╞ - 0 └─────────┘ └─────────┘ + + ┌─┐ + q_0: ───────────────────┤M├────────────────────── + ┌─────────────────┐└╥┘ ┌───┐ + q_1: ┤ Delay(1000[dt]) ├─╫────┤ X ├────────────── + ├─────────────────┤ ║ └─╥─┘ ┌───┐ + q_2: ┤ Delay(1000[dt]) ├─╫──────╫────────┤ X ├─── + └─────────────────┘ ║ ║ └─╥─┘ + ║ ┌────╨────┐┌────╨────┐ + c: 1/════════════════════╩═╡ c_0=0x1 ╞╡ c_0=0x1 ╞ + 0 └─────────┘└─────────┘ """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -168,7 +169,6 @@ def test_c_if_on_different_qubits(self, schedule_pass): expected = QuantumCircuit(3, 1) expected.measure(0, 0) - expected.delay(200, 0) expected.delay(1000, 1) expected.delay(1000, 2) expected.x(1).c_if(0, True) @@ -190,13 +190,13 @@ def test_shorter_measure_after_measure(self, schedule_pass): 0 0 (scheduled) - ┌─┐ - q_0: ──────────────────┤M├─── - ┌────────────────┐└╥┘┌─┐ - q_1: ┤ Delay(300[dt]) ├─╫─┤M├ - └────────────────┘ ║ └╥┘ - c: 1/═══════════════════╩══╩═ - 0 0 + ┌─┐ + q_0: ───────────────────┤M├─── + ┌─────────────────┐└╥┘┌─┐ + q_1: ┤ Delay(1000[dt]) ├─╫─┤M├ + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩══╩═ + 0 0 """ qc = QuantumCircuit(2, 1) qc.measure(0, 0) @@ -208,12 +208,13 @@ def test_shorter_measure_after_measure(self, schedule_pass): expected = QuantumCircuit(2, 1) expected.measure(0, 0) - expected.delay(300, 1) + expected.delay(1000, 1) expected.measure(1, 0) self.assertEqual(expected, scheduled) - def test_measure_after_c_if(self): + @data(ALAPSchedule, ASAPSchedule) + def test_measure_after_c_if(self, schedule_pass): """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. (input) @@ -227,27 +228,16 @@ def test_measure_after_c_if(self): c: 1/═╩═╡ c_0 = T ╞═╩═ 0 └─────────┘ 0 - (scheduled - ASAP) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├───────────────────── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├──────────────────────────── - └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ - q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ - ║ ┌────╨────┐ └╥┘└────────────────┘ - c: 1/════════════════════╩════╡ c_0 = T ╞═════╩═══════════════════ - 0 └─────────┘ 0 - - (scheduled - ALAP) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────── - └┬────────────────┤ ║ └─╥─┘ ┌─┐ - q_2: ─┤ Delay(200[dt]) ├─╫─────────╫─────────┤M├ - └────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/════════════════════╩════╡ c_0 = T ╞═════╩═ - 0 └─────────┘ 0 + (scheduled) + ┌─┐ + q_0: ───────────────────┤M├────────────── + ┌─────────────────┐└╥┘ ┌───┐ + q_1: ┤ Delay(1000[dt]) ├─╫────┤ X ├────── + ├─────────────────┤ ║ └─╥─┘ ┌─┐ + q_2: ┤ Delay(1200[dt]) ├─╫──────╫─────┤M├ + └─────────────────┘ ║ ┌────╨────┐└╥┘ + c: 1/════════════════════╩═╡ c_0=0x1 ╞═╩═ + 0 └─────────┘ 0 """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -255,27 +245,146 @@ def test_measure_after_c_if(self): qc.measure(2, 0) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - actual_asap = PassManager(ASAPSchedule(durations)).run(qc) - actual_alap = PassManager(ALAPSchedule(durations)).run(qc) - - # start times of 2nd measure depends on ASAP/ALAP - expected_asap = QuantumCircuit(3, 1) - expected_asap.measure(0, 0) - expected_asap.delay(200, 0) - expected_asap.delay(1000, 1) - expected_asap.x(1).c_if(0, 1) - expected_asap.measure(2, 0) - expected_asap.delay(200, 2) # delay after measure on q_2 - self.assertEqual(expected_asap, actual_asap) - - expected_aslp = QuantumCircuit(3, 1) - expected_aslp.measure(0, 0) - expected_aslp.delay(200, 0) - expected_aslp.delay(1000, 1) - expected_aslp.x(1).c_if(0, 1) - expected_aslp.delay(200, 2) - expected_aslp.measure(2, 0) # delay before measure on q_2 - self.assertEqual(expected_aslp, actual_alap) + pm = PassManager(schedule_pass(durations)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1200, 2) + expected.measure(0, 0) + expected.x(1).c_if(0, 1) + expected.measure(2, 0) + expected.draw() + + self.assertEqual(expected, scheduled) + + def test_parallel_gate_different_length(self): + """Test circuit having two parallel instruction with different length. + + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + + (expected, ALAP) + ┌────────────────┐┌───┐┌─┐ + q_0: ┤ Delay(200[dt]) ├┤ X ├┤M├ + └─────┬───┬──────┘└┬─┬┘└╥┘ + q_1: ──────┤ X ├────────┤M├──╫─ + └───┘ └╥┘ ║ + c: 2/════════════════════╩═══╩═ + 1 0 + + (expected, ASAP) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + """ + qc = QuantumCircuit(2, 2) + qc.x(0) + qc.x(1) + qc.measure(0, 0) + qc.measure(1, 1) + + durations = InstructionDurations( + [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] + ) + pm = PassManager(ALAPSchedule(durations)) + qc_alap = pm.run(qc) + + alap_expected = QuantumCircuit(2, 2) + alap_expected.delay(200, 0) + alap_expected.x(0) + alap_expected.x(1) + alap_expected.measure(0, 0) + alap_expected.measure(1, 1) + + self.assertEqual(qc_alap, alap_expected) + + pm = PassManager(ASAPSchedule(durations)) + qc_asap = pm.run(qc) + + asap_expected = QuantumCircuit(2, 2) + asap_expected.x(0) + asap_expected.x(1) + asap_expected.measure(0, 0) # immediately start after X gate + asap_expected.measure(1, 1) + + self.assertEqual(qc_asap, asap_expected) + + def test_parallel_gate_different_length_with_barrier(self): + """Test circuit having two parallel instruction with different length with barrier. + + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + + (expected, ALAP) + ┌────────────────┐┌───┐ ░ ┌─┐ + q_0: ┤ Delay(200[dt]) ├┤ X ├─░─┤M├─── + └─────┬───┬──────┘└───┘ ░ └╥┘┌─┐ + q_1: ──────┤ X ├─────────────░──╫─┤M├ + └───┘ ░ ║ └╥┘ + c: 2/═══════════════════════════╩══╩═ + 0 1 + + (expected, ASAP) + ┌───┐┌────────────────┐ ░ ┌─┐ + q_0: ┤ X ├┤ Delay(200[dt]) ├─░─┤M├─── + ├───┤└────────────────┘ ░ └╥┘┌─┐ + q_1: ┤ X ├───────────────────░──╫─┤M├ + └───┘ ░ ║ └╥┘ + c: 2/═══════════════════════════╩══╩═ + 0 1 + """ + qc = QuantumCircuit(2, 2) + qc.x(0) + qc.x(1) + qc.barrier() + qc.measure(0, 0) + qc.measure(1, 1) + + durations = InstructionDurations( + [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] + ) + pm = PassManager(ALAPSchedule(durations)) + qc_alap = pm.run(qc) + + alap_expected = QuantumCircuit(2, 2) + alap_expected.delay(200, 0) + alap_expected.x(0) + alap_expected.x(1) + alap_expected.barrier() + alap_expected.measure(0, 0) + alap_expected.measure(1, 1) + + self.assertEqual(qc_alap, alap_expected) + + pm = PassManager(ASAPSchedule(durations)) + qc_asap = pm.run(qc) + + asap_expected = QuantumCircuit(2, 2) + asap_expected.x(0) + asap_expected.delay(200, 0) + asap_expected.x(1) + asap_expected.barrier() + asap_expected.measure(0, 0) + asap_expected.measure(1, 1) + + self.assertEqual(qc_asap, asap_expected) if __name__ == "__main__": From 0b00a5cc79177687b42207f0b77351fb640b01eb Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 17 Feb 2022 03:53:27 +0900 Subject: [PATCH 2/9] Update behavior of passes Added `BaseScheduler` as a parent class of ASAP and ALAP passes. These scheduler can take two control parameters `clbit_write_latency` and `conditional_latency`. These represent I/O latency of clbits. In addition, delays in the very end of the scheduled circuit is readded because Dynamical Decoupling passes inserts echo sequence there. More unittests and reno are also added. --- qiskit/transpiler/passes/scheduling/alap.py | 128 +++--- qiskit/transpiler/passes/scheduling/asap.py | 126 +++--- .../passes/scheduling/base_scheduler.py | 231 ++++++++++ ...ade-alap-asap-passes-bcacc0f1053c9828.yaml | 91 ++++ .../transpiler/test_instruction_alignments.py | 8 +- .../python/transpiler/test_scheduling_pass.py | 393 +++++++++++++++--- 6 files changed, 807 insertions(+), 170 deletions(-) create mode 100644 qiskit/transpiler/passes/scheduling/base_scheduler.py create mode 100644 releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index d55fc9c83589..7a0075042138 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -11,37 +11,20 @@ # that they have been altered from the originals. """ALAP Scheduling.""" -from qiskit.circuit import Delay, Qubit -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGCircuit, DAGInNode -from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit import Delay, Qubit, Measure, Gate +from qiskit.dagcircuit import DAGCircuit 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`. @@ -67,41 +50,70 @@ def run(self, dag): 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." - ) - - this_t0 = max(idle_before[q] for q in node.qargs + node.cargs + node.op.condition_bits) - this_t1 = this_t0 + node.op.duration - - for prev_node in dag.predecessors(node): - if isinstance(prev_node, DAGInNode): - continue - # Keep current bit status until operation complete - for blocked_bit in set(node.op.condition_bits) & set(prev_node.cargs): - idle_before[blocked_bit] = this_t1 - - for bit in node.qargs + node.cargs: - before = idle_before[bit] - delta = this_t0 - before - if before > 0 and delta > 0 and isinstance(bit, Qubit): + op_duration = self._get_node_duration(node, bit_indices, dag) + + # compute t0, t1: instruction interval + # 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): + 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) + if t0c > t0q: + # this is situation something like below + # + # t0q + # Q ░░░░░░░░░░░|▒▒▒▒ + # C ░░░░░░|▒▒▒▒▒▒▒▒▒ + # t0c + # + # In this case, there is no actual clbit read before gate. + # + # t1 = t0c + # Q ░░░░░░|▒▒▒||▒▒▒▒ + # C ░░░|▒▒|▒▒▒▒▒▒▒▒▒ + # t1 + conditional_latency + # + # rather than naively doing + # + # t1 = t0c + duration + # Q ░░|▒▒▒|░░░░|▒▒▒▒ + # C |▒▒|░░|▒▒▒▒▒▒▒▒▒ + # t0c + duration + conditional_latency + # + t0 = max(t0q, t0c - op_duration) + else: + t0 = t0q + 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: + # 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] = this_t1 + idle_before[bit] = t1 new_dag.apply_operation_front(node.op, node.qargs, node.cargs) diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index aadff8ab0560..1da05ea28aac 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -11,37 +11,20 @@ # that they have been altered from the originals. """ASAP Scheduling.""" -from qiskit.circuit import Delay, Qubit -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGCircuit, DAGOutNode -from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit import Delay, Qubit, Measure, Gate +from qiskit.dagcircuit import DAGCircuit 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`. @@ -68,44 +51,75 @@ def run(self, dag): 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." - ) - - this_t0 = max(idle_after[q] for q in node.qargs + node.cargs + node.op.condition_bits) - this_t1 = this_t0 + node.op.duration - - for next_node in dag.successors(node): - if isinstance(next_node, DAGOutNode): - continue - # Keep current bit status until operation complete - for blocked_bit in set(node.op.condition_bits) & set(next_node.cargs): - idle_after[blocked_bit] = this_t1 - - for bit in node.qargs + node.cargs: - delta = this_t0 - idle_after[bit] + op_duration = self._get_node_duration(node, bit_indices, dag) + + # compute t0, t1: instruction interval + 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) + if t0q > t0c: + t0 = t0q + else: + # This is situation something like below + # + # 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. + # + # t0 = t0c - clbit_write_latency + # Q ▒▒▒▒|░▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒|▒▒▒▒▒▒▒ + # t0c + # + # rather than naively doing + # + # t0 = t0c + # Q ▒▒▒▒|░░░|▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒|░░▒▒▒▒▒ + # + 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) + 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] = this_t1 + idle_after[bit] = t1 new_dag.apply_operation_back(node.op, node.qargs, node.cargs) 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 diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py new file mode 100644 index 000000000000..8513ac2c232e --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -0,0 +1,231 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""Base circuit scheduling pass.""" + +from typing import Dict +from qiskit.transpiler import InstructionDurations +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion +from qiskit.dagcircuit import DAGOpNode, DAGCircuit +from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.transpiler.exceptions import TranspilerError + + +class BaseScheduler(TransformationPass): + """Base scheduler pass. + + Classical register lock for control flow operations + + In the dispersive QND readout scheme, qubit is measured with microwave stimulus to qubit (Q) + followed by resonator ring-down (depopulation). This microwave signal is recorded + in the buffer memory (B) with hardware kernel, then a discriminated (D) binary value + is moved to the classical register (C). + The sequence from t0 to t1 of the measure instruction interval might be modeled as follows: + + .. parsed-literal:: + + Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + B ░░▒▒▒▒▒▒▒▒░░░░░░░░░ + D ░░░░░░░░░░▒▒▒▒▒▒░░░ + C ░░░░░░░░░░░░░░░░▒▒░ + + However, ``QuantumCircuit`` representation is not enough accurate to represent + this model. In the circuit representation, thus ``Qubit`` is occupied by the + stimulus microwave signal during the first half of the interval, + and ``Clbit`` is only occupied at the very end of the interval. + + This precise model may induce weird edge case. + + .. parsed-literal:: + + ┌───┐ + q_0: ───┤ X ├────── + └─╥─┘ ┌─┐ + q_1: ─────╫─────┤M├ + ┌────╨────┐└╥┘ + c: 1/╡ c_0=0x1 ╞═╩═ + └─────────┘ 0 + + In this example, user may intend to measure the state of ``q_1``, after ``XGate`` is + applied to the ``q_0``. This is correct interpretation from viewpoint of + the topological node ordering, i.e. x gate node come in front of the measure node. + However, according to the measurement model above, the data in the register + is unchanged during the stimulus, thus two nodes are simultaneously operated. + If one `alap`-schedule this circuit, it may return following circuit. + + .. parsed-literal:: + + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(500[dt]) ├───┤ X ├─── + └──────┬─┬───────┘ └─╥─┘ + q_1: ───────┤M├─────────────╫───── + └╥┘ ┌────╨────┐ + c: 1/════════╩═════════╡ c_0=0x1 ╞ + 0 └─────────┘ + + It looks like the topological order of nodes is inverted. This behavior can be + understood by considering the control flow model described above, + + .. parsed-literal:: + + : Quantum Circuit, first-measure + 0 ░░░░░░░░░░░░▒▒▒▒▒▒░ + 1 ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + + : In wire q0 + Q ░░░░░░░░░░░░░░░▒▒▒░ + C ░░░░░░░░░░░░▒▒░░░░░ + + : In wire q1 + Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + B ░░▒▒▒▒▒▒▒▒░░░░░░░░░ + D ░░░░░░░░░░▒▒▒▒▒▒░░░ + C ░░░░░░░░░░░░░░░░▒▒░ + + Since there is no qubit register (Q0, Q1) overlap, the node ordering is determined by the + shared classical register C. As you can see, the execution order is still + preserved on C, i.e. read C then apply ``XGate``, finally store the measured outcome in C. + Because ``DAGOpNode`` cannot define different durations for associated registers, + the time ordering of two nodes is inverted anyways. + + This behavior can be controlled by ``clbit_write_latency`` and ``conditional_latency``. + The former parameter determines the delay of the register write-access from + the beginning of the measure instruction t0, and another parameter determines + the delay of conditional gate operation from t0 which comes from the register read-access. + + Since we usually expect topological ordering and time ordering are identical + without the context of microarchitecture, both latencies are set to zero by default. + In this case, ``Measure`` instruction immediately locks the register C. + Under this configuration, the `alap`-scheduled circuit of above example may become + + .. parsed-literal:: + + ┌───┐ + q_0: ───┤ X ├────── + └─╥─┘ ┌─┐ + q_1: ─────╫─────┤M├ + ┌────╨────┐└╥┘ + c: 1/╡ c_0=0x1 ╞═╩═ + └─────────┘ 0 + + If the backend microarchitecture supports smart scheduling of the control flow, i.e. + it may separately schedule qubit and classical register, + insertion of the delay yields unnecessary longer total execution time. + + .. parsed-literal:: + : Quantum Circuit, first-xgate + 0 ░▒▒▒░░░░░░░░░░░░░░░ + 1 ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + + : In wire q0 + Q ░▒▒▒░░░░░░░░░░░░░░░ + C ░░░░░░░░░░░░░░░░░░░ (zero latency) + + : In wire q1 + Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + C ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ (zero latency, scheduled after C0 read-access) + + However this result is much more intuitive in the topological ordering view. + If finite conditional latency is provided, for example, 30 dt, the circuit + is scheduled as follows. + + .. parsed-literal:: + + ┌───────────────┐ ┌───┐ + q_0: ┤ Delay(30[dt]) ├───┤ X ├────── + ├───────────────┤ └─╥─┘ ┌─┐ + q_1: ┤ Delay(30[dt]) ├─────╫─────┤M├ + └───────────────┘┌────╨────┐└╥┘ + c: 1/═════════════════╡ c_0=0x1 ╞═╩═ + └─────────┘ 0 + + with the timing model: + + .. parsed-literal:: + : Quantum Circuit, first-xgate + 0 ░░▒▒▒░░░░░░░░░░░░░░░ + 1 ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + + : In wire q0 + Q ░░▒▒▒░░░░░░░░░░░░░░░ + C ░▒░░░░░░░░░░░░░░░░░░ (30dt latency) + + : In wire q1 + Q ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + C ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + + See https://arxiv.org/abs/2102.01682 for more details. + + """ + + def __init__( + self, + durations: InstructionDurations, + clbit_write_latency: int = 0, + conditional_latency: int = 0, + ): + """Scheduler initializer. + + Args: + durations: Durations of instructions to be used in scheduling + clbit_write_latency: A control flow constraints. Because standard superconducting + quantum processor implement dispersive QND readout, the actual data transfer + to the clbit happens after the round-trip stimulus signal is buffered + and discriminated into quantum state. + The interval ``[t0, t0 + clbit_write_latency]`` is regarded as idle time + for clbits associated with the measure instruction. + This defaults to 0 dt which is identical to Qiskit Pulse scheduler. + conditional_latency: A control flow constraints. This value represents + a latency of reading a classical register for the conditional operation. + The gate operation occurs after this latency. This appears as a delay + in front of the DAGOpNode of the gate. + This defaults to 0 dt. + """ + super().__init__() + self.durations = durations + + # Control flow constraints. + self.clbit_write_latency = clbit_write_latency + self.conditional_latency = conditional_latency + + # Ensure op node durations are attached and in consistent unit + self.requires.append(TimeUnitConversion(durations)) + + @staticmethod + def _get_node_duration( + node: DAGOpNode, + bit_index_map: Dict, + dag: DAGCircuit, + ) -> int: + """A helper method to get duration from node or calibration.""" + indices = [bit_index_map[qarg] for qarg in node.qargs] + + if dag.has_calibration_for(node): + # If node has calibration, this value should be the highest priority + cal_key = tuple(indices), tuple(float(p) for p in node.op.params) + duration = dag.calibrations[node.op.name][cal_key].duration + else: + duration = node.op.duration + + if isinstance(duration, ParameterExpression): + raise TranspilerError( + f"Parameterized duration ({duration}) " + f"of {node.op.name} on qubits {indices} is not bounded." + ) + if duration is None: + raise TranspilerError(f"Duration of {node.op.name} on qubits {indices} is not found.") + + return duration + + def run(self, dag: DAGCircuit): + raise NotImplementedError diff --git a/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml new file mode 100644 index 000000000000..1ad08fe8bfa6 --- /dev/null +++ b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml @@ -0,0 +1,91 @@ +--- +upgrade: + - | + The circuit scheduling passes + :class:`~qiskit.transpiler.passes.scheduling.asap.ASAPSchedule` and + :class:`~qiskit.transpiler.passes.scheduling.alap.ALAPSchedule` have been upgraded. + Now these passes can be instantiated with two new parameters + ``clbit_write_latency`` and ``conditional_latency``, which allows scheduler to + more carefully schedule instructions with classical feedback. + The former option represents a latency of clbit access from the begging of the + measurement instruction, and the other represents a latency of the + conditional gate operation from the first conditional clbit read-access. + + The standard behavior of these passes has been also upgraded to align + timing ordering with the topological ordering of the DAG nodes. + This change may affect the scheduling outcome if it includes conditional operations, + or simultaneously measuring two qubits with the same classical register (edge-case). + To reproduce conventional behavior, create a pass manager with one of + scheduling passes with ``clbit_write_latency`` identical to the measurement instruction length. + + The followign example may clearly explain the change of scheduler spec. + + .. parsed-literal:: + + ┌───┐┌─┐ + q_0: ┤ X ├┤M├────────────── + └───┘└╥┘ ┌───┐ + q_1: ──────╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ──────╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/══════╩═╡ c_0=0x1 ╞═╩═ + 0 └─────────┘ 0 + + The conventional behavior is as follows. + + .. jupyter-execute:: + + from qiskit import QuantumCircuit + from qiskit.transpiler import InstructionDurations, PassManager + from qiskit.transpiler.passes import ALAPSchedule + from qiskit.visualization.timeline import draw + + circuit = QuantumCircuit(3, 1) + circuit.x(0) + circuit.measure(0, 0) + circuit.x(1).c_if(0, 1) + circuit.measure(2, 0) + + durations = InstructionDurations([("x", None, 160), ("measure", None, 800)]) + + pm = PassManager( + ALAPSchedule(durations, clbit_write_latency=800, conditional_latency=0) + ) + draw(pm.run(circuit)) + + As you can see in the timeline view, the measurement on ``q_2`` starts before + the conditional X gate on the ``q_1``, which seems to be opposite to the + topological ordering of the node. This is also expected behavior + because clbit write-access happens at the end edge of the measure instruction, + and the read-access of the conditional gate happens the begin edge of the instruction. + Thus topological ordering is preserved on the timeslot of the classical register, + which is not captured by the timeline view. + However, this assumes a paticular microarchitecture design, and the circuit is + not necessary scheduled like this. + + By using the default configuration of passes, the circuit is schedule like below. + + .. jupyter-execute:: + + from qiskit import QuantumCircuit + from qiskit.transpiler import InstructionDurations, PassManager + from qiskit.transpiler.passes import ALAPSchedule + from qiskit.visualization.timeline import draw + + circuit = QuantumCircuit(3, 1) + circuit.x(0) + circuit.measure(0, 0) + circuit.x(1).c_if(0, 1) + circuit.measure(2, 0) + + durations = InstructionDurations([("x", None, 160), ("measure", None, 800)]) + + pm = PassManager(ALAPSchedule(durations)) + draw(pm.run(circuit)) + + Note that clbit is locked throughout the measurement instruction interval. + This behavior is designed based on the Qiskit Pulse, in which the acquire instruction takes + ``AcquireChannel`` and ``MemorySlot`` which are not allowed to overlap with other instructions, + i.e. simultaneous memory access from the different instructions is prohibited. + This also always aligns the timing ordering with the topological node ordering. diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py index c9d09ee4af2d..4822300ffcee 100644 --- a/test/python/transpiler/test_instruction_alignments.py +++ b/test/python/transpiler/test_instruction_alignments.py @@ -44,7 +44,13 @@ def setUp(self): ] ) self.time_conversion_pass = TimeUnitConversion(inst_durations=instruction_durations) - self.scheduling_pass = ALAPSchedule(durations=instruction_durations) + # reproduce old behavior of 0.20.0 before #7655 + # currently default write latency is 0 + self.scheduling_pass = ALAPSchedule( + durations=instruction_durations, + clbit_write_latency=1600, + conditional_latency=0, + ) self.align_measure_pass = AlignMeasures(alignment=16) def test_t1_experiment_type(self): diff --git a/test/python/transpiler/test_scheduling_pass.py b/test/python/transpiler/test_scheduling_pass.py index aa84e213032b..b913107759a4 100644 --- a/test/python/transpiler/test_scheduling_pass.py +++ b/test/python/transpiler/test_scheduling_pass.py @@ -14,7 +14,7 @@ import unittest -from ddt import ddt, data +from ddt import ddt, data, unpack from qiskit import QuantumCircuit from qiskit.test import QiskitTestCase from qiskit.transpiler.instruction_durations import InstructionDurations @@ -29,7 +29,6 @@ class TestSchedulingPass(QiskitTestCase): def test_alap_agree_with_reverse_asap_reverse(self): """Test if ALAP schedule agrees with doubly-reversed ASAP schedule.""" qc = QuantumCircuit(2) - qc.barrier() qc.h(0) qc.delay(500, 1) qc.cx(0, 1) @@ -52,7 +51,7 @@ def test_alap_agree_with_reverse_asap_reverse(self): @data(ALAPSchedule, ASAPSchedule) def test_classically_controlled_gate_after_measure(self, schedule_pass): """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. - See: https://github.com/Qiskit/qiskit-terra/issues/7006 + See: https://github.com/Qiskit/qiskit-terra/issues/7654 (input) ┌─┐ @@ -65,14 +64,14 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): 0 └─────────┘ (scheduled) - ┌─┐ - q_0: ───────────────────┤M├─────────── - ┌─────────────────┐└╥┘ ┌───┐ - q_1: ┤ Delay(1000[dt]) ├─╫────┤ X ├─── - └─────────────────┘ ║ └─╥─┘ - ║ ┌────╨────┐ - c: 1/════════════════════╩═╡ c_0=0x1 ╞ - 0 └─────────┘ + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├ + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── + └─────────────────┘ ║ └─╥─┘ + ║ ┌────╨────┐ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════ + 0 └─────────┘ """ qc = QuantumCircuit(2, 1) qc.measure(0, 0) @@ -86,13 +85,14 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): expected.measure(0, 0) expected.delay(1000, 1) # x.c_if starts after measure expected.x(1).c_if(0, True) + expected.delay(200, 0) self.assertEqual(expected, scheduled) @data(ALAPSchedule, ASAPSchedule) def test_measure_after_measure(self, schedule_pass): """Test if ALAP/ASAP schedules circuits with measure after measure with a common clbit. - See: https://github.com/Qiskit/qiskit-terra/issues/7006 + See: https://github.com/Qiskit/qiskit-terra/issues/7654 (input) ┌───┐┌─┐ @@ -104,13 +104,13 @@ def test_measure_after_measure(self, schedule_pass): 0 0 (scheduled) - ┌───┐ ┌─┐ - q_0: ───────┤ X ├───────┤M├─── - ┌──────┴───┴──────┐└╥┘┌─┐ - q_1: ┤ Delay(1200[dt]) ├─╫─┤M├ - └─────────────────┘ ║ └╥┘ - c: 1/════════════════════╩══╩═ - 0 0 + ┌───┐ ┌─┐┌─────────────────┐ + q_0: ───────┤ X ├───────┤M├┤ Delay(1000[dt]) ├ + ┌──────┴───┴──────┐└╥┘└───────┬─┬───────┘ + q_1: ┤ Delay(1200[dt]) ├─╫─────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩══════════╩═════════ + 0 0 """ qc = QuantumCircuit(2, 1) qc.x(0) @@ -126,6 +126,7 @@ def test_measure_after_measure(self, schedule_pass): expected.measure(0, 0) expected.delay(1200, 1) expected.measure(1, 0) + expected.delay(1000, 0) self.assertEqual(expected, scheduled) @@ -147,16 +148,16 @@ def test_c_if_on_different_qubits(self, schedule_pass): (scheduled) - ┌─┐ - q_0: ───────────────────┤M├────────────────────── - ┌─────────────────┐└╥┘ ┌───┐ - q_1: ┤ Delay(1000[dt]) ├─╫────┤ X ├────────────── - ├─────────────────┤ ║ └─╥─┘ ┌───┐ - q_2: ┤ Delay(1000[dt]) ├─╫──────╫────────┤ X ├─── - └─────────────────┘ ║ ║ └─╥─┘ - ║ ┌────╨────┐┌────╨────┐ - c: 1/════════════════════╩═╡ c_0=0x1 ╞╡ c_0=0x1 ╞ - 0 └─────────┘└─────────┘ + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────────────── + ├─────────────────┤ ║ └─╥─┘ ┌───┐ + q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── + └─────────────────┘ ║ ║ └─╥─┘ + ║ ┌────╨────┐ ┌────╨────┐ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + 0 └─────────┘ └─────────┘ """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -173,6 +174,7 @@ def test_c_if_on_different_qubits(self, schedule_pass): expected.delay(1000, 2) expected.x(1).c_if(0, True) expected.x(2).c_if(0, True) + expected.delay(200, 0) self.assertEqual(expected, scheduled) @@ -190,19 +192,19 @@ def test_shorter_measure_after_measure(self, schedule_pass): 0 0 (scheduled) - ┌─┐ - q_0: ───────────────────┤M├─── - ┌─────────────────┐└╥┘┌─┐ - q_1: ┤ Delay(1000[dt]) ├─╫─┤M├ - └─────────────────┘ ║ └╥┘ - c: 1/════════════════════╩══╩═ - 0 0 + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(700[dt]) ├ + ┌─────────────────┐└╥┘└──────┬─┬───────┘ + q_1: ┤ Delay(1000[dt]) ├─╫────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩═════════╩═════════ + 0 0 """ qc = QuantumCircuit(2, 1) qc.measure(0, 0) qc.measure(1, 0) - durations = InstructionDurations([("measure", 0, 1000), ("measure", 1, 700)]) + durations = InstructionDurations([("measure", [0], 1000), ("measure", [1], 700)]) pm = PassManager(schedule_pass(durations)) scheduled = pm.run(qc) @@ -210,6 +212,7 @@ def test_shorter_measure_after_measure(self, schedule_pass): expected.measure(0, 0) expected.delay(1000, 1) expected.measure(1, 0) + expected.delay(700, 0) self.assertEqual(expected, scheduled) @@ -229,15 +232,15 @@ def test_measure_after_c_if(self, schedule_pass): 0 └─────────┘ 0 (scheduled) - ┌─┐ - q_0: ───────────────────┤M├────────────── - ┌─────────────────┐└╥┘ ┌───┐ - q_1: ┤ Delay(1000[dt]) ├─╫────┤ X ├────── - ├─────────────────┤ ║ └─╥─┘ ┌─┐ - q_2: ┤ Delay(1200[dt]) ├─╫──────╫─────┤M├ - └─────────────────┘ ║ ┌────╨────┐└╥┘ - c: 1/════════════════════╩═╡ c_0=0x1 ╞═╩═ - 0 └─────────┘ 0 + ┌─┐┌─────────────────┐ + q_0: ───────────────────┤M├┤ Delay(1000[dt]) ├────────────────── + ┌─────────────────┐└╥┘└──────┬───┬──────┘┌────────────────┐ + q_1: ┤ Delay(1000[dt]) ├─╫────────┤ X ├───────┤ Delay(800[dt]) ├ + ├─────────────────┤ ║ └─╥─┘ └──────┬─┬───────┘ + q_2: ┤ Delay(1000[dt]) ├─╫──────────╫────────────────┤M├──────── + └─────────────────┘ ║ ┌────╨────┐ └╥┘ + c: 1/════════════════════╩═════╡ c_0=0x1 ╞════════════╩═════════ + 0 └─────────┘ 0 """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -250,11 +253,12 @@ def test_measure_after_c_if(self, schedule_pass): expected = QuantumCircuit(3, 1) expected.delay(1000, 1) - expected.delay(1200, 2) + expected.delay(1000, 2) expected.measure(0, 0) expected.x(1).c_if(0, 1) expected.measure(2, 0) - expected.draw() + expected.delay(1000, 0) + expected.delay(800, 1) self.assertEqual(expected, scheduled) @@ -280,13 +284,14 @@ def test_parallel_gate_different_length(self): 1 0 (expected, ASAP) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - ├───┤└╥┘┌─┐ - q_1: ┤ X ├─╫─┤M├ - └───┘ ║ └╥┘ - c: 2/══════╩══╩═ - 0 1 + ┌───┐┌─┐┌────────────────┐ + q_0: ┤ X ├┤M├┤ Delay(200[dt]) ├ + ├───┤└╥┘└──────┬─┬───────┘ + q_1: ┤ X ├─╫────────┤M├──────── + └───┘ ║ └╥┘ + c: 2/══════╩═════════╩═════════ + 0 1 + """ qc = QuantumCircuit(2, 2) qc.x(0) @@ -317,6 +322,7 @@ def test_parallel_gate_different_length(self): asap_expected.x(1) asap_expected.measure(0, 0) # immediately start after X gate asap_expected.measure(1, 1) + asap_expected.delay(200, 0) self.assertEqual(qc_asap, asap_expected) @@ -386,6 +392,283 @@ def test_parallel_gate_different_length_with_barrier(self): self.assertEqual(qc_asap, asap_expected) + def test_measure_after_c_if_on_edge_locking(self): + """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. + + The scheduler is configured to reproduce behavior of the 0.20.0, + in which clbit lock is applied to the end-edge of measure instruction. + See https://github.com/Qiskit/qiskit-terra/pull/7655 + + (input) + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 + + (ASAP scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├───────────────────── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├──────────────────────────── + └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ + q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ + ║ ┌────╨────┐ └╥┘└────────────────┘ + c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═══════════════════ + 0 └─────────┘ 0 + + (ALAP scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────── + └┬────────────────┤ ║ └─╥─┘ ┌─┐ + q_2: ─┤ Delay(200[dt]) ├─╫─────────╫─────────┤M├ + └────────────────┘ ║ ┌────╨────┐ └╥┘ + c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═ + 0 └─────────┘ 0 + + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, 1) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + + # lock at the end edge + actual_asap = PassManager(ASAPSchedule(durations, clbit_write_latency=1000)).run(qc) + actual_alap = PassManager(ALAPSchedule(durations, clbit_write_latency=1000)).run(qc) + + # start times of 2nd measure depends on ASAP/ALAP + expected_asap = QuantumCircuit(3, 1) + expected_asap.measure(0, 0) + expected_asap.delay(1000, 1) + expected_asap.x(1).c_if(0, 1) + expected_asap.measure(2, 0) + expected_asap.delay(200, 0) + expected_asap.delay(200, 2) + self.assertEqual(expected_asap, actual_asap) + + expected_alap = QuantumCircuit(3, 1) + expected_alap.measure(0, 0) + expected_alap.delay(1000, 1) + expected_alap.x(1).c_if(0, 1) + expected_alap.delay(200, 2) + expected_alap.measure(2, 0) + expected_alap.delay(200, 0) + self.assertEqual(expected_alap, actual_alap) + + @data([100, 200], [500, 0], [1000, 200]) + @unpack + def test_active_reset_circuit(self, write_lat, cond_lat): + """Test practical example of reset circuit. + + Because of the stimulus pulse overlap with the previous XGate on the q register, + measure instruction is always triggered after XGate regardless of write latency. + Thus only conditional latency matters in the scheduling. + + (input) + ┌─┐ ┌───┐ ┌─┐ ┌───┐ ┌─┐ ┌───┐ + q: ┤M├───┤ X ├───┤M├───┤ X ├───┤M├───┤ X ├─── + └╥┘ └─╥─┘ └╥┘ └─╥─┘ └╥┘ └─╥─┘ + ║ ┌────╨────┐ ║ ┌────╨────┐ ║ ┌────╨────┐ + c: 1/═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞ + 0 └─────────┘ 0 └─────────┘ 0 └─────────┘ + + """ + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + + durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) + actual_asap = PassManager( + ASAPSchedule(durations, clbit_write_latency=write_lat, conditional_latency=cond_lat) + ).run(qc) + actual_alap = PassManager( + ALAPSchedule(durations, clbit_write_latency=write_lat, conditional_latency=cond_lat) + ).run(qc) + + expected = QuantumCircuit(1, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + + self.assertEqual(expected, actual_asap) + self.assertEqual(expected, actual_alap) + + def test_random_complicated_circuit(self): + """Test scheduling complicated circuit with control flow. + + (input) + ┌────────────────┐ ┌───┐ ░ ┌───┐ » + q_0: ┤ Delay(100[dt]) ├───┤ X ├────░──────────────────┤ X ├───» + └────────────────┘ └─╥─┘ ░ ┌───┐ └─╥─┘ » + q_1: ───────────────────────╫──────░───────┤ X ├────────╫─────» + ║ ░ ┌─┐ └─╥─┘ ║ » + q_2: ───────────────────────╫──────░─┤M├─────╫──────────╫─────» + ┌────╨────┐ ░ └╥┘┌────╨────┐┌────╨────┐» + c: 1/══════════════════╡ c_0=0x1 ╞════╩═╡ c_0=0x0 ╞╡ c_0=0x0 ╞» + └─────────┘ 0 └─────────┘└─────────┘» + « ┌────────────────┐┌───┐ + «q_0: ┤ Delay(300[dt]) ├┤ X ├─────■───── + « └────────────────┘└───┘ ┌─┴─┐ + «q_1: ────────■─────────────────┤ X ├─── + « ┌─┴─┐ ┌─┐ └─╥─┘ + «q_2: ──────┤ X ├────────┤M├──────╫───── + « └───┘ └╥┘ ┌────╨────┐ + «c: 1/════════════════════╩══╡ c_0=0x0 ╞ + « 0 └─────────┘ + + (ASAP scheduled) duration = 2800 dt + ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» + q_0: ┤ Delay(100[dt]) ├┤ Delay(100[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├» + ├────────────────┤└────────────────┘ └─╥─┘ ░ ├─────────────────┤» + q_1: ┤ Delay(300[dt]) ├───────────────────────╫──────░─┤ Delay(1200[dt]) ├» + ├────────────────┤ ║ ░ └───────┬─┬───────┘» + q_2: ┤ Delay(300[dt]) ├───────────────────────╫──────░─────────┤M├────────» + └────────────────┘ ┌────╨────┐ ░ └╥┘ » + c: 1/════════════════════════════════════╡ c_0=0x1 ╞════════════╩═════════» + └─────────┘ 0 » + « ┌───┐ ┌────────────────┐» + «q_0: ────────────────────────────────┤ X ├───┤ Delay(300[dt]) ├» + « ┌───┐ └─╥─┘ └────────────────┘» + «q_1: ───┤ X ├──────────────────────────╫─────────────■─────────» + « └─╥─┘ ┌────────────────┐ ║ ┌─┴─┐ » + «q_2: ─────╫─────┤ Delay(300[dt]) ├─────╫───────────┤ X ├───────» + « ┌────╨────┐└────────────────┘┌────╨────┐ └───┘ » + «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» + « └─────────┘ └─────────┘ » + « ┌───┐ ┌────────────────┐ + «q_0: ──────┤ X ├────────────■─────┤ Delay(400[dt]) ├ + « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ + «q_1: ┤ Delay(400[dt]) ├───┤ X ├───┤ Delay(400[dt]) ├ + « └──────┬─┬───────┘ └─╥─┘ └────────────────┘ + «q_2: ───────┤M├─────────────╫─────────────────────── + « └╥┘ ┌────╨────┐ + «c: 1/════════╩═════════╡ c_0=0x0 ╞══════════════════ + « 0 └─────────┘ + + (ALAP scheduled) duration = 3100 + ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» + q_0: ┤ Delay(100[dt]) ├┤ Delay(100[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├» + ├────────────────┤└────────────────┘ └─╥─┘ ░ ├─────────────────┤» + q_1: ┤ Delay(300[dt]) ├───────────────────────╫──────░─┤ Delay(1200[dt]) ├» + ├────────────────┤ ║ ░ └───────┬─┬───────┘» + q_2: ┤ Delay(300[dt]) ├───────────────────────╫──────░─────────┤M├────────» + └────────────────┘ ┌────╨────┐ ░ └╥┘ » + c: 1/════════════════════════════════════╡ c_0=0x1 ╞════════════╩═════════» + └─────────┘ 0 » + « ┌───┐ ┌────────────────┐» + «q_0: ────────────────────────────────┤ X ├───┤ Delay(300[dt]) ├» + « ┌───┐ ┌────────────────┐ └─╥─┘ └────────────────┘» + «q_1: ───┤ X ├───┤ Delay(300[dt]) ├─────╫─────────────■─────────» + « └─╥─┘ ├────────────────┤ ║ ┌─┴─┐ » + «q_2: ─────╫─────┤ Delay(600[dt]) ├─────╫───────────┤ X ├───────» + « ┌────╨────┐└────────────────┘┌────╨────┐ └───┘ » + «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» + « └─────────┘ └─────────┘ » + « ┌───┐ ┌────────────────┐ + «q_0: ──────┤ X ├────────────■─────┤ Delay(700[dt]) ├ + « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ + «q_1: ┤ Delay(100[dt]) ├───┤ X ├───┤ Delay(700[dt]) ├ + « └──────┬─┬───────┘ └─╥─┘ └────────────────┘ + «q_2: ───────┤M├─────────────╫─────────────────────── + « └╥┘ ┌────╨────┐ + «c: 1/════════╩═════════╡ c_0=0x0 ╞══════════════════ + « 0 └─────────┘ + + """ + qc = QuantumCircuit(3, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, 1) + qc.barrier() + qc.measure(2, 0) + qc.x(1).c_if(0, 0) + qc.x(0).c_if(0, 0) + qc.delay(300, 0) + qc.cx(1, 2) + qc.x(0) + qc.cx(0, 1).c_if(0, 0) + qc.measure(2, 0) + + durations = InstructionDurations( + [("x", None, 100), ("measure", None, 1000), ("cx", None, 200)] + ) + + actual_asap = PassManager( + ASAPSchedule(durations, clbit_write_latency=100, conditional_latency=200) + ).run(qc) + actual_alap = PassManager( + ALAPSchedule(durations, clbit_write_latency=100, conditional_latency=200) + ).run(qc) + + expected_asap = QuantumCircuit(3, 1) + expected_asap.delay(100, 0) + expected_asap.delay(100, 0) # due to conditional latency of 200dt + expected_asap.delay(300, 1) + expected_asap.delay(300, 2) + expected_asap.x(0).c_if(0, 1) + expected_asap.barrier() + expected_asap.delay(1400, 0) + expected_asap.delay(1200, 1) + expected_asap.measure(2, 0) + expected_asap.x(1).c_if(0, 0) + expected_asap.x(0).c_if(0, 0) + expected_asap.delay(300, 0) + expected_asap.x(0) + expected_asap.delay(300, 2) + expected_asap.cx(1, 2) + expected_asap.delay(400, 1) + expected_asap.cx(0, 1).c_if(0, 0) + expected_asap.measure(2, 0) + expected_asap.delay(400, 0) + expected_asap.delay(400, 1) + self.assertEqual(expected_asap, actual_asap) + self.assertEqual(actual_asap.duration, 2800) + + expected_alap = QuantumCircuit(3, 1) + expected_alap.delay(100, 0) + expected_alap.delay(100, 0) # due to conditional latency of 200dt + expected_alap.delay(300, 1) + expected_alap.delay(300, 2) + expected_alap.x(0).c_if(0, 1) + expected_alap.barrier() + expected_alap.delay(1400, 0) + expected_alap.delay(1200, 1) + expected_alap.measure(2, 0) + expected_alap.x(1).c_if(0, 0) + expected_alap.x(0).c_if(0, 0) + expected_alap.delay(300, 0) + expected_alap.x(0) + expected_alap.delay(300, 1) + expected_alap.delay(600, 2) + expected_alap.cx(1, 2) + expected_alap.delay(100, 1) + expected_alap.cx(0, 1).c_if(0, 0) + expected_alap.measure(2, 0) + expected_alap.delay(700, 0) + expected_alap.delay(700, 1) + self.assertEqual(expected_alap, actual_alap) + self.assertEqual(actual_alap.duration, 3100) + if __name__ == "__main__": unittest.main() From d91569c0866b0c2a6a5f3e811f622e213e267f18 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Fri, 18 Feb 2022 19:22:48 +0900 Subject: [PATCH 3/9] fix ASAP conditional bug The conditional bit start time was only looking cregs. But this should start right before the gate. Co-authored-by: Toshinari Itoko --- qiskit/transpiler/passes/scheduling/asap.py | 16 +++++ .../python/transpiler/test_scheduling_pass.py | 67 ++++++++++++++++--- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index 1da05ea28aac..f48c8e3bdc3b 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -91,6 +91,22 @@ def run(self, dag): 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 diff --git a/test/python/transpiler/test_scheduling_pass.py b/test/python/transpiler/test_scheduling_pass.py index b913107759a4..6d234747fa1e 100644 --- a/test/python/transpiler/test_scheduling_pass.py +++ b/test/python/transpiler/test_scheduling_pass.py @@ -557,14 +557,14 @@ def test_random_complicated_circuit(self): «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» « └─────────┘ └─────────┘ » « ┌───┐ ┌────────────────┐ - «q_0: ──────┤ X ├────────────■─────┤ Delay(400[dt]) ├ + «q_0: ──────┤ X ├────────────■─────┤ Delay(700[dt]) ├ « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ - «q_1: ┤ Delay(400[dt]) ├───┤ X ├───┤ Delay(400[dt]) ├ - « └──────┬─┬───────┘ └─╥─┘ └────────────────┘ - «q_2: ───────┤M├─────────────╫─────────────────────── - « └╥┘ ┌────╨────┐ - «c: 1/════════╩═════════╡ c_0=0x0 ╞══════════════════ - « 0 └─────────┘ + «q_1: ┤ Delay(400[dt]) ├───┤ X ├───┤ Delay(700[dt]) ├ + « ├────────────────┤ └─╥─┘ └──────┬─┬───────┘ + «q_2: ┤ Delay(300[dt]) ├─────╫────────────┤M├──────── + « └────────────────┘┌────╨────┐ └╥┘ + «c: 1/══════════════════╡ c_0=0x0 ╞════════╩═════════ + « └─────────┘ 0 (ALAP scheduled) duration = 3100 ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» @@ -638,11 +638,12 @@ def test_random_complicated_circuit(self): expected_asap.cx(1, 2) expected_asap.delay(400, 1) expected_asap.cx(0, 1).c_if(0, 0) + expected_asap.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) + expected_asap.delay(700, 1) # no creg write until 100dt. thus measure can move left by 300dt. + expected_asap.delay(300, 2) expected_asap.measure(2, 0) - expected_asap.delay(400, 0) - expected_asap.delay(400, 1) self.assertEqual(expected_asap, actual_asap) - self.assertEqual(actual_asap.duration, 2800) + self.assertEqual(actual_asap.duration, 3100) expected_alap = QuantumCircuit(3, 1) expected_alap.delay(100, 0) @@ -669,6 +670,52 @@ def test_random_complicated_circuit(self): self.assertEqual(expected_alap, actual_alap) self.assertEqual(actual_alap.duration, 3100) + def test_dag_introduces_extra_dependency_between_conditionals(self): + """Test dependency between conditional operations in the scheduling. + + In the below example circuit, the conditional x on q1 could start at time 0, + however it must be scheduled after the conditional x on q0 in ASAP scheduling. + That is because circuit model used in the transpiler passes (DAGCircuit) + interprets instructions acting on common clbits must be run in the order + given by the original circuit (QuantumCircuit). + + (input) + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├─── + └─────┬───┬──────┘ └─╥─┘ + q_1: ──────┤ X ├────────────╫───── + └─╥─┘ ║ + ┌────╨────┐ ┌────╨────┐ + c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + └─────────┘ └─────────┘ + + (ASAP scheduled) + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── + ├────────────────┤ └─╥─┘ ┌───┐ + q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── + └────────────────┘ ║ └─╥─┘ + ┌────╨────┐┌────╨────┐ + c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ + └─────────┘└─────────┘ + """ + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, True) + qc.x(1).c_if(0, True) + + durations = InstructionDurations([("x", None, 160)]) + pm = PassManager(ASAPSchedule(durations)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.delay(100, 0) + expected.delay(100, 1) # due to extra dependency on clbits + expected.x(0).c_if(0, True) + expected.x(1).c_if(0, True) + + self.assertEqual(expected, scheduled) + if __name__ == "__main__": unittest.main() From 59698accf9265125bf9dbbd94d831bcef7bf7001 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Mon, 21 Feb 2022 16:56:18 +0900 Subject: [PATCH 4/9] respond to review comments - fix typo - update model drawing in comment - more comment --- qiskit/transpiler/passes/scheduling/alap.py | 60 ++++++++-------- qiskit/transpiler/passes/scheduling/asap.py | 68 +++++++++---------- .../passes/scheduling/base_scheduler.py | 4 +- ...ade-alap-asap-passes-bcacc0f1053c9828.yaml | 2 +- .../python/transpiler/test_scheduling_pass.py | 4 +- 5 files changed, 70 insertions(+), 68 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 7a0075042138..e793c8a16b6f 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -52,7 +52,10 @@ def run(self, dag): for node in reversed(list(dag.topological_op_nodes())): op_duration = self._get_node_duration(node, bit_indices, dag) - # compute t0, t1: instruction interval + # 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. @@ -61,10 +64,10 @@ def run(self, dag): 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) + # |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) @@ -73,31 +76,28 @@ def run(self, dag): 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) - if t0c > t0q: - # this is situation something like below - # - # t0q - # Q ░░░░░░░░░░░|▒▒▒▒ - # C ░░░░░░|▒▒▒▒▒▒▒▒▒ - # t0c - # - # In this case, there is no actual clbit read before gate. - # - # t1 = t0c - # Q ░░░░░░|▒▒▒||▒▒▒▒ - # C ░░░|▒▒|▒▒▒▒▒▒▒▒▒ - # t1 + conditional_latency - # - # rather than naively doing - # - # t1 = t0c + duration - # Q ░░|▒▒▒|░░░░|▒▒▒▒ - # C |▒▒|░░|▒▒▒▒▒▒▒▒▒ - # t0c + duration + conditional_latency - # - t0 = max(t0q, t0c - op_duration) - else: - t0 = t0q + # 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 diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index f48c8e3bdc3b..337a903d268b 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -53,36 +53,36 @@ def run(self, dag): for node in dag.topological_op_nodes(): op_duration = self._get_node_duration(node, bit_indices, dag) - # compute t0, t1: instruction interval + # 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) - if t0q > t0c: - t0 = t0q - else: - # This is situation something like below - # - # 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. - # - # t0 = t0c - clbit_write_latency - # Q ▒▒▒▒|░▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒|▒▒▒▒▒▒▒ - # t0c - # - # rather than naively doing - # - # t0 = t0c - # Q ▒▒▒▒|░░░|▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒|░░▒▒▒▒▒ - # - t0 = max(t0q, t0c - self.clbit_write_latency) + # 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 @@ -94,17 +94,17 @@ def run(self, dag): if t0q > t0c: # This is situation something like below # - # t0q - # Q ▒▒▒▒▒▒▒▒|░░ - # C ▒▒▒|░░░░░░░ - # t0c + # |t0q + # Q ▒▒▒▒▒▒▒▒▒░░ + # C ▒▒▒░░░░░░░░ + # |t0c # # In this case, you can insert readout access before tq0 # - # t0q - # Q ▒▒▒▒▒▒▒▒|▒▒ - # C ▒▒▒|░░|▒|░░ - # t0q - conditional_latency + # |t0q + # Q ▒▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒░░░▒▒░░░ + # |t0q - conditional_latency # t0c = max(t0q - self.conditional_latency, t0c) t1c = t0c + self.conditional_latency diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index 8513ac2c232e..326eb36c2ba5 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -73,8 +73,8 @@ class BaseScheduler(TransformationPass): c: 1/════════╩═════════╡ c_0=0x1 ╞ 0 └─────────┘ - It looks like the topological order of nodes is inverted. This behavior can be - understood by considering the control flow model described above, + It looks like the ordering of nodes is inverted (actually not). + This behavior can be understood by considering the control flow model described above, .. parsed-literal:: diff --git a/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml index 1ad08fe8bfa6..2199eb686514 100644 --- a/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml +++ b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml @@ -18,7 +18,7 @@ upgrade: To reproduce conventional behavior, create a pass manager with one of scheduling passes with ``clbit_write_latency`` identical to the measurement instruction length. - The followign example may clearly explain the change of scheduler spec. + The following example may clearly explain the change of scheduler spec. .. parsed-literal:: diff --git a/test/python/transpiler/test_scheduling_pass.py b/test/python/transpiler/test_scheduling_pass.py index 6d234747fa1e..5693ba0e4f27 100644 --- a/test/python/transpiler/test_scheduling_pass.py +++ b/test/python/transpiler/test_scheduling_pass.py @@ -639,7 +639,9 @@ def test_random_complicated_circuit(self): expected_asap.delay(400, 1) expected_asap.cx(0, 1).c_if(0, 0) expected_asap.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) - expected_asap.delay(700, 1) # no creg write until 100dt. thus measure can move left by 300dt. + expected_asap.delay( + 700, 1 + ) # no creg write until 100dt. thus measure can move left by 300dt. expected_asap.delay(300, 2) expected_asap.measure(2, 0) self.assertEqual(expected_asap, actual_asap) From 6240ce730de416578900abb86dd5839261d7868d Mon Sep 17 00:00:00 2001 From: knzwnao Date: Mon, 21 Feb 2022 17:11:04 +0900 Subject: [PATCH 5/9] update documentation and add todo comment --- qiskit/transpiler/passes/scheduling/alap.py | 2 ++ qiskit/transpiler/passes/scheduling/asap.py | 4 +++- .../passes/scheduling/base_scheduler.py | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index e793c8a16b6f..37422129764c 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -74,6 +74,8 @@ def run(self, dag): elif isinstance(node.op, Gate): t0q = max(idle_before[q] for q in node.qargs) if node.op.condition_bits: + # TODO consider conditional ops for non-gate instruction, if necessary + # 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): diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index 337a903d268b..155caac4556c 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -89,7 +89,9 @@ def run(self, dag): elif isinstance(node.op, Gate): t0q = max(idle_after[q] for q in node.qargs) if node.op.condition_bits: - # Conditional gate + # TODO consider conditional ops for non-gate instruction, if necessary + + # conditional is bit tricky due to conditional_latency t0c = max(idle_after[bit] for bit in node.op.condition_bits) if t0q > t0c: # This is situation something like below diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index 326eb36c2ba5..bbaeaaa1c015 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -66,14 +66,16 @@ class BaseScheduler(TransformationPass): .. parsed-literal:: ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(500[dt]) ├───┤ X ├─── - └──────┬─┬───────┘ └─╥─┘ - q_1: ───────┤M├─────────────╫───── - └╥┘ ┌────╨────┐ - c: 1/════════╩═════════╡ c_0=0x1 ╞ - 0 └─────────┘ - - It looks like the ordering of nodes is inverted (actually not). + q_0: ┤ Delay(500[dt]) ├───┤ X ├────── + └────────────────┘ └─╥─┘ ┌─┐ + q_1: ───────────────────────╫─────┤M├ + ┌────╨────┐└╥┘ + c: 1/══════════════════╡ c_0=0x1 ╞═╩═ + └─────────┘ 0 + + Note that there is no delay on ``q_1`` wire, and the measure instruction immediately + start after t=0, while the conditional gate starts after the delay. + It looks like the topological ordering between the nodes are flipped in the scheduled view. This behavior can be understood by considering the control flow model described above, .. parsed-literal:: From 8c258c74c00376ef649429aebbb3e74598d0b999 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Mon, 21 Feb 2022 17:28:10 +0900 Subject: [PATCH 6/9] update logic to insert conditional bit assert --- qiskit/transpiler/passes/scheduling/alap.py | 41 +++++----- qiskit/transpiler/passes/scheduling/asap.py | 77 ++++++++++--------- .../passes/scheduling/base_scheduler.py | 2 + 3 files changed, 66 insertions(+), 54 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 37422129764c..9f395e01f6f6 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -36,6 +36,7 @@ def run(self, dag): Raises: TranspilerError: if the circuit is not mapped on physical qubits. + TranspilerError: if conditional bit is added to non-supported instruction. """ if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ALAP schedule runs on physical circuits only") @@ -59,23 +60,9 @@ def run(self, dag): # 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): + if isinstance(node.op, self.__condition_supported): t0q = max(idle_before[q] for q in node.qargs) if node.op.condition_bits: - # TODO consider conditional ops for non-gate instruction, if necessary - # 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): @@ -107,9 +94,27 @@ def run(self, dag): t0 = t0q t1 = t0 + op_duration else: - # It happens to be Barrier - t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration + if node.op.condition_bits: + raise TranspilerError( + f"Conditional instruction {node.op.name} is not supported in ALAP scheduler." + ) + + 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) + else: + # It happens to be directives such as 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] diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index 155caac4556c..f2d8cb5e67df 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -36,6 +36,7 @@ def run(self, dag): Raises: TranspilerError: if the circuit is not mapped on physical qubits. + TranspilerError: if conditional bit is added to non-supported instruction. """ if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ASAP schedule runs on physical circuits only") @@ -56,41 +57,9 @@ def run(self, 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): + if isinstance(node.op, self.__condition_supported): t0q = max(idle_after[q] for q in node.qargs) if node.op.condition_bits: - # TODO consider conditional ops for non-gate instruction, if necessary - # conditional is bit tricky due to conditional_latency t0c = max(idle_after[bit] for bit in node.op.condition_bits) if t0q > t0c: @@ -119,9 +88,45 @@ def run(self, dag): 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 + if node.op.condition_bits: + raise TranspilerError( + f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." + ) + + 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 + else: + # It happens to be directives such as 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: diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index bbaeaaa1c015..4f19f461aced 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -17,6 +17,7 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion from qiskit.dagcircuit import DAGOpNode, DAGCircuit +from qiskit.circuit import Delay, Gate from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.transpiler.exceptions import TranspilerError @@ -169,6 +170,7 @@ class BaseScheduler(TransformationPass): See https://arxiv.org/abs/2102.01682 for more details. """ + __condition_supported = [Gate, Delay] def __init__( self, From de550137818a93cdcbaec2ebba5b519c7b28be22 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Mon, 21 Feb 2022 17:46:15 +0900 Subject: [PATCH 7/9] add more docs on topological ordering --- .../passes/scheduling/base_scheduler.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index 4f19f461aced..9286444c40e5 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -25,7 +25,44 @@ class BaseScheduler(TransformationPass): """Base scheduler pass. - Classical register lock for control flow operations + Policy of topological node ordering in scheduling + + The DAG representation of ``QuantumCircuit`` respects the node ordering also in the + classical register wires, though theoretically two conditional instructions + conditioned on the same register are commute, i.e. read-access to the + classical register doesn't change its state. + + .. parsed-literal:: + + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├─── + └─────┬───┬──────┘ └─╥─┘ + q_1: ──────┤ X ├────────────╫───── + └─╥─┘ ║ + ┌────╨────┐ ┌────╨────┐ + c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + └─────────┘ └─────────┘ + + The scheduler SHOULD comply with above topological ordering policy of the DAG circuit. + Accordingly, the `asap`-scheduled circuit will become + + .. parsed-literal:: + + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── + ├────────────────┤ └─╥─┘ ┌───┐ + q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── + └────────────────┘ ║ └─╥─┘ + ┌────╨────┐┌────╨────┐ + c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ + └─────────┘└─────────┘ + + Note that this scheduling might be inefficient in some cases, + because the second conditional operation can start without waiting the delay of 100 dt. + However, such optimization should be done by another pass, + otherwise scheduling may break topological ordering of the original circuit. + + Realistic control flow scheduling respecting for microarcitecture In the dispersive QND readout scheme, qubit is measured with microwave stimulus to qubit (Q) followed by resonator ring-down (depopulation). This microwave signal is recorded From 8faf454d69ea3620176078a80dcb9c1a1301f040 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 21 Feb 2022 18:04:20 +0900 Subject: [PATCH 8/9] Update qiskit/transpiler/passes/scheduling/base_scheduler.py Co-authored-by: Toshinari Itoko <15028342+itoko@users.noreply.github.com> --- .../transpiler/passes/scheduling/base_scheduler.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index 9286444c40e5..6d2dca913124 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -34,14 +34,10 @@ class BaseScheduler(TransformationPass): .. parsed-literal:: - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├─── - └─────┬───┬──────┘ └─╥─┘ - q_1: ──────┤ X ├────────────╫───── - └─╥─┘ ║ - ┌────╨────┐ ┌────╨────┐ - c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ - └─────────┘ └─────────┘ + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, True) + qc.x(1).c_if(0, True) The scheduler SHOULD comply with above topological ordering policy of the DAG circuit. Accordingly, the `asap`-scheduled circuit will become From 9b6f658772fb498609a52fa4cd60f9a3b6c6e58c Mon Sep 17 00:00:00 2001 From: knzwnao Date: Mon, 21 Feb 2022 18:19:13 +0900 Subject: [PATCH 9/9] lint fix --- qiskit/transpiler/passes/scheduling/alap.py | 4 ++-- qiskit/transpiler/passes/scheduling/asap.py | 4 ++-- qiskit/transpiler/passes/scheduling/base_scheduler.py | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 9f395e01f6f6..86fa679b38f6 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """ALAP Scheduling.""" -from qiskit.circuit import Delay, Qubit, Measure, Gate +from qiskit.circuit import Delay, Qubit, Measure from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError @@ -60,7 +60,7 @@ def run(self, dag): # 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, self.__condition_supported): + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): t0q = max(idle_before[q] for q in node.qargs) if node.op.condition_bits: # conditional is bit tricky due to conditional_latency diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index f2d8cb5e67df..5c3be528fb8f 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """ASAP Scheduling.""" -from qiskit.circuit import Delay, Qubit, Measure, Gate +from qiskit.circuit import Delay, Qubit, Measure from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError @@ -57,7 +57,7 @@ def run(self, dag): # compute t0, t1: instruction interval, note that # t0: start time of instruction # t1: end time of instruction - if isinstance(node.op, self.__condition_supported): + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): t0q = max(idle_after[q] for q in node.qargs) if node.op.condition_bits: # conditional is bit tricky due to conditional_latency diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index 6d2dca913124..b0b5581a0d79 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -203,7 +203,8 @@ class BaseScheduler(TransformationPass): See https://arxiv.org/abs/2102.01682 for more details. """ - __condition_supported = [Gate, Delay] + + CONDITIONAL_SUPPORTED = (Gate, Delay) def __init__( self,