Skip to content

Memory Qubits#3159

Draft
fedimser wants to merge 18 commits into
mainfrom
fedimser/qmem
Draft

Memory Qubits#3159
fedimser wants to merge 18 commits into
mainfrom
fedimser/qmem

Conversation

@fedimser
Copy link
Copy Markdown
Contributor

@fedimser fedimser commented Apr 26, 2026

This PR adds support for new Q# type MemoryQubit and extends standard library with operations on it.

A “Memory Qubit” is a useful abstraction in quantum computing. It is a logical qubit on which gates or measurements are not performed directly. It can only hold a quantum state. When we want to do something with that state, we must “load” it to a regular (“compute”) qubit. When we want to retain state of compute qubit for a while but don’t perform computations, we can “store” it to a memory qubit.

This PR contains:

  • MemoryQubit type definition and changes to compiler to support it.
  • Definitions of 4 intrinsics to allocate, free, load and store qubits.
  • Compiler changes to support use syntax (similarly to Qubits) and translate it to allocation instrinsic.
  • Compiler changes to automatically free MemoryQubit when going out of scope.
  • New 3 methods in Backend (memory_qubit_allocate, memory_qubit_load, memory_qubit_store). Together with existing qubit_free, this is the interface between the Q# language and backends.
  • Supported MemoryQubits in resource estimation backend (count.qs). In other backends use default implementation that uses Qubits as MemoryQubits and Reset+SWAP for stores and loads (which is correct for simulation).
  • Added Std.MemoryQubits which defines operations to work with memory qubits (Load, Store, LoadArray, StoreArray, DoComputation) that internally delegate to 2 intrinsics (load/store).
  • Added short description of MemoryQubits in Std.MemoryQubits.
  • Added tests for syntax, simulator and resource estimation in single rust test file.

Comment thread source/compiler/qsc_eval/src/tests.rs Fixed
Comment thread source/compiler/qsc_eval/src/tests.rs Fixed
Comment thread source/compiler/qsc_eval/src/tests.rs Fixed
@fedimser fedimser changed the title [Draft] Memory Qubits Memory Qubits Apr 27, 2026
@fedimser fedimser marked this pull request as ready for review April 27, 2026 16:45
@github-actions
Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for fd2595c

Test This Branch On Main Difference
compile core + standard lib 26549011 bytes 24572311 bytes 1976700 bytes

@fedimser fedimser marked this pull request as draft April 27, 2026 16:49
@fedimser fedimser marked this pull request as ready for review April 29, 2026 17:37
@github-actions
Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 6c5e7da

Test This Branch On Main Difference
compile core + standard lib 26549011 bytes 24572311 bytes 1976700 bytes

Copy link
Copy Markdown
Collaborator

@swernli swernli left a comment

Choose a reason for hiding this comment

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

Reviewed code so far, and provided some feedback. Also, it looks like the pipeline is blocked due to some pending merge conflicts.

Comment thread library/src/tests.rs
}

/// Asserts that given Q# expression fails to compile with given error message.
pub fn test_compile_fails(expr: &str, lib: &str, expected_error_substring: &str) {
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.

Is this new function necessary? There are other tests that use test_expression_fails defined above and then expect on the output string. Would that be sufficient for new tests?

Comment thread library/src/tests.rs
Comment on lines +318 to +329
logical_counts_expr(&mut interpreter, expr)
.unwrap_or_else(|errs| panic_with_resource_estimation_errors(&errs))
}

fn panic_with_resource_estimation_errors(errs: &[ResourceEstimatorError]) -> ! {
let joined = errs
.iter()
.map(|e| format!("{e}:{e:?}"))
.collect::<Vec<_>>()
.join("\n");
panic!("resource estimation failed:\n{joined}");
}
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.

Rather than have a utility to panic with all the errors joined, you could just use the first error returned. Most of the APIs other than compile use a vector of errors for compatibility sake with parts of the infra but only ever return one error.

/// Memory register to transform.
/// ## op
/// Operation to apply to the temporary compute buffer.
operation DoComputation(mem_qs : MemoryQubit[], op : Qubit[] => Unit) : Unit {
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 feel like "do" reads weird here... I think ApplyComputation would better match our naming patterns.

}

// MemoryQubit operations.
operation __quantum__qis__memory_qubit_load(memory_qubit : MemoryQubit, qubit : Qubit) : Unit {
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.

To follow QIR naming conventions, things with __qis__ should end in __body, making this one __quantum__qis__memory_qubit_load__body. I think the "qubit" part may be a bit redundant, since we already have "quantum" and "qis" which stands for Quantum Intruction Set, so __quantum__qis__memory_load__body would be fine. Likewise the corresponding operation below would be __quantum__qis__memory_store__body.

Side note: it seems awkward to add __body but it is really only to differentiation between the inverse/adjoint operations, which have the __adj suffix.

Comment on lines +4 to +18
// # Description
// Prepares a compute qubit in |1>, stores it in a memory qubit, then loads
// it back and measures. The result should be `One`.

import Std.MemoryQubits.*;

operation Main() : Result {
use (q, mem) = (Qubit(), MemoryQubit());

X(q);
Store(q, mem);
Load(mem, q);

return MResetZ(q);
}
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 sample could use some additional explanatory text in the top level description and comments in the body itself, like our other language samples do. The motivation is both to help educate users and to act as guidance for LLMs, so it's woth explaining the patterns and motivations here.

"__quantum__qis__mresetz__body" => {
Ok(self.measure_qubit(builder::mresetz_decl(), args_value))
}
"__quantum__qis__memory_qubit_load" | "__quantum__qis__memory_qubit_load__body" => {
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.

Once the name fix in the libraries is added, only the __body name is needed here. Same for the case below.

@@ -1766,6 +1770,56 @@ impl<'a> PartialEvaluator<'a> {
"__quantum__qis__mresetz__body" => {
Ok(self.measure_qubit(builder::mresetz_decl(), args_value))
}
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.

Looks like this file is missing some clippy lint clean up. The two cases for load and store are identical, since the arguments ordering is enough to change the directionality of the operation.

.try_into()
.expect("could not convert qubit ID to u32"),
)),
Value::MemoryQubit(q) => Operand::Literal(Literal::Qubit(
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.

This can be combined with the qubit case above.

Comment on lines +3789 to +3795
let callable = Callable {
name: "__quantum__qis__swap__body".to_string(),
input_type: vec![rir::Ty::Prim(rir::Prim::Qubit), rir::Ty::Prim(rir::Prim::Qubit)],
output_type: None,
body: None,
call_type: CallableType::Regular,
};
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.

to match the pattern we have above, this should be adder to the builder class instead of inlined here.

"__quantum__rt__qubit_allocate" | "__quantum__rt__qubit_borrow" => {
"__quantum__rt__qubit_allocate"
| "__quantum__rt__qubit_borrow"
| "__quantum__rt__memory_qubit_allocate" => {
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.

no action on this, just a note to confirm understanding: this is consistent with the goal of supporting memory qubit programs as normal compute programs in QIR, as this does not guarantee that the two qubit types will use distinct pools, and if/when we want to support memory qubits as a distinct pool of ids we'd want to differentiate the two kinds of allocation/release calls with separate management (among other changes, like emitting load and store directly). Does that match your expectation as well?

@fedimser fedimser marked this pull request as draft May 7, 2026 17:58
pull Bot pushed a commit to Mattlk13/qsharp that referenced this pull request May 22, 2026
This PR introduces a new way to manually manage Memory qubits. It adds
two new operations `Std.Memory.MemoryQubitLoad` and
`Std.Memory.MemoryQubitStore` that operate on a single qubit and
instruct runtime to "load" qubit (move from memory to compute) or "save"
it (move from compute to memory).

Example:
```
Std.ResourceEstimation.EnableMemoryComputeArchitecture(0, 2);
use q = Qubit();
X(q);
Std.Memory.Store(q);
Std.Memory.Load(q);
```

Conventions:
* Q#'s `Qubit` is the "quantum value" rather than a location on a
physical device. When it is moved between locations (e.g. from "hot" to
"cold" area of quantum computer), in Q# it's still the same Q# object.
This is why Load and Store act on single qubit and mutate its "type"
(compute/memory) rather than action between 2 qubits. At some point Load
and Store is translated to 2-qubit operation between memory and compute
qubit, but this is hidden from the programmer.
* All qubits become "compute" qubits immediately as allocated.
* Applying gate/measurement to memory qubit is an error.
  * `swap_id` can be performet only between 2 compute qubits.
  * `Reset` on memory qubit is allowed.

Notes on implementing these operations in backends:
* These operations currently have effect only in resource estimation,
when `Std.ResourceEstimation.EnableMemoryComputeArchitecture` was called
with `strategy=2` (which corresponds to manual strategy). They are
no-ops in any other backend.
* This allows to have exactly the same algorithm to be resource
estimated with and without memory-compute architecture.
* In future we plan to implement these in code generator, by maintaining
2 pools for compute and memory qubits and synthesizing 2-qubit
instructions for read/write operations between memory and compute qubit.
* This is why these 2 operations are in `Std.Memory`, not
`Std.ResourceEstimation`. They describe operations that make sense
outside resource estimation, even though currently they are only
implemented in resource estimation.
* There will be no need to support these in simulators, they will remain
no-op.

Notes on interaction with existing "automatic" memory-compute
architecture:
* These features are very similar, so they either need to be merged or
be mutually exclusive. I decided to make them mutually exclusive: you
either use automatic memory-compute (using strategy=0 or 1) or manual
(strategy=2).
* Code for `MemoryComputeInfo` implementing automatic memory-compute is
untouched, I just wrapped it into enum `MemoryCompute`. By being enum,
it forces mutual exlcusivity of memory and compute architecture.
* There are differences between automatic and manual memory-compute:
* In Manual mode, qubits become "compute" immediately as allocated. In
Auto mode, between allocation and first usage qubits are neither compute
nor memory, and they become compute only on first usage.
* When trying to apply computation (gate/measurement) on memory qubit:
in Auto mode, it will load the qubit form memory. In "Manual" mode it
will result in error. This is added to force users to explicitly add
Load instruction. So that if resource estimation succeeds, the user can
be sure that they don't accidentally apply computation on memory qubits
where they didn't intend to. This can be easily changed to do auto-load,
but it is my intention that in manual mode all loads and stores must be
explicit.
* In Auto mode, all inserts into cache are counted as reads, even if
they correspond to freshly allocated qubit.
  * Total number of qubits is computed differently.

This PR is equivalent to microsoft#3159 in a
sense that it allows to model algorithms with Memory and Compute qubits
with manually moving qubits between Memory and Compute, and it allows to
get exactly the same resource estimates. Unlike that PR, this one
doesn't require any changes to the Q# language.

---------

Co-authored-by: Stefan J. Wernli <swernli@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants