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

Port linear_depth_lnn to rust #13654

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

gadial
Copy link
Contributor

@gadial gadial commented Jan 13, 2025

Summary

Ports the optimize_cx_circ_depth_5n_line method to Rust.

Closes #12228

Details and comments

This is synthesis pass for constructing general linear reversible circuits given by an invertible boolean matrix. The code generating the instruction list was ported to Rust in a straightforward manner; construction of the circuit itself is done in Python.

A quick benchmarking was performed by using random_invertible_binary_matrix(n, seed=seed) and timing the application of synth_cnot_depth_line_kms on the result, showing consistent improvement for the rust version.

╒═════════════════════╤═════════════╤═════════════╤═══════════════════════╕
│ Test Case           │        rust │      python │   Ratio (python/rust) │
╞═════════════════════╪═════════════╪═════════════╪═══════════════════════╡
│ 5x5 (seed 42)       │ 0.000237703 │ 0.000352144 │               1.48144 │
├─────────────────────┼─────────────┼─────────────┼───────────────────────┤
│ 10x10 (seed 42)     │ 0.000721216 │ 0.00221419  │               3.07008 │
├─────────────────────┼─────────────┼─────────────┼───────────────────────┤
│ 20x20 (seed 55)     │ 0.00298257  │ 0.00747766  │               2.50712 │
├─────────────────────┼─────────────┼─────────────┼───────────────────────┤
│ 50x50 (seed 13)     │ 0.0209641   │ 0.0665068   │               3.17242 │
├─────────────────────┼─────────────┼─────────────┼───────────────────────┤
│ 60x60 (seed 55)     │ 0.0432329   │ 0.119858    │               2.77237 │
├─────────────────────┼─────────────┼─────────────┼───────────────────────┤
│ 70x70 (seed 13)     │ 0.0471578   │ 0.168697    │               3.57728 │
├─────────────────────┼─────────────┼─────────────┼───────────────────────┤
│ 100x100 (seed 1089) │ 0.103011    │ 0.439383    │               4.26539 │
├─────────────────────┼─────────────┼─────────────┼───────────────────────┤
│ 120x120 (seed 17)   │ 0.175328    │ 0.675542    │               3.85302 │
├─────────────────────┼─────────────┼─────────────┼───────────────────────┤
│ 150x150 (seed 99)   │ 0.287093    │ 1.47674     │               5.14378 │
╘═════════════════════╧═════════════╧═════════════╧═══════════════════════╛

@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@coveralls
Copy link

coveralls commented Jan 13, 2025

Pull Request Test Coverage Report for Build 12747661672

Details

  • 238 of 240 (99.17%) changed or added relevant lines in 4 files are covered.
  • 27 unchanged lines in 4 files lost coverage.
  • Overall coverage decreased (-0.01%) to 88.92%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/accelerate/src/synthesis/linear/lnn.rs 232 234 99.15%
Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/unitary_synthesis.rs 1 93.18%
crates/qasm2/src/lex.rs 4 92.23%
crates/accelerate/src/synthesis/linear/mod.rs 10 82.8%
crates/qasm2/src/parse.rs 12 97.15%
Totals Coverage Status
Change from base Build 12744574228: -0.01%
Covered Lines: 79545
Relevant Lines: 89457

💛 - Coveralls

@ShellyGarion ShellyGarion added synthesis Rust This PR or issue is related to Rust code in the repository labels Jan 13, 2025
Copy link
Member

@ShellyGarion ShellyGarion left a comment

Choose a reason for hiding this comment

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

Thanks @gadial for your contribution! I only have some minor comments and questions.

}

/// Get the instructions for a lower triangular basis change of a matrix mat.
/// See the proof of Proposition 7.3 in [1].
Copy link
Member

Choose a reason for hiding this comment

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

there are extra spaces in the docstring

}

/// Transform a north-west triangular matrix to identity in depth 3*n by Proposition 7.4 of [1]
fn _north_west_to_identity(n: usize, mut mat: ArrayViewMut2<bool>) -> InstructionList {
Copy link
Member

Choose a reason for hiding this comment

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

extra spaces in docstring

Some(result)
}
/// Perform ROW operation on a matrix mat
fn _row_op(mat: ArrayViewMut2<bool>, ctrl: usize, trgt: usize) {
Copy link
Member

Choose a reason for hiding this comment

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

we already have the function row_op here:

fn row_op(mut mat: PyReadwriteArray2<bool>, ctrl: usize, trgt: usize) {

if you need a non pyo3 function, please also add it to the util file.

Copy link
Contributor Author

@gadial gadial Jan 15, 2025

Choose a reason for hiding this comment

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

I think my main question here is - should I use pyo3 function? It seems like adding overhead where it's not needed (but sure, I'll move to the utils file).

Copy link
Member

Choose a reason for hiding this comment

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

you don't have to use the pyo3 function, but please add your new function to the util file, and see that the naming conventions are consistent (there are many functions there that have both rust and pyfunctions)

}

/// Perform COL operation on a matrix mat (in the inverse direction)
fn _col_op(mat: ArrayViewMut2<bool>, ctrl: usize, trgt: usize) {
Copy link
Member

Choose a reason for hiding this comment

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

we already have the function col_op here:

fn col_op(mut mat: PyReadwriteArray2<bool>, ctrl: usize, trgt: usize) {

if you need a non pyo3 function, please also add it to the util file.

/// `arXiv:quant-ph/0701194 <https://arxiv.org/abs/quant-ph/0701194>`_.

type InstructionList = Vec<(usize, usize)>;
fn _row_sum(row_1: ArrayView1<bool>, row_2: ArrayView1<bool>) -> Option<Array1<bool>> {
Copy link
Member

Choose a reason for hiding this comment

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

I think this function should go into the util file:
https://github.com/Qiskit/qiskit/blob/main/crates/accelerate/src/synthesis/linear/utils.rs

could you write a docstring explaining what this function does?

Copy link
Contributor

Choose a reason for hiding this comment

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

A small question: In the case that the two rows have different lengths, would it make sense to return an Error rather than None (that is, returning a Result rather than Option)?

let mut mat = mat.to_owned();
let mut mat_t = mat.to_owned();
let mut mat_inv_t = mat_inv.to_owned();

Copy link
Member

Choose a reason for hiding this comment

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

are the to_owned needed ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, we aggressively call copy in the Python code, but it might be a good time to revisit if this excessive copying is really required.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have also thought about it. On the surface of it we need all those copies, but then again - maybe not.

mat_inv, in this stage, is a clone of the original matrix passed to the algorithm (it's cloned in line 305, in optimize_cx_circ_depth_5n_line). We pass it to _matrix_to_north_west and don't use it again in _north_west_to_identity. We also don't use it inside _matrix_to_north_west other than as the input to _get_lower_triangular so it is possible to have _get_lower_triangular altering it instead of creating a copy (this is less "clean", code-wise, so I didn't rush to do it).

As to the the clonings in 65 and 66 - I don't see a way around it. mat is changed in _matrix_to_north_west and used again later so it must be cloned prior to the changes done in _get_lower_triangular. Inside _get_lower_triangular itself, we modify mat to generate the instruction list, and then need to perform the row operations on mat_t in order to obtain the label array; this cannot be done on the original mat or the one after the changes.

// Use the instructions in U, which contains only gates of the form cx(a,b) a>b
// to transform the matrix to a permuted lower-triangular matrix.
// The original Matrix is unchanged.
for i in (0..n).rev() {
Copy link
Member

Choose a reason for hiding this comment

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

can we replace some for loops by iterators? (I don't know how, just asking...)

Copy link
Contributor

Choose a reason for hiding this comment

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

We definitely can, but I think this is a good tradeoff between code clarity and code "rustiness". 😄

Copy link
Member

Choose a reason for hiding this comment

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

it's worth to do it for better performance, for the clarity one can add a comment.

Copy link
Contributor Author

@gadial gadial Jan 16, 2025

Choose a reason for hiding this comment

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

I actually think the iterator-based version can be easier to understand. compare

(0..i)
.rev()
.filter(|k| mat[[k, first_j]])
.for_each(|k| {
    _row_op_update_instructions(&mut cx_instructions_rows, mat.view_mut(), i, k);
});

With

for k in (0..i).rev() {
   if mat[[k, first_j]] {
       _row_op_update_instructions(&mut cx_instructions_rows, mat.view_mut(), i, k)
   }

While the latter is more concise, it's less explicit in describing what it actually does.

/// For each row in mat_t, save the column index of the last "1"
fn _get_label_arr(n: usize, mat_t: ArrayView2<bool>) -> Vec<usize> {
let mut label_arr: Vec<usize> = Vec::new();
for i in 0..n {
Copy link
Member

Choose a reason for hiding this comment

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

can we replace some for loops by iterators? (I don't know how, just asking...)

Copy link
Contributor

Choose a reason for hiding this comment

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

just for fun, you could probably do something like this (I am not sure this compiles or returns the correct result):

fn _get_label_arr(n: usize, mat_t: ArrayView2<bool>) -> Vec<usize> {
    mat
      .rows()
      .into_iter()
      .map(|i|i.iter().rposition(|&r| r == true).unwrap())
      .collect()
}

@ShellyGarion
Copy link
Member

I also think we can add some release notes saying that there was a performance improvement in synth_cnot_depth_line_kms since it's implementation was ported to rust

Copy link
Contributor

@alexanderivrii alexanderivrii left a comment

Choose a reason for hiding this comment

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

Thanks Gadi, the PR looks very nice and is quite close to the original Python code. I have left a few comments in-place. The main one is probably to rethink which functions we want to expose from the rust side. You probably want to add release notes as well.

Comment on lines +19 to +26
/// Optimize the synthesis of an n-qubit circuit contains only CX gates for
/// linear nearest neighbor (LNN) connectivity.
/// The depth of the circuit is bounded by 5*n, while the gate count is approximately 2.5*n^2
///
/// References:
/// [1]: Kutin, S., Moulton, D. P., Smithline, L. (2007).
/// Computation at a Distance.
/// `arXiv:quant-ph/0701194 <https://arxiv.org/abs/quant-ph/0701194>`_.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you mean this as a regular comment, and not as a docstring comment for InstructionList. If so, it should preceded by //, not by ///. In addition, it may make sense to move the reference to the paper to the docstring of optimize_cx_circ_depth_5n_line.

/// `arXiv:quant-ph/0701194 <https://arxiv.org/abs/quant-ph/0701194>`_.

type InstructionList = Vec<(usize, usize)>;
fn _row_sum(row_1: ArrayView1<bool>, row_2: ArrayView1<bool>) -> Option<Array1<bool>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

A small question: In the case that the two rows have different lengths, would it make sense to return an Error rather than None (that is, returning a Result rather than Option)?

let mut mat = mat.to_owned();
let mut mat_t = mat.to_owned();
let mut mat_inv_t = mat_inv.to_owned();

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, we aggressively call copy in the Python code, but it might be a good time to revisit if this excessive copying is really required.

// Use the instructions in U, which contains only gates of the form cx(a,b) a>b
// to transform the matrix to a permuted lower-triangular matrix.
// The original Matrix is unchanged.
for i in (0..n).rev() {
Copy link
Contributor

Choose a reason for hiding this comment

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

We definitely can, but I think this is a good tradeoff between code clarity and code "rustiness". 😄

/// For each row in mat_t, save the column index of the last "1"
fn _get_label_arr(n: usize, mat_t: ArrayView2<bool>) -> Vec<usize> {
let mut label_arr: Vec<usize> = Vec::new();
for i in 0..n {
Copy link
Contributor

Choose a reason for hiding this comment

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

just for fun, you could probably do something like this (I am not sure this compiles or returns the correct result):

fn _get_label_arr(n: usize, mat_t: ArrayView2<bool>) -> Vec<usize> {
    mat
      .rows()
      .into_iter()
      .map(|i|i.iter().rposition(|&r| r == true).unwrap())
      .collect()
}

Comment on lines +294 to +299
#[pyfunction]
#[pyo3(signature = (mat))]
pub fn optimize_cx_circ_depth_5n_line(
_py: Python,
mat: PyReadonlyArray2<bool>,
) -> PyResult<(InstructionList, InstructionList)> {
Copy link
Contributor

Choose a reason for hiding this comment

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

For the rust function to be entirely self-contained and for consistency with other synthesis functions, it would be nice to return PyResult<CircuitData>.

@@ -33,7 +33,7 @@

from qiskit.circuit import QuantumCircuit
from qiskit.synthesis.linear.linear_matrix_utils import calc_inverse_matrix
from qiskit.synthesis.linear.linear_depth_lnn import _optimize_cx_circ_depth_5n_line
from qiskit._accelerate.synthesis.linear import optimize_cx_circ_depth_5n_line
Copy link
Contributor

Choose a reason for hiding this comment

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

is this change needed, i.e. shouldn't the relevant function be importable from qiskit.synthesis.linear.linear_depth_lnn?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not the "main" function in the sense that it is not the one that generates the circuit, but the helper method that generates the instruction lists (as lists of pairs), so I'm not sure if we should wrap it as well.

Comment on lines +61 to 62
cx_inst = optimize_cx_circ_depth_5n_line(mat)
qc = QuantumCircuit(num_qubits)
Copy link
Contributor

Choose a reason for hiding this comment

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

it would be nice to have all the code to construct the quantum circuit be entirely in rust.

cx_instructions_rows_m2nw, cx_instructions_rows_nw2id = _optimize_cx_circ_depth_5n_line(mat_x)
cx_instructions_rows_m2nw, cx_instructions_rows_nw2id = optimize_cx_circ_depth_5n_line(mat_x)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see the problem: we actually want to have a method that returns both lists. So maybe we can expose two different functions from the Rust crate (the one that returns a pair of lists and the one that returns the full circuit), what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I'll do this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Rust This PR or issue is related to Rust code in the repository synthesis
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Port synth_cnot_depth_line_kms to Rust
5 participants