-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Oxidize GateDirection pass #13069
Oxidize GateDirection pass #13069
Conversation
Move GateDirection._run_coupling_map and GateDirection._run_target to Rust. Also moving the code to create replacement DAGs to Rust. Both GateDirection and CheckGateDirection will reside in gate_direction.rs.
30bf21b
to
653b5b6
Compare
One or more of the following people are relevant to this code:
|
Will add performance benchmarking numbers soon. |
Pull Request Test Coverage Report for Build 11665219277Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good so far, I have a couple of comments mainly suggesting the usage of rust native methods for the DAGCircuit
. But I feel this should be ready after those corrections.
m.add_wrapped(wrap_pyfunction!(py_fix_with_coupling_map))?; | ||
m.add_wrapped(wrap_pyfunction!(py_fix_with_target))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why we're callingn these fix_...
and not run_...
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As stated in #13069 (comment) - the main reason being that both GateDirection
and CheckGateDirection
live in this file and there are the check_...
couple and the fix_...
couple, each pair corresponds to their respective passes. I could BTW just have one run method for each pass which could take both Target and a coupling map but I actually liked better the modularity of having 2 entry points (one for target and one for coupling map), where each defines a specialized closure but both call the same function for each pass. These are being called by GateDirection.run()
anyway. That said, should we follow a convention that all pass entry functions should be named run..,
? If so I'll prefix the four python functions in the file with it. Let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is good to have conventions to keep everything consistent. That said, we haven't had any of them apply to transpiler passes in rust yet. We do have the main run method calling upon other helper methods, so having the rust methods that represent the main run
method of the pass have run
somewhere in the name would be nice. I did it as a suffix for #13237, but having it as a prefix sounds more elegant. Maybe we should open an issue about these conventions here (sort of what #12936 proposes).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a second thought and after considering some alternatives I actually liked most this one: py_check_direction_coupling_map
(and similarly for the fix
and target
variants). I find it more informational and succinct than other options like: py_run_check_with_coupling_map
or py_run_check_coupling_map
or py_run_check_direction_coupling_map
etc... Also the py_
prefix in it indicates that it's the Python entry point, hence also the pass entry point so the run
prefix is not really required for this purpose. So unless you feel strongly against it, I would prefer to stay with this more informational naming scheme here until we decide on a convention for the pass entry point names.
m.add_wrapped(wrap_pyfunction!(py_check_with_coupling_map))?; | ||
m.add_wrapped(wrap_pyfunction!(py_check_with_target))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we using these two methods outside of Rust? I don't see them being used in the python gate_direction.py
fie.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, these are used in check_gate_direction.py
. Both CheckGateDirection
and GateDirection
are implemented in gate_direction.rs
. The logic is very similar for both, so I figured it makes sense to put those together and they are rather small each.
Mainly replace going through Python with Rust-native calls in various places, most prominently when building replacement DAGs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks about ready to merge, just a couple of suggestions that warrant discussion.
|
||
for i in 0..num_qubits { | ||
let qubit = qreg.call_method1(intern!(py, "__getitem__"), (i,))?; | ||
qargs.push(dag.qubits().find(&qubit).expect("Qubit should stored")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
qargs.push(dag.qubits().find(&qubit).expect("Qubit should stored")); | |
qargs.push(dag.qubits().find(&qubit).expect("Qubit should have been stored in the DAGCircuit")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah... good catch. Thx. Done in 79a6217
qargs, | ||
&[], | ||
param, | ||
ExtraInstructionAttributes::new(None, None, None, None), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Default
here:
ExtraInstructionAttributes::new(None, None, None, None), | |
ExtraInstructionAttributes::default(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 79a6217
fn cx_replacement_dag(py: Python) -> PyResult<DAGCircuit> { | ||
let new_dag = &mut DAGCircuit::new(py)?; | ||
let qargs = add_qreg(py, new_dag, 2)?; | ||
let qargs = qargs.as_slice(); | ||
|
||
apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[0]], None)?; | ||
apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[1]], None)?; | ||
apply_operation_back( | ||
py, | ||
new_dag, | ||
StandardGate::CXGate, | ||
&[qargs[1], qargs[0]], | ||
None, | ||
)?; | ||
apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[0]], None)?; | ||
apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[1]], None)?; | ||
|
||
Ok(new_dag.clone()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we maybe optimize these circuit building functions by having them work more like OnceCell
? Where you could create a static instance that is initialized just once, you can then just call clone each time you need a replacement DAGCircuit
. For this you could use GILOnceCell
, and have these function calls just call on .get_or_init(py, |_| )
Here's an example of how it could work:
static FOO_DAG: GILOnceCell<PyResult<DAGCircuit>> = GILOnceCell::new();
fn foo_replacement_dag(py: Python) -> PyResult<DAGCircuit> {
FOO_DAG.get_or_init(py, || -> PyResult<DAGCircuit> {
let mut dag: DAGCircuit = DAGCircuit::new(py);
// Not sure if you can call `add_qreg` from within the closure.
let qargs = add_qreg(py, new_dag, 2)?;
let qargs = qargs.as_slice();
// not sure if you can call `apply_operation_back` from inside the closure.
apply_operation_back(py, &mut dag, StandardGate::XGate, &[qargs[0]], None)?;
Ok(dag)
}).cloned()
}
This ensures that the circuit is built one and that whenever we call on this function, we just clone the circuit.
This change is merely a suggestion since #13335 makes improvements on chained additions to the DAGCircuit
and these changes could be made in a follow up 🙃.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As said in #13069 (comment) this is definitely an optimization opportunity I would like to explore. I am actually exploring already some caching implementation, similar to what you suggested, namely generating a replacement DAG (on-demand) once and cloning when needed. This applies to non-parameterized gates only since replacements for parameterized gates (e.g. RZX) depend on the actual parameter. It also includes caching a template DAG for RZX and then when an RZX replacement is needed replacing the RZX node with a concrete parametrically-bound RZX node. Then I also want to look at avoiding calling .clone()
, which I believe could give even more improvement, but this will require more refactoring. But bottom-line I'm afraid I will not have time to properly explore and benchmark more optimization for the 1.3 release, so I think we should have this in a new PR. And obviously there is a risk of jut trying to micro optimize without too much benefit, but again, I'll need more time to benchmark in detail.
Runtime comparison results:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Just one inline comment that I don't think we'll address here. Great job!
let dag_op_node = Py::new( | ||
py, | ||
( | ||
DAGOpNode { | ||
instruction: CircuitInstruction { | ||
operation: packed_inst.op.clone(), | ||
qubits: py_args.unbind(), | ||
clbits: PyTuple::empty_bound(py).unbind(), | ||
params: packed_inst.params_view().iter().cloned().collect(), | ||
extra_attrs: packed_inst.extra_attrs.clone(), | ||
#[cfg(feature = "cache_pygates")] | ||
py_op: packed_inst.py_op.clone(), | ||
}, | ||
sort_key: "".into_py(py), | ||
}, | ||
DAGNode { node: None }, | ||
), | ||
)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is something I discussed with @kevinhartman, but we will probably move to using .into_py(py)
to convert an PyClass
to a PyObject
in Rust, but we want to make sure to do it in a follow up to make sure it is used everywhere, so I'm okay with leaving it as it is here but perform the change to .into_py(py)
in a follow up.
Summary
Move
GateDirection._run_coupling_map
andGateDirection._run_target
to Rust. Also moving the code which creates replacement DAGs to Rust. BothGateDirection
andCheckGateDirection
will reside after this ingate_direction.rs
.Details and comments
Depends on #13042
Close #12252