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

feat!: track memory dependencies involving the block terminator #433

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 68 additions & 25 deletions quil-rs/src/program/scheduling/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,23 @@ pub enum ScheduleErrorVariant {
Extern,
UncalibratedInstruction,
UnresolvedCallInstruction,
ControlFlowNotBlockTerminator,
UnschedulableInstruction,
}

#[derive(Debug, Clone, thiserror::Error)]
#[error("Error scheduling instruction {}: {}: {variant:?}", .instruction_index.map(|i| i.to_string()).unwrap_or(String::from("")), .instruction.to_quil_or_debug())]
#[error(
"Error scheduling {}: {}: {variant:?}",
match .instruction_node {
None => "an instruction".to_string(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if we should just implement Display here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe? I'm always a bit reluctant to implement Display for fragments like this that wouldn't make sense in every sentence

Some(ScheduledGraphNode::BlockStart) => "the start of the block".to_string(),
Some(ScheduledGraphNode::InstructionIndex(index)) => format!("instruction {index}"),
Some(ScheduledGraphNode::BlockEnd) => "the block terminator instruction".to_string(),
},
.instruction.to_quil_or_debug()
)]
pub struct ScheduleError {
pub instruction_index: Option<usize>,
pub instruction_node: Option<ScheduledGraphNode>,
pub instruction: Instruction,
pub variant: ScheduleErrorVariant,
}
Expand Down Expand Up @@ -306,17 +316,28 @@ impl<'a> ScheduledBasicBlock<'a> {

let extern_signature_map = ExternSignatureMap::try_from(program.extern_pragma_map.clone())
.map_err(|(pragma, _)| ScheduleError {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wouldn't hold you to this, but I probably should have made ScheduleError into an enum here; this variant would include the pragma rather than a None for instruction_node. That involves making downstream accommodations.

instruction_index: None,
instruction_node: None,
instruction: Instruction::Pragma(pragma),
variant: ScheduleErrorVariant::Extern,
})?;
for (index, &instruction) in basic_block.instructions().iter().enumerate() {
let node = graph.add_node(ScheduledGraphNode::InstructionIndex(index));

let terminator = basic_block.terminator().clone().into_instruction();
let terminator_ref = terminator.as_ref();

let instructions_iter = basic_block
.instructions()
.iter()
.enumerate()
.map(|(index, instr)| (ScheduledGraphNode::InstructionIndex(index), *instr))
.chain(terminator_ref.map(|instr| (ScheduledGraphNode::BlockEnd, instr)));

for (node, instruction) in instructions_iter {
graph.add_node(node);

let accesses = custom_handler
.memory_accesses(instruction, &extern_signature_map)
.map_err(|_| ScheduleError {
instruction_index: Some(index),
instruction_node: Some(node),
instruction: instruction.clone(),
variant: ScheduleErrorVariant::UnresolvedCallInstruction,
})?;
Expand Down Expand Up @@ -417,13 +438,19 @@ impl<'a> ScheduledBasicBlock<'a> {

Ok(())
}
InstructionRole::ControlFlow => Err(ScheduleError {
instruction_index: Some(index),
instruction: instruction.clone(),
variant: ScheduleErrorVariant::UnschedulableInstruction,
}),
InstructionRole::ControlFlow => {
if let ScheduledGraphNode::BlockEnd = node {
Ok(())
} else {
Err(ScheduleError {
instruction_node: Some(node),
instruction: instruction.clone(),
variant: ScheduleErrorVariant::ControlFlowNotBlockTerminator,
})
}
}
InstructionRole::ProgramComposition => Err(ScheduleError {
instruction_index: Some(index),
instruction_node: Some(node),
instruction: instruction.clone(),
variant: ScheduleErrorVariant::UnschedulableInstruction,
}),
Expand Down Expand Up @@ -547,13 +574,12 @@ impl From<ScheduledBasicBlock<'_>> for ScheduledBasicBlockOwned {
}
}

#[cfg(test)]
mod tests {
#[cfg(all(test, feature = "graphviz-dot"))]
mod graphviz_dot_tests {
use super::*;
#[cfg(feature = "graphviz-dot")]

use crate::program::scheduling::graphviz_dot::tests::build_dot_format_snapshot_test_case;

#[cfg(feature = "graphviz-dot")]
mod custom_handler {
use super::*;

Expand Down Expand Up @@ -700,7 +726,6 @@ PRAGMA RAW-INSTRUCTION foo

// Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
// we expect an edge from the first load to the second (0 -> 1).
#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
classical_write_read_load_load,
r#"
Expand All @@ -715,7 +740,6 @@ LOAD params1[0] params2 integers[0] # reads params2

// Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
// we expect an edge from the mul to the load (0 -> 1).
#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
classical_write_read_mul_load,
r#"
Expand All @@ -730,7 +754,6 @@ LOAD params1[0] params2 integers[0] # just reads params2

// Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
// we expect an edge from the mul to the add (0 -> 1).
#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
classical_write_read_add_mul,
r#"
Expand All @@ -745,7 +768,6 @@ MUL params1[0] 2 # this reads and writes params1

// Because any instruction that reads a particular region must precede any later instructions that write to/ capture that memory region,
// we expect an edge from the first load to the second (0, 1).
#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
classical_read_write_load_load,
r#"
Expand All @@ -760,7 +782,6 @@ LOAD params2[0] params3 integers[0] # writes params2

// Because any instruction that reads a particular region must precede any later instructions that write to/ capture that memory region,
// we expect an edge from the load to the mul (0, 1).
#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
classical_read_write_load_mul,
r#"
Expand All @@ -776,7 +797,6 @@ MUL params2[0] 2 # reads and writes params2
// Because memory reading and writing dependencies also apply to RfControl instructions, we
// expect edges from the first load to the first shift-phase (0 -> 1), the first shift-phase
// to the second load (1 -> 2), and the second load to the second shift-phase (2 -> 3).
#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
quantum_write_parameterized_operations,
r#"
Expand All @@ -797,13 +817,11 @@ SHIFT-PHASE 1 "rf" params2[0] # reads params2
}

// Because a pragma by default will have no memory accesses, it should only have edges from the block start and to the block end.
#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
classical_no_memory_pragma,
r#"PRAGMA example"#
}

#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
write_capture_read,
r#"
Expand All @@ -815,7 +833,6 @@ LOAD bits3[0] bits integers[0] # read
"#
}

#[cfg(feature = "graphviz-dot")]
build_dot_format_snapshot_test_case! {
write_write_read,
r#"
Expand All @@ -826,6 +843,32 @@ DECLARE integers INTEGER[1]
LOAD bits[0] bits2 integers[0] # write
LOAD bits[0] bits3 integers[0] # write
LOAD bits4[0] bits integers[0] # read
"#
}

build_dot_format_snapshot_test_case! {
memory_dependency_not_in_block_terminator,
r#"
DECLARE ro BIT
DECLARE depends_on_ro BIT
NONBLOCKING CAPTURE 0 "ro_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) ro
MOVE depends_on_ro ro
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens here if depends_on_ro is used in the next block? Is that supported? I would make a test case for that, regardless as to whether it's supported. If not supported, a failing test case, issue, and some documentation on the limitation would be nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We only track dependencies within blocks because there's already a total ordering constraint between blocks; we don't do inter-block scheduling. (This is, AFAIK, already documented.) So it's not supported but because it's not really relevant. A test case is a fine idea, but I don't think it will fail and don't think there's an issue to create.

JUMP @eq
LABEL @eq
PULSE 0 "ro_tx" gaussian(duration: 1, fwhm: 2, t0: 3)
"#
}

build_dot_format_snapshot_test_case! {
memory_dependency_in_block_terminator,
r#"
DECLARE ro BIT
NONBLOCKING CAPTURE 0 "ro_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) ro
JUMP-WHEN @eq ro
LABEL @eq
PULSE 0 "ro_tx" gaussian(duration: 1, fwhm: 2, t0: 3)
"#
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
source: quil-rs/src/program/scheduling/graph.rs
expression: dot_format
---
digraph {
entry -> "block_0_start";
entry [label="Entry Point"];
subgraph cluster_0 {
label="block_0";
node [style="filled"];
"block_0_start" [shape=circle, label="start"];
"block_0_start" -> "block_0_0" [label="ordering
timing"];
"block_0_0" [shape=rectangle, label="[0] NONBLOCKING CAPTURE 0 \"ro_rx\" flat(detuning: 0, duration: 2.0000000000000003e-6, iq: 1, phase: 0.8745492960861506, scale: 1) ro[0]"];
"block_0_0" -> "block_0_end" [label="await capture
ordering
timing"];
"block_0_end" [shape=circle, label="end"];
}
"block_0_end" -> "@eq_start" [label="if ro[0] != 0"];
"block_0_end" -> "@eq_start" [label="if ro[0] == 0"];
subgraph cluster_1 {
label="@eq";
node [style="filled"];
"@eq_start" [shape=circle, label="start"];
"@eq_start" -> "@eq_0" [label="ordering
timing"];
"@eq_start" -> "@eq_end" [label="ordering
timing"];
"@eq_0" [shape=rectangle, label="[0] PULSE 0 \"ro_tx\" gaussian(duration: 1, fwhm: 2, t0: 3)"];
"@eq_0" -> "@eq_end" [label="ordering
timing"];
"@eq_end" [shape=circle, label="end"];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
source: quil-rs/src/program/scheduling/graph.rs
expression: dot_format
---
digraph {
entry -> "block_0_start";
entry [label="Entry Point"];
subgraph cluster_0 {
label="block_0";
node [style="filled"];
"block_0_start" [shape=circle, label="start"];
"block_0_start" -> "block_0_0" [label="ordering
timing"];
"block_0_0" [shape=rectangle, label="[0] NONBLOCKING CAPTURE 0 \"ro_rx\" flat(detuning: 0, duration: 2.0000000000000003e-6, iq: 1, phase: 0.8745492960861506, scale: 1) ro[0]"];
"block_0_0" -> "block_0_1" [label="await capture"];
"block_0_0" -> "block_0_end" [label="ordering
timing"];
"block_0_1" [shape=rectangle, label="[1] MOVE depends_on_ro[0] ro[0]"];
"block_0_1" -> "block_0_end" [label="ordering"];
"block_0_end" [shape=circle, label="end"];
}
"block_0_end" -> "@eq_start" [label="always"];
subgraph cluster_1 {
label="@eq";
node [style="filled"];
"@eq_start" [shape=circle, label="start"];
"@eq_start" -> "@eq_0" [label="ordering
timing"];
"@eq_start" -> "@eq_end" [label="ordering
timing"];
"@eq_0" [shape=rectangle, label="[0] PULSE 0 \"ro_tx\" gaussian(duration: 1, fwhm: 2, t0: 3)"];
"@eq_0" -> "@eq_end" [label="ordering
timing"];
"@eq_end" [shape=circle, label="end"];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ timing"];
"@measure_0" -> "@measure_end" [label="ordering
timing"];
"@measure_1" [shape=rectangle, label="[1] NONBLOCKING CAPTURE 0 \"ro_rx\" test(duration: 1000000) ro[0]"];
"@measure_1" -> "@measure_end" [label="ordering
"@measure_1" -> "@measure_end" [label="await capture
ordering
timing"];
"@measure_end" [shape=circle, label="end"];
}
Expand Down
Loading