-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: Move passes from
algorithms
into a separate crate (#1100)
Closes #1000 . Also: - Moved some tests that were making use of these passes (or their test code) from the `hugr` crate into the `hugr-passes` crate. - Moved some functions from test code into the `utils` module and made it public. - Made `Hugr::new()` public. I thought I could avoid this by using the `CFGBuilder` but doing so caused some extension-inference tests to fail, for reasons I couldn't fathom. Do we want to update the `release-plz` configuration? Not sure exactly what to do here. BREAKING CHANGE: The `algorithms` module is removed from `hugr`. It's functions are now in a separate `hugr-passes` crate. --------- Co-authored-by: Agustín Borgna <[email protected]>
- Loading branch information
Showing
25 changed files
with
780 additions
and
666 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
rust: | ||
- "hugr/**" | ||
- "hugr-passes/**" | ||
- "Cargo.toml" | ||
- "specification/schema/**" | ||
|
||
|
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Changelog | ||
|
||
## 0.1.0 (2024-05-23) | ||
|
||
Initial release, with functions ported from the `hugr::algorithms` module. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
[package] | ||
name = "hugr-passes" | ||
version = "0.1.0" | ||
edition = { workspace = true } | ||
rust-version = { workspace = true } | ||
license = { workspace = true } | ||
readme = "README.md" | ||
documentation = "https://docs.rs/hugr-passes/" | ||
homepage = { workspace = true } | ||
repository = { workspace = true } | ||
description = "Compiler passes for Quantinuum's HUGR" | ||
keywords = ["Quantum", "Quantinuum"] | ||
categories = ["compilers"] | ||
|
||
[dependencies] | ||
hugr = { path = "../hugr", version = "0.4.0" } | ||
itertools = { workspace = true } | ||
lazy_static = { workspace = true } | ||
paste = { workspace = true } | ||
thiserror = { workspace = true } | ||
|
||
[features] | ||
extension_inference = ["hugr/extension_inference"] | ||
|
||
[dev-dependencies] | ||
rstest = "0.19.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
![](/hugr/assets/hugr_logo.svg) | ||
|
||
hugr-passes | ||
=============== | ||
|
||
[![build_status][]](https://github.com/CQCL/hugr/actions) | ||
[![crates][]](https://crates.io/crates/hugr-passes) | ||
[![msrv][]](https://github.com/CQCL/hugr) | ||
[![codecov][]](https://codecov.io/gh/CQCL/hugr) | ||
|
||
The Hierarchical Unified Graph Representation (HUGR, pronounced _hugger_) is the | ||
common representation of quantum circuits and operations in the Quantinuum | ||
ecosystem. | ||
|
||
It provides a high-fidelity representation of operations, that facilitates | ||
compilation and encodes runnable programs. | ||
|
||
The HUGR specification is [here](https://github.com/CQCL/hugr/blob/main/specification/hugr.md). | ||
|
||
This crate provides compilation passes that act on HUGR programs. | ||
|
||
## Usage | ||
|
||
Add the dependency to your project: | ||
|
||
```bash | ||
cargo add hugr-passes | ||
``` | ||
|
||
Please read the [API documentation here][]. | ||
|
||
## Experimental Features | ||
|
||
- `extension_inference`: | ||
Experimental feature which allows automatic inference of extension usages and | ||
requirements in a HUGR and validation that extensions are correctly specified. | ||
Not enabled by default. | ||
|
||
## Recent Changes | ||
|
||
See [CHANGELOG][] for a list of changes. The minimum supported rust | ||
version will only change on major releases. | ||
|
||
## Development | ||
|
||
See [DEVELOPMENT.md](https://github.com/CQCL/hugr/blob/main/DEVELOPMENT.md) for instructions on setting up the development environment. | ||
|
||
## License | ||
|
||
This project is licensed under Apache License, Version 2.0 ([LICENSE][] or http://www.apache.org/licenses/LICENSE-2.0). | ||
|
||
[API documentation here]: https://docs.rs/hugr-passes/ | ||
[build_status]: https://github.com/CQCL/hugr/actions/workflows/ci-rs.yml/badge.svg?branch=main | ||
[msrv]: https://img.shields.io/badge/rust-1.75.0%2B-blue.svg | ||
[crates]: https://img.shields.io/crates/v/hugr-passes | ||
[codecov]: https://img.shields.io/codecov/c/gh/CQCL/hugr?logo=codecov | ||
[LICENSE]: https://github.com/CQCL/hugr/blob/main/LICENCE | ||
[CHANGELOG]: https://github.com/CQCL/hugr/blob/main/hugr-passes/CHANGELOG.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
//! Constant folding routines. | ||
use std::collections::{BTreeSet, HashMap}; | ||
|
||
use itertools::Itertools; | ||
use thiserror::Error; | ||
|
||
use hugr::hugr::{SimpleReplacementError, ValidationError}; | ||
use hugr::types::SumType; | ||
use hugr::Direction; | ||
use hugr::{ | ||
builder::{DFGBuilder, Dataflow, DataflowHugr}, | ||
extension::{ConstFoldResult, ExtensionRegistry}, | ||
hugr::{ | ||
hugrmut::HugrMut, | ||
rewrite::consts::{RemoveConst, RemoveLoadConstant}, | ||
views::SiblingSubgraph, | ||
}, | ||
ops::{OpType, Value}, | ||
type_row, | ||
types::FunctionType, | ||
utils::sorted_consts, | ||
Hugr, HugrView, IncomingPort, Node, SimpleReplacement, | ||
}; | ||
|
||
#[derive(Error, Debug)] | ||
#[allow(missing_docs)] | ||
pub enum ConstFoldError { | ||
#[error("Failed to verify {label} HUGR: {err}")] | ||
VerifyError { | ||
label: String, | ||
#[source] | ||
err: ValidationError, | ||
}, | ||
#[error(transparent)] | ||
SimpleReplaceError(#[from] SimpleReplacementError), | ||
} | ||
|
||
/// Tag some output constants with [`OutgoingPort`] inferred from the ordering. | ||
fn out_row(consts: impl IntoIterator<Item = Value>) -> ConstFoldResult { | ||
let vec = consts | ||
.into_iter() | ||
.enumerate() | ||
.map(|(i, c)| (i.into(), c)) | ||
.collect(); | ||
Some(vec) | ||
} | ||
|
||
/// For a given op and consts, attempt to evaluate the op. | ||
pub fn fold_leaf_op(op: &OpType, consts: &[(IncomingPort, Value)]) -> ConstFoldResult { | ||
let fold_result = match op { | ||
OpType::Noop { .. } => out_row([consts.first()?.1.clone()]), | ||
OpType::MakeTuple { .. } => { | ||
out_row([Value::tuple(sorted_consts(consts).into_iter().cloned())]) | ||
} | ||
OpType::UnpackTuple { .. } => { | ||
let c = &consts.first()?.1; | ||
let Value::Tuple { vs } = c else { | ||
panic!("This op always takes a Tuple input."); | ||
}; | ||
out_row(vs.iter().cloned()) | ||
} | ||
|
||
OpType::Tag(t) => out_row([Value::sum( | ||
t.tag, | ||
consts.iter().map(|(_, konst)| konst.clone()), | ||
SumType::new(t.variants.clone()), | ||
) | ||
.unwrap()]), | ||
OpType::CustomOp(op) => { | ||
let ext_op = op.as_extension_op()?; | ||
ext_op.constant_fold(consts) | ||
} | ||
_ => None, | ||
}; | ||
debug_assert!(fold_result.as_ref().map_or(true, |x| x.len() | ||
== op.value_port_count(Direction::Outgoing))); | ||
fold_result | ||
} | ||
|
||
/// Generate a graph that loads and outputs `consts` in order, validating | ||
/// against `reg`. | ||
fn const_graph(consts: Vec<Value>, reg: &ExtensionRegistry) -> Hugr { | ||
let const_types = consts.iter().map(Value::get_type).collect_vec(); | ||
let mut b = DFGBuilder::new(FunctionType::new(type_row![], const_types)).unwrap(); | ||
|
||
let outputs = consts | ||
.into_iter() | ||
.map(|c| b.add_load_const(c)) | ||
.collect_vec(); | ||
|
||
b.finish_hugr_with_outputs(outputs, reg).unwrap() | ||
} | ||
|
||
/// Given some `candidate_nodes` to search for LoadConstant operations in `hugr`, | ||
/// return an iterator of possible constant folding rewrites. The | ||
/// [`SimpleReplacement`] replaces an operation with constants that result from | ||
/// evaluating it, the extension registry `reg` is used to validate the | ||
/// replacement HUGR. The vector of [`RemoveLoadConstant`] refer to the | ||
/// LoadConstant nodes that could be removed - they are not automatically | ||
/// removed as they may be used by other operations. | ||
pub fn find_consts<'a, 'r: 'a>( | ||
hugr: &'a impl HugrView, | ||
candidate_nodes: impl IntoIterator<Item = Node> + 'a, | ||
reg: &'r ExtensionRegistry, | ||
) -> impl Iterator<Item = (SimpleReplacement, Vec<RemoveLoadConstant>)> + 'a { | ||
// track nodes for operations that have already been considered for folding | ||
let mut used_neighbours = BTreeSet::new(); | ||
|
||
candidate_nodes | ||
.into_iter() | ||
.filter_map(move |n| { | ||
// only look at LoadConstant | ||
hugr.get_optype(n).is_load_constant().then_some(())?; | ||
|
||
let (out_p, _) = hugr.out_value_types(n).exactly_one().ok()?; | ||
let neighbours = hugr | ||
.linked_inputs(n, out_p) | ||
.filter(|(n, _)| used_neighbours.insert(*n)) | ||
.collect_vec(); | ||
if neighbours.is_empty() { | ||
// no uses of LoadConstant that haven't already been considered. | ||
return None; | ||
} | ||
let fold_iter = neighbours | ||
.into_iter() | ||
.filter_map(|(neighbour, _)| fold_op(hugr, neighbour, reg)); | ||
Some(fold_iter) | ||
}) | ||
.flatten() | ||
} | ||
|
||
/// Attempt to evaluate and generate rewrites for the operation at `op_node` | ||
fn fold_op( | ||
hugr: &impl HugrView, | ||
op_node: Node, | ||
reg: &ExtensionRegistry, | ||
) -> Option<(SimpleReplacement, Vec<RemoveLoadConstant>)> { | ||
// only support leaf folding for now. | ||
let neighbour_op = hugr.get_optype(op_node); | ||
let (in_consts, removals): (Vec<_>, Vec<_>) = hugr | ||
.node_inputs(op_node) | ||
.filter_map(|in_p| { | ||
let (con_op, load_n) = get_const(hugr, op_node, in_p)?; | ||
Some(((in_p, con_op), RemoveLoadConstant(load_n))) | ||
}) | ||
.unzip(); | ||
// attempt to evaluate op | ||
let (nu_out, consts): (HashMap<_, _>, Vec<_>) = fold_leaf_op(neighbour_op, &in_consts)? | ||
.into_iter() | ||
.enumerate() | ||
.filter_map(|(i, (op_out, konst))| { | ||
// for each used port of the op give the nu_out entry and the | ||
// corresponding Value | ||
hugr.single_linked_input(op_node, op_out) | ||
.map(|np| ((np, i.into()), konst)) | ||
}) | ||
.unzip(); | ||
let replacement = const_graph(consts, reg); | ||
let sibling_graph = SiblingSubgraph::try_from_nodes([op_node], hugr) | ||
.expect("Operation should form valid subgraph."); | ||
|
||
let simple_replace = SimpleReplacement::new( | ||
sibling_graph, | ||
replacement, | ||
// no inputs to replacement | ||
HashMap::new(), | ||
nu_out, | ||
); | ||
Some((simple_replace, removals)) | ||
} | ||
|
||
/// If `op_node` is connected to a LoadConstant at `in_p`, return the constant | ||
/// and the LoadConstant node | ||
fn get_const(hugr: &impl HugrView, op_node: Node, in_p: IncomingPort) -> Option<(Value, Node)> { | ||
let (load_n, _) = hugr.single_linked_output(op_node, in_p)?; | ||
let load_op = hugr.get_optype(load_n).as_load_constant()?; | ||
let const_node = hugr | ||
.single_linked_output(load_n, load_op.constant_port())? | ||
.0; | ||
let const_op = hugr.get_optype(const_node).as_const()?; | ||
|
||
// TODO avoid const clone here | ||
Some((const_op.as_ref().clone(), load_n)) | ||
} | ||
|
||
/// Exhaustively apply constant folding to a HUGR. | ||
pub fn constant_fold_pass<H: HugrMut>(h: &mut H, reg: &ExtensionRegistry) { | ||
#[cfg(test)] | ||
let verify = |label, h: &H| { | ||
h.validate_no_extensions(reg).unwrap_or_else(|err| { | ||
panic!( | ||
"constant_fold_pass: failed to verify {label} HUGR: {err}\n{}", | ||
h.mermaid_string() | ||
) | ||
}) | ||
}; | ||
#[cfg(test)] | ||
verify("input", h); | ||
loop { | ||
// We can only safely apply a single replacement. Applying a | ||
// replacement removes nodes and edges which may be referenced by | ||
// further replacements returned by find_consts. Even worse, if we | ||
// attempted to apply those replacements, expecting them to fail if | ||
// the nodes and edges they reference had been deleted, they may | ||
// succeed because new nodes and edges reused the ids. | ||
// | ||
// We could be a lot smarter here, keeping track of `LoadConstant` | ||
// nodes and only looking at their out neighbours. | ||
let Some((replace, removes)) = find_consts(h, h.nodes(), reg).next() else { | ||
break; | ||
}; | ||
h.apply_rewrite(replace).unwrap(); | ||
for rem in removes { | ||
// We are optimistically applying these [RemoveLoadConstant] and | ||
// [RemoveConst] rewrites without checking whether the nodes | ||
// they attempt to remove have remaining uses. If they do, then | ||
// the rewrite fails and we move on. | ||
if let Ok(const_node) = h.apply_rewrite(rem) { | ||
// if the LoadConst was removed, try removing the Const too. | ||
let _ = h.apply_rewrite(RemoveConst(const_node)); | ||
} | ||
} | ||
} | ||
#[cfg(test)] | ||
verify("output", h); | ||
} | ||
|
||
#[cfg(test)] | ||
mod test; |
Oops, something went wrong.