Rewrite Sabre interfaces to be Rust native#14317
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
This commit reverts the move of star prerouting to run in Rust. At the time of this commit it was before the majority of the transpiler infrastructure was ported to rust. At that time the pass was ported we had a few one off passes that were ported to rust and we had to manage the Python rust boundary carefully. The pattern was typically to define custom rust data types that provided an alternate view of the DAGCircuit and the target for compilation that was built out of the Python space dag and target. The star prerouting pass was ported to Rust and leveraged the existing infrastructure used by SabreSwap and SabreLayout as the function of the pass on a DAG was very similar to Sabre (finding where to insert swaps and rebuilding the dag where swaps are needed). At the time this was a pragmatic choice to accelerate the pass and not need to wait for the needed infrastructure to be written in Rust. However in practice the performance gains the Rust porting of the algorithm provided were not as large as was originally hoped and now the code of the pass is tied directly to the current implementation of sabre. As the sabre interface layer is being re-written in Qiskit#14317 this is proving to be a blocker for that effort. This commit reverts the current rust implementation to unblock the sabre refactoring effort. It's still definitely worth porting this pass to Rust but we now have sufficient infrastructure in Rust to write full transpiler passes without Python. We should follow up this PR and rewrite the star prerouting pass so that full pass operates completely in rust and directly leverages the native data structures that exist in Rust now instead of the hybrid approach that ties the is tightly coupled to the internals of the sabre pass. This reverts commit b23c545.
* Revert "Port star_preroute to rust" This commit reverts the move of star prerouting to run in Rust. At the time of this commit it was before the majority of the transpiler infrastructure was ported to rust. At that time the pass was ported we had a few one off passes that were ported to rust and we had to manage the Python rust boundary carefully. The pattern was typically to define custom rust data types that provided an alternate view of the DAGCircuit and the target for compilation that was built out of the Python space dag and target. The star prerouting pass was ported to Rust and leveraged the existing infrastructure used by SabreSwap and SabreLayout as the function of the pass on a DAG was very similar to Sabre (finding where to insert swaps and rebuilding the dag where swaps are needed). At the time this was a pragmatic choice to accelerate the pass and not need to wait for the needed infrastructure to be written in Rust. However in practice the performance gains the Rust porting of the algorithm provided were not as large as was originally hoped and now the code of the pass is tied directly to the current implementation of sabre. As the sabre interface layer is being re-written in #14317 this is proving to be a blocker for that effort. This commit reverts the current rust implementation to unblock the sabre refactoring effort. It's still definitely worth porting this pass to Rust but we now have sufficient infrastructure in Rust to write full transpiler passes without Python. We should follow up this PR and rewrite the star prerouting pass so that full pass operates completely in rust and directly leverages the native data structures that exist in Rust now instead of the hybrid approach that ties the is tightly coupled to the internals of the sabre pass. This reverts commit b23c545. * Add back reverted alt text for plot in docs
e6e50d3 to
0549a6d
Compare
|
One or more of the following people are relevant to this code:
|
|
As noted in the now-updated PR comment: there's still some minor work to fix up the last couple of I also haven't run benchmarks on this any time recently. From a now-old run, I know that one of the ASV utility-scale benchmarks shows a regression, but I tracked it down to a miniscule deviation in (I think) floating-point stuff 10k+ steps into the Sabre routing algorithm. Functionally, the only reason it appears at all is because #14244 is not fixed by this PR, and the dodgy extended-set tracking causes the heuristic to be completely borked for huge circuits. I have a couple of PRs to follow this one that break RNG compatibility. They build on stuff that might seem a bit superfluous in the current PR - the reason is because I wrote way too much at once, and then modified the patch series back into an RNG-compatible patch followed by improvements, so some of the extra structures appear to get added before they're properly taken advantage of, but it was too hard to write another completely new implementation to get rid of them. |
|
Local benchmarking suggests that while this has expected performance improvements in |
|
No longer "on hold" for performance: the trouble was that this commit forcibly disabled double parallelisation in the routing component of a single layout trial, and on machines where the single layer of parallelism didn't saturate the thread count, that was a regression. The intent had been to reduce parallelism synchronisation overhead, but in practice, each routing run takes long enough, and Rayon is efficient enough under these circumstances, that it's irrelevant. |
|
Changed benchmarks, running on the 192-thread benchmarking server. |
|
The logical merge conflict with #14589 should now be resolved. |
The recently merged Qiskit#14317 the sabre passes were updated to build a coupling graph from a target in Rust as part of moving all the passes' logic into rust. There was a small oversight in the construction of the coupling graph when handling targets that contain globally defined operations. Previously, the constructed connectivity graph would treat the presence of any globally defined operation in the target as meaning the target had all to all connectivity which isn't always the case. If the globally defined gate is a 1q operation there could still be a connectivity graph. Similarly in the presence of > 2q gates in the target the python version didn't invalidate the construction of a connectivity graph unless there were no 2q operations. Instead the connectivity graph was ignoring the multiqubit operations. This commit updates the logic to match what is done in the Python space `Target.build_coupling_map()`.
The recently merged Qiskit#14317 the sabre passes were updated to build a coupling graph from a target in Rust as part of moving all the passes' logic into rust. There was a small oversight in the construction of the coupling graph when handling targets that contain globally defined operations. Previously, the constructed connectivity graph would treat the presence of any globally defined operation in the target as meaning the target had all to all connectivity which isn't always the case. If the globally defined gate is a 1q operation there could still be a connectivity graph. Similarly in the presence of > 2q gates in the target the python version didn't invalidate the construction of a connectivity graph unless there were no 2q operations. Instead the connectivity graph was ignoring the multiqubit operations. This commit updates the logic to match what is done in the Python space `Target.build_coupling_map()`.
* Correctly handle target with global 1q or 3q gates in Sabre The recently merged #14317 the sabre passes were updated to build a coupling graph from a target in Rust as part of moving all the passes' logic into rust. There was a small oversight in the construction of the coupling graph when handling targets that contain globally defined operations. Previously, the constructed connectivity graph would treat the presence of any globally defined operation in the target as meaning the target had all to all connectivity which isn't always the case. If the globally defined gate is a 1q operation there could still be a connectivity graph. Similarly in the presence of > 2q gates in the target the python version didn't invalidate the construction of a connectivity graph unless there were no 2q operations. Instead the connectivity graph was ignoring the multiqubit operations. This commit updates the logic to match what is done in the Python space `Target.build_coupling_map()`. * Store graph of 2q target component in MultiQ error
…4715) * Correctly handle target with global 1q or 3q gates in Sabre The recently merged Qiskit#14317 the sabre passes were updated to build a coupling graph from a target in Rust as part of moving all the passes' logic into rust. There was a small oversight in the construction of the coupling graph when handling targets that contain globally defined operations. Previously, the constructed connectivity graph would treat the presence of any globally defined operation in the target as meaning the target had all to all connectivity which isn't always the case. If the globally defined gate is a 1q operation there could still be a connectivity graph. Similarly in the presence of > 2q gates in the target the python version didn't invalidate the construction of a connectivity graph unless there were no 2q operations. Instead the connectivity graph was ignoring the multiqubit operations. This commit updates the logic to match what is done in the Python space `Target.build_coupling_map()`. * Store graph of 2q target component in MultiQ error
This commit fixes the support for pickling SabreSwap. In Qiskit#14317 a new RoutingTarget rust struct was added to encapsulate the target details, and this was exposed to Python so that the Python class for the transpiler pass was able to reuse the object between multiple runs. However, this new type didn't implement pickle support and it would cause a failure when trying to pickle a SabreSwap instance that had a routing target populated. This commit fixes this oversight and implements pickle support for the RoutingTarget so that SabreSwap can always be pickled. Fixes Qiskit#15071
This commit fixes the support for pickling SabreSwap. In Qiskit#14317 a new RoutingTarget rust struct was added to encapsulate the target details, and this was exposed to Python so that the Python class for the transpiler pass was able to reuse the object between multiple runs. However, this new type didn't implement pickle support and it would cause a failure when trying to pickle a SabreSwap instance that had a routing target populated. This commit fixes this oversight and implements pickle support for the RoutingTarget so that SabreSwap can always be pickled. Fixes Qiskit#15071
This commit fixes the support for pickling SabreSwap. In #14317 a new RoutingTarget rust struct was added to encapsulate the target details, and this was exposed to Python so that the Python class for the transpiler pass was able to reuse the object between multiple runs. However, this new type didn't implement pickle support and it would cause a failure when trying to pickle a SabreSwap instance that had a routing target populated. This commit fixes this oversight and implements pickle support for the RoutingTarget so that SabreSwap can always be pickled. Fixes #15071
This commit fixes the support for pickling SabreSwap. In #14317 a new RoutingTarget rust struct was added to encapsulate the target details, and this was exposed to Python so that the Python class for the transpiler pass was able to reuse the object between multiple runs. However, this new type didn't implement pickle support and it would cause a failure when trying to pickle a SabreSwap instance that had a routing target populated. This commit fixes this oversight and implements pickle support for the RoutingTarget so that SabreSwap can always be pickled. Fixes #15071 (cherry picked from commit a816e64)
This commit fixes the support for pickling SabreSwap. In #14317 a new RoutingTarget rust struct was added to encapsulate the target details, and this was exposed to Python so that the Python class for the transpiler pass was able to reuse the object between multiple runs. However, this new type didn't implement pickle support and it would cause a failure when trying to pickle a SabreSwap instance that had a routing target populated. This commit fixes this oversight and implements pickle support for the RoutingTarget so that SabreSwap can always be pickled. Fixes #15071 (cherry picked from commit a816e64) Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
* Add further creation and destructuring methods to `NLayout`
These are to allow more efficient Rust-space use of the object, allowing
reallocations to be avoided when doing things like ancilla expansion and
layout transformations at a scale larger than token swapping.
* Rewrite Sabre interfaces to be Rust native
This is a major rewrite of all the Sabre interfaces, though the core
routing logic is unchanged. Note that the test suite is unaltered.
At least as far as the test suite covers, this change is RNG compatible
with its parent, and in several places, explicit code is added to
achieve this. Given the neighbour-tracking structures are updated,
there can be some divergences, however, depending on how the order that
a coupling map was defined.
Changes to data structures
==========================
Changed `SabreDAG`
------------------
`SabreDAG` is now completely Rust-internal and not even exposed to
Python. It's constructed directly from a `DAGCircuit`. When
control-flow blocks are encountered, the temporary `DAGCircuit` backing
them is stored within the `SabreDAG` node itself, so there's no need for
all the `block_results` side-car handling.
The nodes in the graph explicitly store how the node affects routing:
they can be `TwoQ` (the actual routing target), `Synchronize` (a
non-routing object that has only data dependencies) or `ControlFlow`.
This simplifies the internal handling within the router, since now it
doesn't need to re-infer it. It also lets us decay control-flow
operations to simple `Synchronize` points when we're in layout mode,
similar to how clearing the `block_nodes` field could implicitly do
before.
A follow-up commit (which necessarily changes the RNG compatibility)
makes stronger use of this new `SabreDAG` form to compress the
interaction graph during construction, reducing the amount of work done
by routing trials when advancing the state after a gate is routed.
New `RoutingProblem`
--------------------
A minor record-like struct that bundles up several related arguments.
New `RoutingTarget`
-------------------
The old Python-space code had a 3-tuple
(NeighborTable, Graph<(), ()>, ArrayView2<f64>)
to represent the hardware chip the routing was taking place on. The
first two elements stem from difficulties transferring the Rustworkx
Python-wrapped graph backing `CouplingMap` down to Rust space, and from
trying to faithfully port the existing implementation to Rust.
The `NeighborTable` and the `Graph<(), ()>` functionally serve the same
role; they both relate a node to its neighbors, with one presenting a
list-like interface for efficient iteration, and one presenting a
graph-like interface so graph algorithms like `dijsktra`,
`token_swapper` and `adjacency_matrix` could be called on it.
Now in pure Rust space, both are replaced by a single `Neighbors`
structure, which supplies both interfaces. It implements all the
relevant `petgraph::visit` traits, so it can be directly passed to
`petgraph` and `rustworkx_core` graph algorithms. Its data format is a
flat CSR-like sorted list of neighbors. It's logically the same as
`NeighborTable`, except all the `SmallVec`s are flattened into a single
contiguous `Vec` with partition points.
The distance matrix remains the same; we need super fast lookups of
scores because it's in the innermost loop of the swap-mapper.
New `RoutingResult`
-------------------
The new result from `swap_map_trial` is no longer several distinct but
interlinking objects, but an encapsulated `RoutingResult` that borrows
the same data as `RoutingProblem` borrows (the `SabreDAG`, the
`DAGCircuit`, etc), and includes the chosen swaps inline. It contains
all the information necessary to rebuild a fully routed circuit, but
does not do this until requested.
Implementation changes
======================
Remaining Python-space passes
-----------------------------
`SabreLayout` and `SabreSwap` in Python space mostly just organise the
Python-space inputs into the correct data types for Rust space,
pass everything off, then reconstruct the Python-space objects that
those passes are expected to store in the `PropertySet`. They contain
next to no business logic themselves. The exception is that
`SabreLayout` still has its mode for running with a custom routing pass,
which is still all in Python space.
`Target`-native
---------------
Both layout and routing are now `Target`-native. The Rust methods only
accept a `Target`. If Python space is supplied with a `CouplingMap`, we
lift it into a dummy `Target` that implies the same connectivity
constraints. The public attributes of the passes that allowed access to
coupling-map properties are retained, lazily computed on demand, for
those that might still be accessing them.
Disjoint handling
-----------------
The disjoint handling is now done entirely in Rust;
`sabre_layout_and_routing` handles it internally. This includes full
routing of the DAG, even if it is split across multiple components; the
previous `SabreLayout` implementation bailed out at this stage and left
it to a later `SabreSwap` pass, but it's only a couple more lines of
code to handle in Rust space now.
A follow-up commit may hugely simplify the disjoint handling such that
the DAG never even needs to be split up.
* Safely handle zero-block control-flow ops
Python-space Qiskit doesn't actually allow specifying any control-flow
op with zero blocks right now, but this removes a `todo!` and makes us
safe against that assumption changing in the future.
* Handle case of uninitialized `Target`
* Handle out-of-range checks for partial layouts
* Handle implicit all-to-all targets
This generally does not come up within the preset pass-managers, because
we can skip layout selection if there's all-to-all connectivity. If
`SabreLayout` is called directly, however, it is still good to have a
valid output.
* Update documentation
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
* Move layout initialisation out of timing block
* Make bail-out error message more descriptive
---------
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
…4715) * Correctly handle target with global 1q or 3q gates in Sabre The recently merged Qiskit#14317 the sabre passes were updated to build a coupling graph from a target in Rust as part of moving all the passes' logic into rust. There was a small oversight in the construction of the coupling graph when handling targets that contain globally defined operations. Previously, the constructed connectivity graph would treat the presence of any globally defined operation in the target as meaning the target had all to all connectivity which isn't always the case. If the globally defined gate is a 1q operation there could still be a connectivity graph. Similarly in the presence of > 2q gates in the target the python version didn't invalidate the construction of a connectivity graph unless there were no 2q operations. Instead the connectivity graph was ignoring the multiqubit operations. This commit updates the logic to match what is done in the Python space `Target.build_coupling_map()`. * Store graph of 2q target component in MultiQ error
This commit fixes the support for pickling SabreSwap. In Qiskit#14317 a new RoutingTarget rust struct was added to encapsulate the target details, and this was exposed to Python so that the Python class for the transpiler pass was able to reuse the object between multiple runs. However, this new type didn't implement pickle support and it would cause a failure when trying to pickle a SabreSwap instance that had a routing target populated. This commit fixes this oversight and implements pickle support for the RoutingTarget so that SabreSwap can always be pickled. Fixes Qiskit#15071
Summary
From a pure-Rust interface perspective, there are several things going on here, ticks indicate whether they're done or not:
RoutingTarget)SabreDAG)SabreSwapDAG rebuild to Rust (the newRoutingResultandRoutingResult::rebuild)sabre_routing/swap_mapto use the new structures and returns.SabreSwapto use the new Rust components.SabreLayoutDAG rebuild to Rust (RoutingResult::rebuild_ontois most of the rebuild, but it's not hooked up).SabreLayoutto use the new Rust components.SabreLayout'sskip_routingoption (rather than routing and then throwing it away again)SabreLayoutusing Port disjoint layout utils in rust #14177.StarPrerouting(obsoleted by Revert "Port star_preroute to rust" #14381).todo!()stubs that apparently have no test coverage.Details and comments
Close #12279
Close #12280
The main bulk of this PR is preceded by several commits to parts of
crates/circuitthat I've separated out. Any/all of these could become separate PRs if desired; all the commits should be standalone (though I didn't test individual ones). See the commit messages for details.The rest of this section is about commit 0549a6d, which is all actual Sabre stuff (it's just the commit message).
This is a major rewrite of all the Sabre interfaces, though the core routing logic is unchanged. Note that the test suite is unaltered.
At least as far as the test suite covers, this change is 100% RNG compatible with its parent, and in several places, explicit code is added to achieve this. In utility-scale testing (in particular, one ASV test), several tens of thousands of iterations into one particular routing problem, there is a divergence, which I suspect to be floating-point differences given that everything else is the same.
Changes to data structures
Changed
SabreDAGSabreDAGis now completely Rust-internal and not even exposed to Python. It's constructed directly from aDAGCircuit. When control-flow blocks are encountered, the temporaryDAGCircuitbacking them is stored within theSabreDAGnode itself, so there's no need for all theblock_resultsside-car handling.The nodes in the graph explicitly store how the node affects routing: they can be
TwoQ(the actual routing target),Synchronize(a non-routing object that has only data dependencies) orControlFlow. This simplifies the internal handling within the router, since now it doesn't need to re-infer it. It also lets us decay control-flow operations to simpleSynchronizepoints when we're in layout mode, similar to how clearing theblock_nodesfield could implicitly do before.A follow-up commit (which necessarily changes the RNG compatibility) makes stronger use of this new
SabreDAGform to compress the interaction graph during construction, reducing the amount of work done by routing trials when advancing the state after a gate is routed.New
RoutingProblemA minor record-like struct that bundles up several related arguments.
New
RoutingTargetThe old Python-space code had a 3-tuple
to represent the hardware chip the routing was taking place on. The first two elements stem from difficulties transferring the Rustworkx Python-wrapped graph backing
CouplingMapdown to Rust space, and from trying to faithfully port the existing implementation to Rust.The
NeighborTableand theGraph<(), ()>functionally serve the same role; they both relate a node to its neighbors, with one presenting a list-like interface for efficient iteration, and one presenting a graph-like interface so graph algorithms likedijsktra,token_swapperandadjacency_matrixcould be called on it.Now in pure Rust space, both are replaced by a single
Neighborsstructure, which supplies both interfaces. It implements all the relevantpetgraph::visittraits, so it can be directly passed topetgraphandrustworkx_coregraph algorithms. Its data format is a flat CSR-like sorted list of neighbors. It's logically the same asNeighborTable, except all theSmallVecs are flattened into a single contiguousVecwith partition points.The distance matrix remains the same; we need super fast lookups of scores because it's in the innermost loop of the swap-mapper.
New
RoutingResultThe new result from
swap_map_trialis no longer several distinct but interlinking objects, but an encapsulatedRoutingResultthat borrows the same data asRoutingProblemborrows (theSabreDAG, theDAGCircuit, etc), and includes the chosen swaps inline. It contains all the information necessary to rebuild a fully routed circuit, but does not do this until requested.Implementation changes
Remaining Python-space passes
SabreLayoutandSabreSwapin Python space mostly just organise the Python-space inputs into the correct data types for Rust space, pass everything off, then reconstruct the Python-space objects that those passes are expected to store in thePropertySet. They contain next to no business logic themselves. The exception is thatSabreLayoutstill has its mode for running with a custom routing pass, which is still all in Python space.Target-nativeBoth layout and routing are now
Target-native. The Rust methods only accept aTarget. If Python space is supplied with aCouplingMap, we lift it into a dummyTargetthat implies the same connectivity constraints. The public attributes of the passes that allowed access to coupling-map properties are retained, lazily computed on demand, for those that might still be accessing them.Disjoint handling
The disjoint handling is now done entirely in Rust;
sabre_layout_and_routinghandles it internally. This includes full routing of the DAG, even if it is split across multiple components; the previousSabreLayoutimplementation bailed out at this stage and left it to a laterSabreSwappass, but it's only a couple more lines of code to handle in Rust space now.A follow-up commit may hugely simplify the disjoint handling such that the DAG never even needs to be split up.
Old details and comments
Old PR message, now out of date
As of commit 815bc3d (the original HEAD of this PR), `SabreSwap` works (I believe) completely correctly end-to-end. `SabreLayout` is currently totally broken in Python space because I haven't finished rewriting the pass yet. `StarPrerouting` is straight-up deleted because it reaches deep into Sabre internals, and I didn't want to have to deal with the pass just to have a working build - it'll be restored at some point.New
SabreDAGThe new
SabreDAGis far more detached from the Python-space one, and needed (or perhaps just wanted) its control-flow handling rewritten so the ownership model both ofSabreDAGand the outputRoutingResultwas far clearer when it came to control-flow blocks. Previously, the ownership around the temporaryDAGCircuits of the inner blocks was quite awkward in the Rust/Python split, with the blocks being flattened in the result, but Python space holding onto the temporaryDAGCircuits backing the blocks, in order to do the rebuild later. The newSabreDAGstores the temporaryDAGCircuits within itself, next to theSabreDAGthat refers to that block. This part of the change made the move to pure-Rust result and rebuild operations much more natural and easier. The types of node in the graph are now calledSynchronize,TwoQandControlFlow, whereSynchronizeis an operation that has no routing requirements, it only imposes data-synchronisation constraints (like a barrier, say).An additional change to
SabreDAGthat probably shouldn't be in this PR (and I might separate out later) is the coalescence of runs of operations onto the same arguments, or subsets, into the same node in the Sabre interaction graph, wherever possible. For example, any 1q gate is compressed into its preceding 2q gate. Similarly, any time we see a 2q gate followed by zero or more 1q gates on those two qubits, then another 2q gate on the same two qubits, the second 2q gate imposes no possible further routing constraints, so is always compressed into the prior node. I mostly made this change as a performance optimisation forupdate_routeandpopulate_extended_set(there are fewer nodes to explicitly visit), and because I wanted to avoid theHashMap-basedSwapMapstrategy of the Python-space result object, and instead have the swaps inline. The cost, though, is that I didn't want the result object to have loads of emptyVecs in it - coalescing the nodes in the interaction graph has the knock-on effect of making it so most of the individual nodes in the result will have initial swaps.New
RoutingResultThis replaces the tuple of a few
pyclasses, in favour of single unified Rust handling. TheRoutingResultobject stores a reference to theSabreDAG(and theDAGCircuit) it relates to, so it can rebuild itself, and so that it needn't store copies of the full routing order; just as the prior Python result object stored node indices in the Python DAG to facilitate the rebuild, this one stores node indices in theSabreDAG, so we get the full order of coalesced nodes without cloning it into the result object.The new
RoutingResultobject can completely reconstruct the physical output when acting insabre_routingmode, or (in theory - untested as yet)sabre_layoutmode when there is no disjoint handling required.RoutingResult::rebuildis effectively a full implementation of circuit embedding, layout application and routing rebuild. Going further,RoutingResult::rebuild_ontoshould (again, untested) work to manage rebuilding onto a largerDAGCircuitafter routing on only a subset of theTarget(such as happens in disjoint-target handling).New
RoutingTargetThe previous
(NeighborTable, Graph<(), ()>, ArrayView2<f64>)form of the target specification was strongly influenced by what we could create and pass around from Python, but also had a fair amount of duplication in it. Previously we had the problem that the Rustworkx Python-exposedCouplingMapcouldn't be consumed by our Rust passes, so we were creating aNeighborTablefrom a list of edges, rebuilding aGraph<(), ()>from that, and calculating a distance matrix entirely separately.Now we're Rust-native, we only want to work with a
Targetas input.In the new form: the roles fulfilled by
NeighborTableandGraph<(), ()>are combined into a single newNeighborsstructure, which then implements all thepetgraph::visittraits usingPhysicalQubitas the index type, so can be passed natively todijkstra(for the release valve) andtoken_swapper(for control-flow layout reversion), and in turn, the result swap paths come out already in terms ofPhysicalQubit.Internally,
Neighborsis a flat list[n0_0, n0_1, n1_0, n1_1, n1_2, n2_0, ...], wherenX_Ymeans theYth neighbor of qubitX, and a separate list of partition points into that flat list. Fundamentally, it is the same as the list-of-lists approach of the oldNeighborTable, just flattened so we don't even needSmallVecand its "did this spill to the heap?" runtime check on each access for data locality; the data is simply always localised because it is flat and contiguous. It is similar to the CSR-like structure used bySparseObservable, for example.Neighborsthen implements all the suitable petgraph visitor traits as if it were an undirected graph. Actually, it's more like a guaranteed-symmetric directed graph (each edge is duplicated) for performance reasons in swap-candidate enumeration with how Sabre works, but that largely doesn't matter.The distance matrix is now calculated directly from
Neighbors. The relevant method isn't inrustworkx_coreyet (see Qiskit/rustworkx#1439), so this patch contains a (re-optimised) port of that method.