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!: introduce ExecutionDependency::Scheduled #186

Merged
merged 4 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 57 additions & 5 deletions src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,11 +1208,17 @@ impl Instruction {
FrameMatchCondition::AnyOfNames(frame_names),
])
}),
Instruction::Fence(Fence { qubits }) => Some(if qubits.is_empty() {
FrameMatchCondition::All
} else {
FrameMatchCondition::AnyOfQubits(Cow::Borrowed(qubits))
}),
Instruction::Fence(Fence { qubits }) => {
kalzoo marked this conversation as resolved.
Show resolved Hide resolved
if include_blocked {
Some(if qubits.is_empty() {
FrameMatchCondition::All
} else {
FrameMatchCondition::AnyOfQubits(Cow::Borrowed(qubits))
})
} else {
None
}
}
Instruction::Reset(Reset { qubit }) => {
let qubits = match qubit {
Some(qubit) => {
Expand Down Expand Up @@ -1296,6 +1302,52 @@ impl Instruction {
nom::combinator::all_consuming(parse_instruction)(&lexed).map_err(|e| e.to_string())?;
Ok(instruction)
}

/// Per the Quil-T spec, whether this instruction's timing within the pulse
/// program must be precisely controlled so as to begin exactly on the end of
/// the latest preceding timed instruction
pub(crate) fn is_scheduled(&self) -> bool {
match self {
Instruction::Capture(_)
| Instruction::Delay(_)
| Instruction::Fence(_)
| Instruction::Pulse(_)
| Instruction::RawCapture(_) => true,
Instruction::Arithmetic(_)
| Instruction::BinaryLogic(_)
| Instruction::CalibrationDefinition(_)
| Instruction::CircuitDefinition(_)
| Instruction::Convert(_)
| Instruction::Comparison(_)
| Instruction::Declaration(_)
| Instruction::Exchange(_)
| Instruction::FrameDefinition(_)
| Instruction::Gate(_)
| Instruction::GateDefinition(_)
| Instruction::Halt

Choose a reason for hiding this comment

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

A query on the spec; wouldn't a HALT have a precise timing? ie. you would know precisely when the program ends?

Copy link
Contributor Author

@kalzoo kalzoo Apr 18, 2023

Choose a reason for hiding this comment

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

You make a good point, perhaps about the docs or choice of names.

Many instructions will have timing fixed at compile time, but whether a halt is fixed to time X or time Y does not materially affect the program, if we see its pulse sequence as essential to its purpose.

Put another way, depending on the semantics of a hardware provider, the HALT may be 10 ns or 10us or 10ms after the previous instruction, but that does not affect the readout results that one would expect to collect.

That’s in contrast to a PULSE, where durations and times may be statically (if parametrically) determined from the quil alone, and which do not depend on hardware provider implementation details.

How could I better communicate that, maybe in docs but ideally in the code itself?

Choose a reason for hiding this comment

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

I think the documentation. As a user, I never write HALT; it's always implicit, and if the hardware back end chooses to add it for some tracking of the type you describe, so be it. The place I would care is Quil-T based pulse timing analysis. One of the things we can do with Quil-T entirely on the user side is mimic pulse timing rules and determine the timing of all of the instructions ourselves. Here, we report total program duration as the end time of the last pulse. But, where HALT is in play (implicitly or explicitly) and has some real time duration as you describe, that real time should be accounted for (start time of HALT was the end time of the last pulse, but then there is some real end time to HALT that is greater?). Which brings me back to the point-- wouldn't you be able to tell me precisely what that time was? If the back end adds 10 microseconds, shouldn't I be able to see that somehow? Is this was "precise" means here, and what is quil-rs's part to play in this workflow (it may be it is simply out of scope at this level).

This was a query of the semantics only, and not a practical issue, so I'm happy whatever you decide.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for that feedback, @mhodson-rigetti !

Following up from our discussion offline, I've renamed this method to is_scheduled, and ExecutionDependency::Timing to ExecutionDependency::Scheduled.

For anyone following along - "precise timing" vs scheduling is a worthwhile semantic distinction here; only some instructions are considered for "scheduling" as part of the program, and their position within that schedule is essential to the program doing what the author intended. That is now what this function identifies.

All instructions, though, may have "precise times" fixed as part of compilation for a backend. The timing of non-scheduled instructions (like ADD or MOVE) is flexible within some bounds without affecting the output of the program.

| Instruction::Include(_)
| Instruction::Jump(_)
| Instruction::JumpUnless(_)
| Instruction::JumpWhen(_)
| Instruction::Label(_)
| Instruction::Load(_)
| Instruction::MeasureCalibrationDefinition(_)
| Instruction::Measurement(_)
| Instruction::Move(_)
| Instruction::Nop
| Instruction::Pragma(_)
| Instruction::Reset(_)
| Instruction::SetFrequency(_)
| Instruction::SetPhase(_)
| Instruction::SetScale(_)
| Instruction::ShiftFrequency(_)
| Instruction::ShiftPhase(_)
| Instruction::Store(_)
| Instruction::SwapPhases(_)
| Instruction::UnaryLogic(_)
| Instruction::WaveformDefinition(_) => false,
}
}
}

#[cfg(test)]
Expand Down
56 changes: 45 additions & 11 deletions src/program/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ pub enum ExecutionDependency {
/// The downstream instruction must wait for the given operation to complete.
AwaitMemoryAccess(MemoryAccessType),

/// The instructions share a reference frame
ReferenceFrame,
/// The schedule of the downstream instruction depends on the upstream instruction.
/// Per the Quil-T specification, the downstream instruction begins execution at
/// the time that its latest upstream neighbor completes.
Scheduled,

/// The ordering between these two instructions must remain unchanged
StableOrdering,
Expand Down Expand Up @@ -217,7 +219,8 @@ impl Default for InstructionBlock {
///
/// ## Examples
///
/// Note that "depends on" is equivalent to "must execute after completion of".
/// Note that "depends on" is equivalent to "must execute at or after completion of." The interpretation of
/// "at or after" depends on the type of dependency and the compiler.
///
/// ```text
/// user --> user # a second user takes a dependency on the first
Expand All @@ -236,12 +239,14 @@ struct PreviousNodes {

impl Default for PreviousNodes {
/// The default value for [PreviousNodes] is useful in that, if no previous nodes have been recorded
/// as using a frame, we should consider that the start of the instruction block "blocks" use of that frame
/// (in other words, this instruction cannot be scheduled prior to the start of the instruction block).
/// as using a frame, we should consider that the start of the instruction block "uses" of that frame
///
/// In other words, no instruction can be scheduled prior to the start of the instruction block
/// and all scheduled instructions within the block depend on the block's start time, at least indirectly.
fn default() -> Self {
Self {
using: None,
blocking: vec![ScheduledGraphNode::BlockStart].into_iter().collect(),
using: Some(ScheduledGraphNode::BlockStart),
blocking: HashSet::new(),
}
}
}
Expand Down Expand Up @@ -299,6 +304,8 @@ impl InstructionBlock {

// Store the instruction index of the last instruction to block that frame
let mut last_instruction_by_frame: HashMap<FrameIdentifier, PreviousNodes> = HashMap::new();
let mut last_timed_instruction_by_frame: HashMap<FrameIdentifier, PreviousNodes> =
HashMap::new();

// Store memory access reads and writes. Key is memory region name.
// NOTE: this may be refined to serialize by memory region offset rather than by entire region.
Expand All @@ -309,7 +316,7 @@ impl InstructionBlock {

let instruction_role = InstructionRole::from(instruction);
match instruction_role {
// Classical instructions must be strongly ordered by appearance in the program
// Classical instructions must be ordered by appearance in the program
InstructionRole::ClassicalCompute => {
add_dependency!(graph, last_classical_instruction => node, ExecutionDependency::StableOrdering);

Expand All @@ -328,23 +335,44 @@ impl InstructionBlock {
let blocked_but_not_used_frames = blocked_frames.difference(&used_frames);

for frame in &used_frames {
if instruction.is_scheduled() {
let previous_node_ids = last_timed_instruction_by_frame
.entry((*frame).clone())
.or_default()
.get_dependencies_for_next_user(node);

for previous_node_id in previous_node_ids {
add_dependency!(graph, previous_node_id => node, ExecutionDependency::Scheduled);
}
}

let previous_node_ids = last_instruction_by_frame
.entry((*frame).clone())
.or_default()
.get_dependencies_for_next_user(node);

for previous_node_id in previous_node_ids {
add_dependency!(graph, previous_node_id => node, ExecutionDependency::ReferenceFrame);
add_dependency!(graph, previous_node_id => node, ExecutionDependency::StableOrdering);
}
}

for frame in blocked_but_not_used_frames {
if instruction.is_scheduled() {
if let Some(previous_node_id) = last_timed_instruction_by_frame
.entry((*frame).clone())
.or_default()
.get_dependency_for_next_blocker(node)
{
add_dependency!(graph, previous_node_id => node, ExecutionDependency::Scheduled);
}
}

if let Some(previous_node_id) = last_instruction_by_frame
.entry((*frame).clone())
.or_default()
.get_dependency_for_next_blocker(node)
{
add_dependency!(graph, previous_node_id => node, ExecutionDependency::ReferenceFrame);
add_dependency!(graph, previous_node_id => node, ExecutionDependency::StableOrdering);
}
}

Expand Down Expand Up @@ -390,9 +418,15 @@ impl InstructionBlock {
// does not terminate until these are complete
add_dependency!(graph, last_classical_instruction => ScheduledGraphNode::BlockEnd, ExecutionDependency::StableOrdering);

for previous_nodes in last_timed_instruction_by_frame.into_values() {
for node in previous_nodes.drain() {
add_dependency!(graph, node => ScheduledGraphNode::BlockEnd, ExecutionDependency::Scheduled);
}
}

for previous_nodes in last_instruction_by_frame.into_values() {
for node in previous_nodes.drain() {
add_dependency!(graph, node => ScheduledGraphNode::BlockEnd, ExecutionDependency::ReferenceFrame);
add_dependency!(graph, node => ScheduledGraphNode::BlockEnd, ExecutionDependency::StableOrdering);
}
}

Expand Down
19 changes: 18 additions & 1 deletion src/program/graphviz_dot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl InstructionBlock {
MemoryAccessType::Write => "await write",
MemoryAccessType::Capture => "await capture",
},
ExecutionDependency::ReferenceFrame => "frame",
ExecutionDependency::Scheduled => "timing",
ExecutionDependency::StableOrdering => "ordering",
})
.collect::<Vec<&str>>();
Expand Down Expand Up @@ -352,6 +352,15 @@ FENCE 1
"
);

build_dot_format_snapshot_test_case!(
fence_one_wrapper,
r#"
FENCE 0
NONBLOCKING PULSE 0 "rf" flat(iq: 1, duration: 4e-7)
FENCE 0
"#
);

build_dot_format_snapshot_test_case!(
jump,
"DECLARE ro BIT
Expand Down Expand Up @@ -413,6 +422,14 @@ CAPTURE 0 \"ro_rx\" test ro
PULSE 0 \"rf\" test"
);

// assert that a block "waits" for a capture to complete even with a pulse after it
build_dot_format_snapshot_test_case!(
pulse_after_set_frequency,
r#"DECLARE ro BIT
SET-FREQUENCY 0 "rf" 3e9
PULSE 0 "rf" test"#
);

// assert that a block "waits" for a capture to complete
build_dot_format_snapshot_test_case!(
parametric_pulse,
Expand Down
8 changes: 2 additions & 6 deletions src/program/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,15 +471,11 @@ DEFFRAME 0 1 \"2q\":
vec![r#"0 1 "2q""#],
),
// A Fence with qubits specified uses and blocks all frames intersecting that qubit
(
r#"FENCE 1"#,
vec![r#"1 "c""#, r#"0 1 "2q""#],
vec![r#"1 "c""#, r#"0 1 "2q""#],
),
(r#"FENCE 1"#, vec![], vec![r#"1 "c""#, r#"0 1 "2q""#]),
// Fence-all uses and blocks all frames declared in the program
(
r#"FENCE"#,
vec![r#"0 "a""#, r#"0 "b""#, r#"1 "c""#, r#"0 1 "2q""#],
vec![],
vec![r#"0 "a""#, r#"0 "b""#, r#"1 "c""#, r#"0 1 "2q""#],
),
// Delay uses and blocks frames on exactly the given qubits and with any of the given names
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ digraph {
label="measure";
node [style="filled"];
"measure_start" [shape=circle, label="start"];
"measure_start" -> "measure_0" [label="frame"];
"measure_start" -> "measure_1" [label="frame"];
"measure_start" -> "measure_0" [label="ordering
timing"];
"measure_start" -> "measure_1" [label="ordering
timing"];
"measure_start" -> "measure_end" [label="ordering"];
"measure_0" [shape=rectangle, label="[0] NONBLOCKING PULSE 0 \"ro_tx\" test(duration: 1000000)"];
"measure_0" -> "measure_end" [label="frame"];
"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="await capture
frame"];
ordering
timing"];
"measure_end" [shape=circle, label="end"];
}
"measure_end" -> "end_start" [label="if ro[0] == 0"];
Expand All @@ -25,11 +29,13 @@ frame"];
label="feedback";
node [style="filled"];
"feedback_start" [shape=circle, label="start"];
"feedback_start" -> "feedback_0" [label="frame"];
"feedback_start" -> "feedback_end" [label="frame
ordering"];
"feedback_start" -> "feedback_0" [label="ordering
timing"];
"feedback_start" -> "feedback_end" [label="ordering
timing"];
"feedback_0" [shape=rectangle, label="[0] PULSE 0 \"rf\" test(duration: 1000000)"];
"feedback_0" -> "feedback_end" [label="frame"];
"feedback_0" -> "feedback_end" [label="ordering
timing"];
"feedback_end" [shape=circle, label="end"];
}
"feedback_end" -> "measure_start" [label="always"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,27 @@ digraph {
label="block_0";
node [style="filled"];
"block_0_start" [shape=circle, label="start"];
"block_0_start" -> "block_0_0" [label="frame"];
"block_0_start" -> "block_0_1" [label="frame"];
"block_0_start" -> "block_0_2" [label="frame"];
"block_0_start" -> "block_0_end" [label="frame
ordering"];
"block_0_start" -> "block_0_0" [label="ordering
timing"];
"block_0_start" -> "block_0_1" [label="ordering
timing"];
"block_0_start" -> "block_0_2" [label="ordering
timing"];
"block_0_start" -> "block_0_end" [label="ordering
timing"];
"block_0_0" [shape=rectangle, label="[0] PULSE 0 \"rf\" test(duration: 1e-6)"];
"block_0_0" -> "block_0_2" [label="frame"];
"block_0_0" -> "block_0_end" [label="frame"];
"block_0_0" -> "block_0_2" [label="ordering
timing"];
"block_0_0" -> "block_0_end" [label="ordering
timing"];
"block_0_1" [shape=rectangle, label="[1] PULSE 1 \"rf\" test(duration: 1e-6)"];
"block_0_1" -> "block_0_2" [label="frame"];
"block_0_1" -> "block_0_end" [label="frame"];
"block_0_1" -> "block_0_2" [label="ordering
timing"];
"block_0_1" -> "block_0_end" [label="ordering
timing"];
"block_0_2" [shape=rectangle, label="[2] PULSE 0 1 \"cz\" test(duration: 1e-6)"];
"block_0_2" -> "block_0_end" [label="frame"];
"block_0_2" -> "block_0_end" [label="ordering
timing"];
"block_0_end" [shape=circle, label="end"];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,29 @@ digraph {
label="block_0";
node [style="filled"];
"block_0_start" [shape=circle, label="start"];
"block_0_start" -> "block_0_0" [label="frame"];
"block_0_start" -> "block_0_1" [label="frame"];
"block_0_start" -> "block_0_2" [label="frame"];
"block_0_start" -> "block_0_end" [label="frame
ordering"];
"block_0_start" -> "block_0_0" [label="ordering
timing"];
"block_0_start" -> "block_0_1" [label="ordering
timing"];
"block_0_start" -> "block_0_2" [label="ordering
timing"];
"block_0_start" -> "block_0_end" [label="ordering
timing"];
"block_0_0" [shape=rectangle, label="[0] NONBLOCKING PULSE 0 \"ro_tx\" test(duration: 1000000)"];
"block_0_0" -> "block_0_1" [label="frame"];
"block_0_0" -> "block_0_2" [label="frame"];
"block_0_0" -> "block_0_end" [label="frame"];
"block_0_0" -> "block_0_1" [label="ordering
timing"];
"block_0_0" -> "block_0_2" [label="ordering
timing"];
"block_0_0" -> "block_0_end" [label="ordering
timing"];
"block_0_1" [shape=rectangle, label="[1] PULSE 0 \"rf\" test(duration: 1000000)"];
"block_0_1" -> "block_0_2" [label="frame"];
"block_0_1" -> "block_0_end" [label="frame"];
"block_0_1" -> "block_0_2" [label="ordering
timing"];
"block_0_1" -> "block_0_end" [label="ordering
timing"];
"block_0_2" [shape=rectangle, label="[2] PULSE 0 \"ro_rx\" test(duration: 1000000)"];
"block_0_2" -> "block_0_end" [label="frame"];
"block_0_2" -> "block_0_end" [label="ordering
timing"];
"block_0_end" [shape=circle, label="end"];
}
}
Expand Down