Skip to content

✨ Extend Layout API for partial injections - #1956

Open
rturrado wants to merge 3 commits into
munich-quantum-toolkit:mainfrom
rturrado:1867
Open

✨ Extend Layout API for partial injections#1956
rturrado wants to merge 3 commits into
munich-quantum-toolkit:mainfrom
rturrado:1867

Conversation

@rturrado

@rturrado rturrado commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Description

Extend Layout to support M <= N program-to-hardware mappings where N - M hardware slots stay unmapped:

  • Unmapped hardware entries in hardwareToProgram_ carry an UNMAPPED sentinel.
  • Split nqubits() into nProgramQubits() and nHardwareQubits(). random() takes both sizes.
  • Add hasProgramAt(hw) to distinguish mapped from unmapped hardware slots.

A possible follow-up could exercise nProg < nHw and the new query API from inside the mapping pass.

Part of #1867

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • Any agent that created, edited, or submitted GitHub content was explicitly authorized for that scope, as required by our AI Usage Guidelines.
  • Every agent-authored or agent-edited public text body begins with the visible disclosure 🤖 *AI text below* 🤖 (titles are exempt).
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

Assisted-by: Claude Opus 4.7 via Claude Code

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@burgholzer

Copy link
Copy Markdown
Member

@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? 😌

Comment on lines -1397 to -1434
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;
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 MatthiasReumann left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@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!

@rturrado

Copy link
Copy Markdown
Contributor Author

@rturrado Thanks for another PR! 🙌

Thanks! This one touches a really beautiful area.

@MatthiasReumann care to take a first look here since this is closest to the stuff you have been working on? 😌

But I have to admit that it is also more complicated code, so human review here is essential.

@rturrado

Copy link
Copy Markdown
Contributor Author

@rturrado Thanks for the effort 🚀 Really appreciate it!

Many thanks!

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!

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 which needs to be propagated to the parent constraint is key here.

@MatthiasReumann

Copy link
Copy Markdown
Collaborator

@rturrado

That the new wire-index-equals-hardware-index invariant always has to restore to the parent layout and never converges.

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:

  • The layout improvements + everything else regarding getComputation, etc.
  • The "wire-index-equals-hardware-index" logic.

Especially since it's very likely that #1951 is merged before this one.

@rturrado

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

@MatthiasReumann Agreed on the split. Rough sketch:

PR 1 (safe): Layout API additions (nProgramQubits, nHardwareQubits, hasProgramAt, swap with an empty side), the random(nProg, nHw, seed) variant that only places nProg real programs (still with placeholder fill), and any getComputation cleanup that does not depend on the invariant. Keeps WireInfos and the current dispatch (converge for qco.if, restore-yielding-only for scf.while, and vote-and-restore for qco.index_switch from #1951).

PR 2 (invariant): wire index == hardware index everywhere, sentinel WireIterator, delete WireInfos, random drops the placeholder fill, rewrite dispatch for the invariant.

Assisted-by: Claude Opus 4.7 via Claude Code

@rturrado rturrado changed the title ♻️ Layout: non-consecutive qubit indices and injective mapping ♻️ ♻️ Layout: DenseMap storage and partial-injection API Jul 28, 2026
@rturrado rturrado changed the title ♻️ ♻️ Layout: DenseMap storage and partial-injection API ♻️ Layout: DenseMap storage and partial-injection API Jul 28, 2026
@burgholzer

Copy link
Copy Markdown
Member

I think we'll try to get #1951 in asap (either tonight or tomorrow). Then PR1 can go on top 😌

@rturrado

Copy link
Copy Markdown
Contributor Author

I think we'll try to get #1951 in asap (either tonight or tomorrow). Then PR1 can go on top 😌

Perfect. Don't rush it. I'll rebase when you're done with #1951.

@mergify mergify Bot added the conflict label Aug 3, 2026
@rturrado
rturrado force-pushed the 1867 branch 3 times, most recently from 25f536f to b036d42 Compare August 7, 2026 12:00
@mergify mergify Bot removed the conflict label Aug 7, 2026

@MatthiasReumann MatthiasReumann left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great work. Looking forward for merging this 🚀

Some notes / ideas:
We can now reasonably argue that the program and hardware qubits are a consecutive range of numbers (#1993). That is, a Layout holds [M] → [N], where M <= N. Maybe this allows us to avoid DenseMaps and use SmallVectors again. The Layout is an essential data structure in the mapping pass, so we need to make sure the implementation is very efficient.

Moreover, if we now already invest the time to improve this data structure, it could also make sense to add a generalized InjectiveMap<K, V>, where the Layout would then be a subclass of that (If that is even necessary). Using templates, we may need to keep DenseMap; or specialize on integer types using SmallVector. Just an idea - maybe a follow-up!

Comment on lines +924 to +931
const auto materializeKey = [](const Layout& layout) {
SmallVector<size_t> key(layout.nHardwareQubits());
for (size_t prog = 0; prog < layout.nHardwareQubits(); ++prog) {
key[prog] = layout.getHardwareIndex(prog);
}
return key;
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Couldn't we use getProgramToHardware instead as it now returns a SmallVector<size_t>? For the mapping pass it must always hold nHardwareQubits() == nProgramQubits().

// Owns the materialized layout snapshots that `bestDepth` keys point into.
// std::deque never invalidates pointers to existing elements on push_back,
// so ArrayRefs already in `bestDepth` stay stable as we grow it.
std::deque<SmallVector<size_t>> keyStorage;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
std::deque<SmallVector<size_t>> keyStorage;
std::deque<SmallVector<size_t>> keys;

Personal preference; not a must-have.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair enough!

Comment on lines +111 to +114
/// Number of program qubits this layout was declared with.
size_t nProgramQubits_ = 0;
/// Number of hardware qubits this layout was declared with.
size_t nHardwareQubits_ = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why are these required? Shouldn't nProgramQubits (nHardwareQubits) also rather return programToHardware_.size() (hardwareToProgram_.size())?

Comment on lines +82 to 106
void Layout::swap(const size_t hwA, const size_t hwB) {
assert(hwA < nHardwareQubits_ && "hardware index out of bounds");
assert(hwB < nHardwareQubits_ && "hardware index out of bounds");
if (hwA == hwB) {
return;
}
// Read the current value on each side (may be empty), then write each to the
// other side. Empty is treated as a legitimate value.
const auto itA = hardwareToProgram_.find(hwA);
const auto itB = hardwareToProgram_.find(hwB);
const bool hasA = itA != hardwareToProgram_.end();
const bool hasB = itB != hardwareToProgram_.end();
const size_t progA = hasA ? itA->second : 0;
const size_t progB = hasB ? itB->second : 0;
hardwareToProgram_.erase(hwA);
hardwareToProgram_.erase(hwB);
if (hasA) {
hardwareToProgram_[hwB] = progA;
programToHardware_[progA] = hwB;
}
if (hasB) {
hardwareToProgram_[hwA] = progB;
programToHardware_[progB] = hwA;
}
}

Copy link
Copy Markdown
Collaborator

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 assume (assert) that both hardware qubits have program qubits assigned instead of inserting 0s.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. swap now asserts both hardware slots have program qubits assigned.

@mergify mergify Bot added the conflict label Aug 10, 2026
@rturrado

Copy link
Copy Markdown
Contributor Author

@MatthiasReumann Many thanks for the thorough review and the guidance. This PR was just the rebase onto main and doesn't yet take into account the new invariant from #1993: that program and hardware qubits are now consecutive range of numbers. I'll update the PR accordingly.

@rturrado rturrado changed the title ♻️ Layout: DenseMap storage and partial-injection API ✨ Extend Layout API for partial injections Aug 11, 2026
@mergify mergify Bot added conflict and removed conflict labels Aug 11, 2026
@burgholzer burgholzer added this to the MLIR Support milestone Aug 13, 2026
@burgholzer burgholzer added enhancement Improvement of existing feature MLIR Anything related to MLIR labels Aug 13, 2026
@burgholzer

Copy link
Copy Markdown
Member

@MatthiasReumann @rturrado Except for the minor conflict with main, what is the status here? This looks like it would be pretty close to ready, right?

@rturrado

Copy link
Copy Markdown
Contributor Author

@MatthiasReumann @rturrado Except for the minor conflict with main, what is the status here? This looks like it would be pretty close to ready, right?

@burgholzer Thanks for flagging this. I'll have a look into that conflict!

Extend `Layout` to support `M <= N` program-to-hardware mappings where `N - M` hardware slots stay unmapped:
- Unmapped hardware entries in `hardwareToProgram_` carry an `UNMAPPED` sentinel.
- Split `nqubits()` into `nProgramQubits()` and `nHardwareQubits()`. `random()` takes both sizes.
- Add `hasProgramAt(hw)` to distinguish mapped from unmapped hardware slots.

Adapt `Mapping.cpp`:
- rename `nqubits()` call sites to `nHardwareQubits()`,
- pass both sizes to `Layout::random()`.

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: rturrado <rturrado@gmail.com>

@MatthiasReumann MatthiasReumann left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for your continued work on this! @rturrado Much appreciated🙏

The pull request looks really good already! I've only left some very nitpicky comments, which should be fairly easy to resolve. Lastly, I think a changelog entry is missing also. Otherwise, let's get this PR merged ASAP 🛫

Comment on lines +45 to +47
/// Construct a layout from a program-to-hardware mapping,
/// where mapping[prog] = hw.
/// Sets both `nProgramQubits` and `nHardwareQubits` to `mapping.size()`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// Construct a layout from a program-to-hardware mapping,
/// where mapping[prog] = hw.
/// Sets both `nProgramQubits` and `nHardwareQubits` to `mapping.size()`.
/// Construct a layout from a bijective program-to-hardware mapping,
/// where mapping[prog] = hw.
/// Sets both `nProgramQubits` and `nHardwareQubits` to `mapping.size()`.

Another option would be to explicitly allow the caller to set some program qubits to UNMAPPED. This would require to change the semantics of mapping to mapping[hw] = prog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, applied the bijective addition.

On mapping[hw] = prog: agreed as a natural next step when a caller needs partial injection through fromMapping. Today no caller does; Mapping.cpp only uses it with pure permutations. Leaving it for the follow-up that will start exercising M < N from the mapping pass.

Comment on lines +78 to +80
const auto mappedCount =
std::ranges::count_if(std::views::iota(size_t{0}, nHw),
[&](size_t hw) { return layout.hasProgramAt(hw); });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nitpick: Could we do this with llvm:: utilities? For example, llvm::seq should be able to replace iota.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Beautiful. Done, replaced both std::ranges::count_if / std::views::iota with llvm::count_if / llvm::seq.

TEST(LayoutDeathTest, RejectDuplicateHardwareIndex) {
constexpr std::array<size_t, 3> mapping{0, 0, 2};
EXPECT_DEATH((void)qco::Layout::fromMapping(mapping),
EXPECT_DEATH((void)Layout::fromMapping(mapping),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nitpick: Any way to avoid that C-style cast here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Very well spotted. Done, dropped the cast. fromMapping is not [[nodiscard]], so calling it as a plain statement compiles cleanly and the cast was defensive.

@burgholzer

Copy link
Copy Markdown
Member

Lastly, I think a changelog entry is missing also.

In that regard, please just fold this PR number into the existing changelog entry for the mapping pass (and add your name to it) 😌

@rturrado

Copy link
Copy Markdown
Contributor Author

In that regard, please just fold this PR number into the existing changelog entry for the mapping pass (and add your name to it) 😌

Done, folded #1956 into the place-and-route entry and added my name.

- Doc: describe `fromMapping` as bijective.
- Test: use `llvm::count_if` / `llvm::seq` instead of `std::ranges::count_if` / `std::views::iota`.
- Test: drop the defensive `(void)` cast in `EXPECT_DEATH`.
- Add `munich-quantum-toolkit#1956` to the `place-and-route` changelog entry.

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: rturrado <rturrado@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement of existing feature MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants