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!: Move passes from algorithms into a separate crate #1100

Merged
merged 30 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
10f16f6
Create new hugr-passes crate, copying three files from hugr/src/algor…
cqc-alec May 22, 2024
7b1e4f4
Fix up files to build outside hugr crate.
cqc-alec May 22, 2024
e94d594
Include hugr-passes in default workspace members.
cqc-alec May 22, 2024
4762a82
Copy `half_node` into hugr-passes.
cqc-alec May 22, 2024
7bff248
Fix up `half_node` to build outside hugr crate.
cqc-alec May 22, 2024
880d4ab
Make `utils` public and move `sorted_consts` into it.
cqc-alec May 22, 2024
0fd7211
Inherit `extension_inference` feature from hugr crate.
cqc-alec May 23, 2024
04c4175
Make `Hugr::new()` public.
cqc-alec May 23, 2024
24f1df2
Remove `merge_bbs` from hugr crate.
cqc-alec May 23, 2024
2a1ca98
Remove `half_node` and `nest_cfgs` from hugr crate.
cqc-alec May 23, 2024
9ee77bb
Remove `algorithms::const_fold` (and hence `algorithms`) from hugr cr…
cqc-alec May 23, 2024
6971110
Move const-folding tests for int ops into hugr-passes crate.
cqc-alec May 23, 2024
6ff82e8
Fix up moved tests.
cqc-alec May 23, 2024
95146e9
Merge branch 'main' into ae/passes-crate
cqc-alec May 23, 2024
c6cddfd
Use common workspace dependencies.
cqc-alec May 23, 2024
9dab1a7
Move all test code into one file.
cqc-alec May 23, 2024
81cdc19
Rename test file.
cqc-alec May 23, 2024
ffa7d9f
Restore module documentation.
cqc-alec May 23, 2024
617e571
Improve module description.
cqc-alec May 23, 2024
beb1d4d
Add README and CHANGELOG.
cqc-alec May 23, 2024
c56a574
Add hugr-passes to release-plz config.
cqc-alec May 23, 2024
e099121
Add `git_release_name` to release-plz config.
cqc-alec May 23, 2024
f27c71f
Extend package metadata.
cqc-alec May 23, 2024
ad95736
Specify hugr version requirement for publishing.
cqc-alec May 23, 2024
e0ebb9f
Add `CHANGELOG.md` to `CODEOWNERS`.
cqc-alec May 23, 2024
1179886
Add new directory to `rust` filter.
cqc-alec May 23, 2024
be05699
Update hugr-passes/README.md
cqc-alec May 23, 2024
8105f02
Remove top-level CHANGELOG symlink and fix up link from README.
cqc-alec May 23, 2024
b1d9bd6
Merge remote-tracking branch 'origin/ae/passes-crate' into ae/passes-…
cqc-alec May 23, 2024
8840073
Get package.edition from workspace.
cqc-alec May 23, 2024
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
# The release PRs that trigger publication to crates.io or PyPI always modify the changelog.
# We require those PRs to be approved by someone with release permissions.
hugr/CHANGELOG.md @aborgna-q @ss2165
hugr-passes/CHANGELOG.md @aborgna-q @ss2165
hugr-py/CHANGELOG.md @aborgna-q @ss2165
1 change: 1 addition & 0 deletions .github/change-filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

rust:
- "hugr/**"
- "hugr-passes/**"
- "Cargo.toml"
- "specification/schema/**"

Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ lto = "thin"

[workspace]
resolver = "2"
members = ["hugr"]
default-members = ["hugr"]
members = ["hugr", "hugr-passes"]
default-members = ["hugr", "hugr-passes"]

[workspace.package]
rust-version = "1.75"
Expand Down
5 changes: 5 additions & 0 deletions hugr-passes/CHANGELOG.md
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.
26 changes: 26 additions & 0 deletions hugr-passes/Cargo.toml
aborgna-q marked this conversation as resolved.
Show resolved Hide resolved
aborgna-q marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "hugr-passes"
version = "0.1.0"
edition = "2021"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
version = "0.1.0"
edition = "2021"
version = "0.4.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"]

Let's use the same major/minor version for all packages, to avoid confusion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In that case I can add version under [workspace.package] in the top-level Cargo.toml and use {workspace = true } in both packages?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do we want to have to make a new release of hugr whenever we do a new release of hugr-passes? This shouldn't be necessary.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I was thinking about having a matching minor number (0.x), even if the patch version varies.
But it may get desynchronised too, so I guess it's fine to leave it as 0.1.0.

The releases are done synchronously, but only for packages with changes.
See this PR for an example on how this looks.

aborgna-q marked this conversation as resolved.
Show resolved Hide resolved
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"
58 changes: 58 additions & 0 deletions hugr-passes/README.md
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/
[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/CHANGELOG.md
cqc-alec marked this conversation as resolved.
Show resolved Hide resolved
230 changes: 230 additions & 0 deletions hugr-passes/src/const_fold.rs
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;
Loading