♻️ Layout: DenseMap storage and partial-injection API - #1956
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
@rturrado Thanks for another PR! 🙌 @MatthiasReumann care to take a first look here since this is closest to the stuff you have been working on? 😌 |
| const Layout exit = | ||
| TypeSwitch<Operation*, Layout>(op) | ||
| .Case<scf::ForOp>([&](scf::ForOp) { | ||
| // Find (insert) the epilogue SWAP sequence for (into) the child | ||
| // region using the restore strategy. | ||
|
|
||
| const auto swaps = restore(children[0].layout, parent.layout); | ||
| insertSWAPs<Mode>(swaps, children[0], stats, rewriter); | ||
| return parent.layout; | ||
| }) | ||
| .template Case<scf::WhileOp>([&](scf::WhileOp) { | ||
| // Find (insert) the epilogue SWAP sequence for (into) the after | ||
| // region using the restore strategy. | ||
|
|
||
| const auto swaps = restore(children[1].layout, parent.layout); | ||
| insertSWAPs<Mode>(swaps, children[1], stats, rewriter); | ||
|
|
||
| // The scf::YieldOp is the terminator in the before region and | ||
| // thus determines the final output layout. | ||
| return children[0].layout; | ||
| }) | ||
| .template Case<IfOp>([&](IfOp) { | ||
| // Find (insert) the epilogue SWAP sequence for (into) each child | ||
| // branch using the "converge" strategy. | ||
|
|
||
| const auto [convergedLayout, fst, snd] = | ||
| converge(children[0].layout, children[1].layout); | ||
| insertSWAPs<Mode>(fst, children[0], stats, rewriter); | ||
| insertSWAPs<Mode>(snd, children[1], stats, rewriter); | ||
| return convergedLayout; | ||
| }) | ||
| .template Case<IndexSwitchOp>([&](IndexSwitchOp) { | ||
| for (auto& child : children) { | ||
| const auto swaps = restore(child.layout, parent.layout); | ||
| insertSWAPs<Mode>(swaps, child, stats, rewriter); | ||
| } | ||
| return parent.layout; | ||
| }); |
There was a problem hiding this comment.
The converge strategy for qco.if and the restore strategy only of the "yielding" branch of the scf.while operation is definitely not a bug but a feature.
MatthiasReumann
left a comment
There was a problem hiding this comment.
@rturrado Thanks for the effort 🚀 Really appreciate it!
I've left one comment regarding the strategies for mapping SCF which probably shouldn't be reverted to "restore-all". The essential idea is that the SCF operations (besides scf.for) act like permutation networks changing the layout (which needs to be propagated to the parent). If you have any questions, feel free to reach out!
Thanks! This one touches a really beautiful area.
But I have to admit that it is also more complicated code, so human review here is essential. |
Many thanks!
Perfect, thanks! Yes, this was my main concern about this PR. That the new wire-index-equals-hardware-index invariant always has to restore to the parent layout and never converges. That |
I think the "wire-index-equals-hardware-index" is a pretty neat idea, which eventually I would have also looked into. In a previous version of the mapping pass (before SCF mapping), we implemented "wire-index-equals-program-index" which worked pretty nicely. Nonetheless, I think it would make sense to split this PR into two:
Especially since it's very likely that #1951 is merged before this one. |
|
🤖 AI text below 🤖 @MatthiasReumann Agreed on the split. Rough sketch: PR 1 (safe): PR 2 (invariant):
Assisted-by: Claude Opus 4.7 via Claude Code |
Layout: DenseMap storage and partial-injection API
Layout: DenseMap storage and partial-injection APILayout: DenseMap storage and partial-injection API
|
I think we'll try to get #1951 in asap (either tonight or tomorrow). Then PR1 can go on top 😌 |
Replace the two `SmallVector<size_t>` fields inside `Layout` with two `DenseMap<size_t, size_t>` so that the storage no longer assumes program and hardware qubit indices form a contiguous range `[0, N)`. This addresses the first point of munich-quantum-toolkit#1867 at the storage level; callers still add contiguous indices today. `nqubits()` continues to return the size the layout was declared with, now stored explicitly in `nqubits_` since `DenseMap::size()` reports the number of placed entries rather than the declared capacity. `add()` keeps its bounds check against `nqubits_` and gains an injectivity check via `!contains(prog)` / `!contains(hw)`. Drop `getProgramToHardware()`. It only made sense while the internal storage was already an array; under `DenseMap`, the materialization no longer belongs on `Layout`. Its sole caller in `MappingPass::search` now owns the memoization keys locally in a `std::deque<SmallVector<size_t>>`; a materialization lambda builds each key from `layout.getHardwareIndex(prog)`, and `bestDepth` stays `DenseMap<ArrayRef<size_t>, size_t>`. `std::deque` is used because `push_back` does not invalidate references to existing elements, so `ArrayRef` keys already stored in `bestDepth` stay stable as the deque grows. Relaxing the mapping from a bijection to an injection (second point of munich-quantum-toolkit#1867) is addressed in a follow-up commit. Part 1/2 of munich-quantum-toolkit#1867 Assisted-by: Claude Opus 4.7 via Claude Code Signed-off-by: rturrado <rturrado@gmail.com>
3e25d7d to
25f536f
Compare
`Layout` can now describe partial program-to-hardware mappings. `nqubits()` is split into `nProgramQubits()` and `nHardwareQubits()`. `hasProgramAt(hw)` reports whether a hardware qubit currently carries a program qubit. `swap(hwA, hwB)` accepts either side as unplaced and treats empty as a legitimate value that also gets swapped. `random(nProg, nHw, seed)` places `nProg` program qubits on distinct hardware qubits drawn from `[0, nHw)`, so `nProg < nHw` leaves the remaining hardware slots unplaced. The mapping pass keeps calling `random(n, n, rng())` and `nHardwareQubits()`, so behavior is unchanged. Actually using `nProg < nHw` and the query API from inside the pass is left for a follow-up. Part of munich-quantum-toolkit#1867 Assisted-by: Claude Opus 4.7 via Claude Code Signed-off-by: rturrado <rturrado@gmail.com>
🤖 AI text below 🤖
Description
Two commits on
Layout:Change Layout storage to DenseMap: replaceLayout'sSmallVector<size_t>storage withDenseMap<size_t, size_t>so indices no longer need to form a contiguous range[0, N).Prepare Layout API for partial injections: splitnqubits()intonProgramQubits()/nHardwareQubits(), addhasProgramAt(hw), generalizeswapto accept unplaced sides, and extendrandomtorandom(nProg, nHw, seed)sonProg < nHwleaves the remaining hardware slots unplaced.The mapping pass keeps calling
Layout::random(n, n, rng())andnHardwareQubits(), so behavior is unchanged. Actually usingnProg < nHwand the query API from inside the pass is left for a follow-up.Part of #1867
Checklist
If PR contains AI-assisted content:
🤖 *AI text below* 🤖(titles are exempt).Assisted-by: [Model Name] via [Tool Name]footer.Assisted-by: Claude Opus 4.7 via Claude Code