Skip to content

Commit

Permalink
feat!: Move passes from algorithms into a separate crate (#1100)
Browse files Browse the repository at this point in the history
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
cqc-alec and aborgna-q authored May 23, 2024
1 parent 4b8d23d commit 3e17193
Show file tree
Hide file tree
Showing 25 changed files with 780 additions and 666 deletions.
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
1 change: 0 additions & 1 deletion CHANGELOG.md

This file was deleted.

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
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"
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-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
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

0 comments on commit 3e17193

Please sign in to comment.