Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .agent/AUDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ changes. Serialize experiments that share a checkout or build directory. A
read-only investigation does not require aborting merely because unrelated user
edits exist.

For performance experiments, follow the
[benchmark experiment rules](../AGENTS.md#benchmark-experiments). Keep the
harness, raw data, plots, and reproduction steps in
`.agents/benchmarks/<scope>/` and link them from the finding. Record neutral
results and regressions; an untested optimization remains a candidate, not a
measured finding.

### What an experiment proves

- A behavior-preserving variation that breaks an assertion shows a possible
Expand Down
8 changes: 8 additions & 0 deletions .agent/PLANS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ that an old API still exists or a follow-up was resolved. Reconcile remote
status only when it matters to the task; do not turn document cleanup into CI
monitoring.

## Performance evidence

For a performance change, identify the workload, baseline, correctness checks,
and relevant quality measures before implementation. Follow the
[benchmark experiment rules](../AGENTS.md#benchmark-experiments), and link the
self-contained `.agents/benchmarks/<scope>/` record from the plan. Keep the
measured result and its limits in the completed decision record.

## Validation

Use repository build and test entry points. Record the focused command, the
Expand Down
46 changes: 46 additions & 0 deletions .agent/plans/routing-simplification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Simplify routing without changing its heuristics

Status: complete.

## Outcome and decisions

Mapping skips repair for equal layouts and updates RoutingBundle directly.
WireInfos derives membership from its inverse vectors. Graph traversal borrows
adjacency lists; unused distance-matrix and graph APIs are removed. The mapping
boundary diagnoses qubit-carrying calls, multi-block entry functions, and
invalid options. Classical calls remain supported. Documentation describes the
dense temporary workspace and its later cleanup.

Branch convergence, voting, A* ordering, and traversal semantics are unchanged.
Payload control-flow legalization remains owned by #2162. Supported structured
regions already have single-block verifiers; the entry-function restriction is
checked by mapping. No payload capability checks were duplicated.

The optional mapping benchmark and its data, figure, and reproduction steps are
in `.agents/benchmarks/routing/README.md`. Identical mapped-IR hashes and SWAP
counts accompany a 1.56–1.82 times speedup for unchanged branch layouts. The
routing workload is essentially unchanged. Graph measurements are synthetic.

## Validation

Release builds passed 104 mapping tests, 192 QCO utility tests, and 182 compiler
tests. `uvx nox -s lint` passed. Full changed-file C++ lint passed with
`uvx nox -s cpp-lint -- 91a9e0ba514af938680cdd394d6d63195872dc9a`.

A disposable combination with #2162 at `1c5d4cc66` applied cleanly using
three-way patch application. All 100 existing mapping tests passed there. The
combined compiler suite passed 193 of 195 tests. Its two failing tests,
`PayloadControlRejectsLinearStateInGenericSCFControl` and
`PayloadControlRejectsUnstructuredCFG`, fail during parsing because they
allocate qubits outside the entry block. Both failures were reproduced with the
routing changes removed, before the mapping pass runs. Updating those #2162
inputs is outside this PR's scope.

The benchmark is self-contained under `.agents/benchmarks/routing/`, including
an explicit CMake hook. It builds against baseline and candidate checkouts
without source-tree edits or test targets. Relocation checks matched all 45
outputs per revision to the recorded hashes and SWAP counts; a candidate build
with tests disabled and a normal build without the benchmark target verified
isolation. The collection and plotting scripts were exercised separately from
the preserved historical timing data. Shared benchmark rules live in
`AGENTS.md`, with links from the plan and audit guides.
89 changes: 89 additions & 0 deletions .agents/benchmarks/routing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Routing cleanup benchmarks

Compare upstream main `91a9e0ba514af938680cdd394d6d63195872dc9a` with the
routing changes at `c38c99cf3a8974abf1b8967bb94648f2e49e2271`. The same
benchmark source and build settings were used for both executables. The recorded
results predate the relocation of this harness; relocation validation checks
output equivalence without replacing those timing samples.

![Before and after routing cleanup](before-after.png)

| Workload | Size | Before (ms) | After (ms) |
| ------------------- | ------------: | ----------: | ---------: |
| Unchanged branches | 16 sites | 1.320 | 0.845 |
| Unchanged branches | 64 sites | 3.545 | 2.053 |
| Unchanged branches | 256 sites | 13.208 | 7.266 |
| Routing | 16 sites | 0.737 | 0.733 |
| Routing | 64 sites | 2.140 | 2.146 |
| Routing | 256 sites | 23.783 | 23.259 |
| Graph, 100 searches | 128 vertices | 1.044 | 0.587 |
| Graph, 100 searches | 512 vertices | 7.967 | 2.766 |
| Graph, 100 searches | 2048 vertices | 64.042 | 13.656 |

Unchanged branches are 1.56–1.82 times faster. Routing times differ by less than
3%; this experiment does not establish an improvement for that workload.

## Workloads and limits

- **Unchanged branch layouts:** two active qubits, one measurement, and 32
dynamic conditionals containing X gates, on square targets with 16, 64, and
256 sites. No SWAPs are required. This isolates unchanged region boundaries;
it is not representative of every adaptive program.
- **Circuits that need routing:** eight active qubits and 64 CX gates with
varying partners on the same target sizes. The mapper emits 39, 50, and 53
SWAPs respectively. This checks for a cost change when branch repair is
absent.
- **Graph traversal:** 100 cycle searches on an acyclic star with 128, 512, or
2048 vertices. This exposes repeated adjacency copying at high degree. It is a
synthetic helper benchmark, not a mapper speedup on a typical coupling graph.

The mapping interval includes `PassManager::run`, including its verifier. Input
construction, cloning, output verification, SWAP counting, and printing are
outside the interval. Each process discards one warmup per workload and records
five samples. Nine process pairs alternate execution order. The figure shows
medians and interquartile ranges over the 45 samples per variant and workload;
these are sample spread, not confidence intervals or 45 independent processes.
All mapped-IR hashes and SWAP counts match across variants and repetitions. The
mapper uses seed 42, one trial, one refinement iteration, and its default
lookahead and cost weights. MLIR multithreading is disabled in the benchmark.

Measured on DGX Spark (ARM64), pinned to CPU 0, GCC 13.3.0, LLVM/MLIR 23.1.0,
release preset with IPO disabled. This is local evidence, not hosted CI or a
claim about all CPUs or circuits. Build and lint processes were stopped before
the recorded measurement run.

## Reproduce

All benchmark source, build integration, scripts, raw samples, and plots live in
this directory. The CMake top-level include adds the optional target after MQT
Core defines its libraries. Normal builds and CTest do not include it. No
source-tree edits or benchmark copies into the baseline are needed.

Create separate checkouts for the baseline and candidate. For each checkout,
configure an isolated build directory with the same compiler and dependencies.
Use the **same absolute path** to this benchmark's `enable.cmake` for both:

```sh
cmake -S /path/to/checkout -B /path/to/build -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DENABLE_IPO=OFF \
-DBUILD_MQT_CORE_MLIR=ON -DBUILD_MQT_CORE_TESTS=OFF \
-DBUILD_MQT_CORE_BINDINGS=OFF \
-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=/absolute/path/to/.agents/benchmarks/routing/enable.cmake
cmake --build /path/to/build --target mqt-core-mlir-benchmark-mapping
```

The executable is
`/path/to/build/routing-benchmark/mqt-core-mlir-benchmark-mapping` (on
multi-configuration generators, also select and use the Release directory).

Copy the resulting executables to distinct paths before rebuilding either
checkout. From this directory, collect and render the results:

```sh
taskset -c 0 python3 collect.py /path/to/before-binary /path/to/after-binary
uv run plot.py
```

`collect.py` checks output equivalence before replacing `results.csv`. `plot.py`
declares its plotting dependency through uv script metadata. Plotting adds no
project dependency. `results.csv` preserves every recorded sample.
Binary file added .agents/benchmarks/routing/before-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
168 changes: 168 additions & 0 deletions .agents/benchmarks/routing/benchmark_mapping.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Copyright (c) 2023 - 2026 Chair for Design Automation, TUM
* Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH
* All rights reserved.
*
* SPDX-License-Identifier: MIT
*
* Licensed under the MIT License
*/

#include "mlir/Compiler/Target.h"
#include "mlir/Compiler/TargetEnvironment.h"
#include "mlir/Dialect/MQT/IR/MQTDialect.h"
#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h"
#include "mlir/Dialect/QCO/IR/QCODialect.h"
#include "mlir/Dialect/QCO/IR/QCOOps.h"
#include "mlir/Dialect/QCO/Transforms/Passes.h"
#include "mlir/Dialect/QCO/Utils/Graph.h"

#include <llvm/ADT/STLExtras.h>
#include <llvm/ADT/SmallVector.h>
#include <llvm/ADT/StringRef.h>
#include <llvm/Support/Error.h>
#include <llvm/Support/raw_ostream.h>
#include <mlir/Dialect/Arith/IR/Arith.h>
#include <mlir/Dialect/Func/IR/FuncOps.h>
#include <mlir/Dialect/SCF/IR/SCF.h>
#include <mlir/IR/BuiltinOps.h>
#include <mlir/IR/MLIRContext.h>
#include <mlir/IR/OwningOpRef.h>
#include <mlir/IR/Value.h>
#include <mlir/IR/Verifier.h>
#include <mlir/Pass/PassManager.h>
#include <mlir/Support/LLVM.h>

#include <chrono>
#include <cstddef>
#include <ratio>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

using namespace mlir;
using namespace mlir::qco;
using Clock = std::chrono::steady_clock;

namespace {

CompilerTarget grid(size_t side) {
std::vector<CompilerTarget::Coupling> edges;
for (size_t row = 0; row < side; ++row) {
for (size_t col = 0; col < side; ++col) {
const auto vertex = (row * side) + col;
if (col + 1 < side) {
edges.emplace_back(vertex, vertex + 1);
}
if (row + 1 < side) {
edges.emplace_back(vertex, vertex + side);
}
}
}
return llvm::cantFail(CompilerTarget::create(
side * side, CompilerTarget::Connectivity::fromCouplings(edges),
CompilerTarget::NativeOperations::unrestricted()));
}

OwningOpRef<ModuleOp> circuit(MLIRContext& context, bool conditional) {
QCOProgramBuilder builder(&context);
builder.initialize();
SmallVector<Value> qubits;
for (size_t i = 0; i < (conditional ? 2U : 8U); ++i) {
qubits.push_back(builder.allocQubit());
}
if (conditional) {
auto [qubit, condition] = builder.measure(qubits[0]);
qubits[0] = qubit;
for (size_t i = 0; i < 32; ++i) {
qubits[1] = builder.qcoIf(condition, qubits[1],
[&](Value arg) { return builder.x(arg); });
}
} else {
for (size_t layer = 0; layer < 8; ++layer) {
for (size_t i = 0; i < qubits.size(); ++i) {
const auto j = (i + 1 + (layer % 3)) % qubits.size();
std::tie(qubits[i], qubits[j]) = builder.cx(qubits[i], qubits[j]);
}
}
}
llvm::for_each(qubits, [&](Value qubit) { builder.sink(qubit); });
return builder.finalize();
}

} // namespace

/// CSV times cover only pass execution; cloning, printing, and verification
/// are outside the timed interval. Every run reports a deterministic IR hash.
int main() {
MLIRContext context;
context.disableMultithreading();
context.loadDialect<mqt::MQTDialect, QCODialect, arith::ArithDialect,
func::FuncDialect, scf::SCFDialect>();
PayloadFormat format;
format.id = "benchmark.payload";
format.version = "1.0.0";
const auto payload =
llvm::cantFail(PayloadSpecification::create(std::move(format)));
llvm::outs() << "workload,size,sample,milliseconds,swaps,hash\n";
for (const bool conditional : {true, false}) {
for (const size_t side : {4U, 8U, 16U}) {
auto input = circuit(context, conditional);
attachTargetEnvironment(*input, TargetEnvironment(grid(side), payload));
if (failed(verify(*input))) {
return 1;
}
for (size_t sample = 0; sample < 6; ++sample) {
OwningOpRef<ModuleOp> moduleOp(input->clone());
PassManager pm(&context);
pm.addPass(createMappingPass(
MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}));
const auto start = Clock::now();
const auto result = pm.run(*moduleOp);
const auto elapsed =
std::chrono::duration<double, std::milli>(Clock::now() - start)
.count();
if (failed(result) || failed(verify(*moduleOp))) {
return 1;
}
size_t swaps = 0;
moduleOp->walk([&](SWAPOp) { ++swaps; });
std::string ir;
llvm::raw_string_ostream stream(ir);
moduleOp->print(stream);
/// Discard the first run for each workload as a warmup.
if (sample != 0) {
llvm::outs() << (conditional ? "conditional" : "routing") << ','
<< side * side << ',' << sample << ',' << elapsed << ','
<< swaps << ',' << llvm::xxh3_64bits(ir) << '\n';
}
}
}
}
for (const size_t size : {128U, 512U, 2048U}) {
SmallVector<size_t> nodes;
for (size_t i = 0; i < size; ++i) {
nodes.push_back(i);
}
Graph graph(nodes);
for (size_t i = 1; i < size; ++i) {
graph.addEdge(0, i);
}
for (size_t sample = 0; sample < 6; ++sample) {
const auto start = Clock::now();
for (size_t iteration = 0; iteration < 100; ++iteration) {
if (graph.findCycle()) {
return 1;
}
}
const auto elapsed =
std::chrono::duration<double, std::milli>(Clock::now() - start)
.count();
if (sample != 0) {
llvm::outs() << "graph," << size << ',' << sample << ',' << elapsed
<< ",0,0\n";
}
}
}
}
41 changes: 41 additions & 0 deletions .agents/benchmarks/routing/collect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM
# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH
# All rights reserved.
#
# SPDX-License-Identifier: MIT
#
# Licensed under the MIT License

# /// script
# dependencies = []
# ///
"""Collect alternating runs: python collect.py BEFORE_BINARY AFTER_BINARY."""

import csv
import io
import subprocess
import sys
from pathlib import Path

before, after = sys.argv[1:]
rows = []
for pair in range(9):
variants = (("before", before), ("after", after))
if pair % 2:
variants = variants[::-1]
for variant, binary in variants:
# Execute the benchmark binaries explicitly selected by the caller.
result = subprocess.run([binary], check=True, capture_output=True, text=True) # ruff: ignore[subprocess-without-shell-equals-true]
rows.extend({"variant": variant, "pair": pair, **row} for row in csv.DictReader(io.StringIO(result.stdout)))

for workload, size in sorted({(row["workload"], row["size"]) for row in rows}):
group = [row for row in rows if (row["workload"], row["size"]) == (workload, size)]
if len({(row["swaps"], row["hash"]) for row in group}) != 1:
msg = f"Mapped output changed for {workload}, size {size}"
raise RuntimeError(msg)


with Path(__file__).with_name("results.csv").open("w", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=list(rows[0]), lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
Loading
Loading