Skip to content

Commit 448548a

Browse files
fedimserswernli
andauthored
Manual Memory-Compute qubits (microsoft#3204)
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>
1 parent 380f827 commit 448548a

13 files changed

Lines changed: 721 additions & 68 deletions

File tree

library/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ pub const STD_LIB: &[(&str, &str)] = &[
6161
"qsharp-library-source:Std/Measurement.qs",
6262
include_str!("../std/src/Std/Measurement.qs"),
6363
),
64+
(
65+
"qsharp-library-source:Std/Memory.qs",
66+
include_str!("../std/src/Std/Memory.qs"),
67+
),
6468
(
6569
"qsharp-library-source:QIR/Intrinsic.qs",
6670
include_str!("../std/src/QIR/Intrinsic.qs"),

library/std/qsharp.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"src/Std/Logical.qs",
1414
"src/Std/Math.qs",
1515
"src/Std/Measurement.qs",
16+
"src/Std/Memory.qs",
1617
"src/Std/Random.qs",
1718
"src/Std/Range.qs",
1819
"src/Std/ResourceEstimation.qs",

library/std/src/Std/Memory.qs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/// # Summary
5+
/// Loads a qubit from memory.
6+
///
7+
/// # Description
8+
/// Loads a qubit from memory, turning it from "memory" to "compute".
9+
/// The qubit must be in "memory" before calling this operation.
10+
/// Currently only takes effect for resource estimation with memory-compute architecture
11+
/// enabled in Manual mode.
12+
operation Load(q : Qubit) : Unit {
13+
body intrinsic;
14+
}
15+
16+
/// # Summary
17+
/// Stores a qubit into memory.
18+
///
19+
/// # Description
20+
/// Stores a qubit into memory, turning it from "compute" to "memory".
21+
/// The qubit must be in "compute" before calling this operation.
22+
/// Currently only takes effect for resource estimation with memory-compute architecture
23+
/// enabled in Manual mode.
24+
operation Store(q : Qubit) : Unit {
25+
body intrinsic;
26+
}
27+
28+
export Load, Store;

library/std/src/Std/ResourceEstimation.qs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,23 @@ operation RepeatEstimates(count : Int) : Unit is Adj {
185185
/// automatically move qubits from and back into the memory such that never
186186
/// more than `computeCapacity` qubits are used to compute at any time.
187187
///
188+
/// When using "Manual" strategy:
189+
/// - Qubits must be moved between memory and compute using
190+
/// [Std.Memory.Load](xref:Qdk.Std.Memory.Load) and
191+
/// [Std.Memory.Store](xref:Qdk.Std.Memory.Store).
192+
/// - All qubits are initially allocated as Compute qubits.
193+
/// - Capacity is ignored.
194+
/// - Applying a gate to or measuring a memory qubit will result in a runtime error.
195+
///
188196
/// # Input
189197
/// ## computeCapacity
190198
/// The maximum number of compute qubits which can be used to perform
191199
/// operations.
192200
/// ## strategy
193201
/// The strategy applied when evicting qubits from the compute qubits in case
194202
/// of maximum capacity: 0 = LRU (least recently used), 1 = LFU (least
195-
/// frequently used)
196-
function EnableMemoryComputeArchitecture(computeCapacity : Int, strategy : Int) : Unit {
203+
/// frequently used), 2 = Manual.
204+
operation EnableMemoryComputeArchitecture(computeCapacity : Int, strategy : Int) : Unit {
197205
body intrinsic;
198206
}
199207

@@ -211,6 +219,19 @@ function LeastFrequentlyUsed() : Int {
211219
return 1;
212220
}
213221

222+
/// # Summary
223+
/// Enables manual memory/compute mode for resource estimation.
224+
///
225+
/// This is a convenience wrapper over
226+
/// `EnableMemoryComputeArchitecture(0, 2)` where strategy `2` is Manual.
227+
/// In this mode, users explicitly move qubits between memory and compute
228+
/// with [Std.Memory.Load](xref:Qdk.Std.Memory.Load) and
229+
/// [Std.Memory.Store](xref:Qdk.Std.Memory.Store). Call this operation if you
230+
/// want `Load`/`Store` annotations to affect reported resource estimates.
231+
operation EnableManualMemoryComputeArchitecture() : Unit {
232+
EnableMemoryComputeArchitecture(0, 2);
233+
}
234+
214235
export
215236
SingleVariant,
216237
BeginEstimateCaching,
@@ -227,5 +248,6 @@ export
227248
EndRepeatEstimates,
228249
RepeatEstimates,
229250
EnableMemoryComputeArchitecture,
251+
EnableManualMemoryComputeArchitecture,
230252
LeastRecentlyUsed,
231253
LeastFrequentlyUsed;
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "f0c52178-5d77-43b2-8a0c-35691f96fdd9",
6+
"metadata": {},
7+
"source": [
8+
"## Memory Qubits in Q#\n",
9+
"\n",
10+
"This notebook illustrates an advanced feature in Q#: memory qubits. Memory qubits can store a quantum state, but gates or operations cannot be applied to them. We call regular qubits that do support these operations \"compute qubits.\" This distinction makes it possible to design algorithms that use fewer compute qubits, because memory qubits can use more efficient error-correction codes (see, for example, [yoked surface codes](https://arxiv.org/pdf/2312.04522)).\n",
11+
"\n",
12+
"By default, all qubits are allocated as compute qubits. Users can turn a compute qubit into a memory qubit by calling `Std.Memory.Store`; the qubit is then \"stored in memory,\" and no operations can be applied to it. When an operation needs to be performed on a qubit, it must be \"loaded\" from memory by calling `Std.Memory.Load`.\n",
13+
"\n",
14+
"To make `Std.Memory.Load` and `Std.Memory.Store` affect resource estimation, call `Std.ResourceEstimation.EnableManualMemoryComputeArchitecture()`. This enables the \"manual\" memory/compute strategy, where users explicitly move qubits between memory and compute.\n",
15+
"\n",
16+
"### Example: GHZ state preparation and measurement\n",
17+
"\n",
18+
"In the example below, we prepare the [GHZ state](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state) on 10 memory qubits. We move qubits to memory as soon as each has been used as the control qubit of a CNOT gate. As a result, this circuit needs only 2 compute qubits. It also uses 10 memory qubits (to store the state), so in total it uses 12 qubits.\n",
19+
"\n",
20+
"Then we demonstrate how to measure a memory register by moving qubits from memory to compute one-by-one before measuring them."
21+
]
22+
},
23+
{
24+
"cell_type": "code",
25+
"execution_count": 1,
26+
"id": "f596f9b9-0b8b-44cf-8a92-34e6b55ddbe1",
27+
"metadata": {},
28+
"outputs": [],
29+
"source": [
30+
"from qdk import qsharp"
31+
]
32+
},
33+
{
34+
"cell_type": "code",
35+
"execution_count": 2,
36+
"id": "56f322d4-b77d-452a-a392-9ef7fbc6c04a",
37+
"metadata": {
38+
"vscode": {
39+
"languageId": "qsharp"
40+
}
41+
},
42+
"outputs": [],
43+
"source": [
44+
"%%qsharp\n",
45+
"\n",
46+
"// Prepares GHZ state (|0..0> + |1..1>)/sqrt(2) in a memory register of length n.\n",
47+
"// Uses at most 2 compute qubits.\n",
48+
"operation PrepareGhzStateInMemory(qs: Qubit[]) : Unit {\n",
49+
" let n = Length(qs);\n",
50+
" H(qs[0]);\n",
51+
" for i in 1..n-1 {\n",
52+
" CNOT(qs[i-1], qs[i]);\n",
53+
" Std.Memory.Store(qs[i-1]);\n",
54+
" }\n",
55+
" Std.Memory.Store(qs[n-1]);\n",
56+
"}\n",
57+
"\n",
58+
"// Measures the value in a memory-qubit register, interpreted as a little-endian integer.\n",
59+
"// Uses a single compute qubit as a buffer.\n",
60+
"operation MeasureMemory(qs : Qubit[]) : Int {\n",
61+
" mutable results = [];\n",
62+
" for q in qs {\n",
63+
" Std.Memory.Load(q);\n",
64+
" set results += [MResetZ(q)];\n",
65+
" }\n",
66+
" return Std.Convert.ResultArrayAsInt(results);\n",
67+
"}\n",
68+
"\n",
69+
"operation Main() : Int {\n",
70+
" Std.ResourceEstimation.EnableManualMemoryComputeArchitecture();\n",
71+
" use qs = Qubit[10];\n",
72+
" PrepareGhzStateInMemory(qs);\n",
73+
" return MeasureMemory(qs);\n",
74+
"}\n",
75+
"\n",
76+
"operation MainNoMemoryCompute() : Int {\n",
77+
" use qs = Qubit[10];\n",
78+
" PrepareGhzStateInMemory(qs);\n",
79+
" return MeasureMemory(qs);\n",
80+
"}"
81+
]
82+
},
83+
{
84+
"cell_type": "code",
85+
"execution_count": 3,
86+
"id": "764ac24b-c5d3-4fee-93ee-e64cdb7a3be6",
87+
"metadata": {},
88+
"outputs": [
89+
{
90+
"data": {
91+
"text/plain": [
92+
"Counter({1023: 12, 0: 8})"
93+
]
94+
},
95+
"execution_count": 3,
96+
"metadata": {},
97+
"output_type": "execute_result"
98+
}
99+
],
100+
"source": [
101+
"from collections import Counter\n",
102+
"Counter(qsharp.run(\"Main()\", shots=20))"
103+
]
104+
},
105+
{
106+
"cell_type": "code",
107+
"execution_count": 4,
108+
"id": "79196eac-7f1e-4f21-9b4e-fecb95c3f5ea",
109+
"metadata": {},
110+
"outputs": [
111+
{
112+
"data": {
113+
"text/plain": [
114+
"{'numQubits': 12,\n",
115+
" 'tCount': 0,\n",
116+
" 'rotationCount': 0,\n",
117+
" 'rotationDepth': 0,\n",
118+
" 'cczCount': 0,\n",
119+
" 'ccixCount': 0,\n",
120+
" 'measurementCount': 10,\n",
121+
" 'numComputeQubits': 2,\n",
122+
" 'readFromMemoryCount': 10,\n",
123+
" 'writeToMemoryCount': 10}"
124+
]
125+
},
126+
"execution_count": 4,
127+
"metadata": {},
128+
"output_type": "execute_result"
129+
}
130+
],
131+
"source": [
132+
"qsharp.logical_counts(\"Main()\")"
133+
]
134+
},
135+
{
136+
"cell_type": "markdown",
137+
"id": "74e433db-45ce-4ff4-836b-35d4a24a912e",
138+
"metadata": {},
139+
"source": [
140+
"`logical_counts` reports 2 compute qubits and 12 total qubits, which implies 10 memory qubits, as expected.\n",
141+
"\n",
142+
"For comparison, if we do not call `EnableManualMemoryComputeArchitecture`, we get 10 total qubits without a breakdown into memory and compute, which means 10 compute qubits:"
143+
]
144+
},
145+
{
146+
"cell_type": "code",
147+
"execution_count": 5,
148+
"id": "cc72bc32-b2bf-4086-aa46-36488cdbe102",
149+
"metadata": {},
150+
"outputs": [
151+
{
152+
"data": {
153+
"text/plain": [
154+
"{'numQubits': 10,\n",
155+
" 'tCount': 0,\n",
156+
" 'rotationCount': 0,\n",
157+
" 'rotationDepth': 0,\n",
158+
" 'cczCount': 0,\n",
159+
" 'ccixCount': 0,\n",
160+
" 'measurementCount': 10}"
161+
]
162+
},
163+
"execution_count": 5,
164+
"metadata": {},
165+
"output_type": "execute_result"
166+
}
167+
],
168+
"source": [
169+
"qsharp.logical_counts(\"MainNoMemoryCompute()\")"
170+
]
171+
}
172+
],
173+
"metadata": {
174+
"kernelspec": {
175+
"display_name": "Python 3 (ipykernel)",
176+
"language": "python",
177+
"name": "python3"
178+
},
179+
"language_info": {
180+
"codemirror_mode": {
181+
"name": "ipython",
182+
"version": 3
183+
},
184+
"file_extension": ".py",
185+
"mimetype": "text/x-python",
186+
"name": "python",
187+
"nbconvert_exporter": "python",
188+
"pygments_lexer": "ipython3",
189+
"version": "3.12.3"
190+
}
191+
},
192+
"nbformat": 4,
193+
"nbformat_minor": 5
194+
}

source/compiler/qsc_doc_gen/src/generate_docs/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ fn top_index_file_generation() {
223223
| [`Std.Logical`](xref:Qdk.Std.Logical-toc) | Boolean Logic functions. |
224224
| [`Std.Math`](xref:Qdk.Std.Math-toc) | Items for classical math operations. |
225225
| [`Std.Measurement`](xref:Qdk.Std.Measurement-toc) | Items for measuring quantum results. |
226+
| [`Std.Memory`](xref:Qdk.Std.Memory-toc) | Items for managing memory qubits. |
226227
| [`Std.Random`](xref:Qdk.Std.Random-toc) | Items for creating random values. |
227228
| [`Std.Range`](xref:Qdk.Std.Range-toc) | Items for working with ranges. |
228229
| [`Std.ResourceEstimation`](xref:Qdk.Std.ResourceEstimation-toc) | Items for working with the Microsoft Quantum Resource Estimator. |

source/compiler/qsc_doc_gen/src/table_of_contents.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The Q# standard library contains the following namespaces:
2222
| [`Std.Logical`](xref:Qdk.Std.Logical-toc) | Boolean Logic functions. |
2323
| [`Std.Math`](xref:Qdk.Std.Math-toc) | Items for classical math operations. |
2424
| [`Std.Measurement`](xref:Qdk.Std.Measurement-toc) | Items for measuring quantum results. |
25+
| [`Std.Memory`](xref:Qdk.Std.Memory-toc) | Items for managing memory qubits. |
2526
| [`Std.Random`](xref:Qdk.Std.Random-toc) | Items for creating random values. |
2627
| [`Std.Range`](xref:Qdk.Std.Range-toc) | Items for working with ranges. |
2728
| [`Std.ResourceEstimation`](xref:Qdk.Std.ResourceEstimation-toc) | Items for working with the Microsoft Quantum Resource Estimator. |

source/compiler/qsc_eval/src/backend.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,9 @@ impl Backend for SparseSim {
10871087
| "AccountForEstimatesInternal"
10881088
| "BeginRepeatEstimatesInternal"
10891089
| "EndRepeatEstimatesInternal"
1090-
| "EnableMemoryComputeArchitecture" => Some(Ok(Value::unit())),
1090+
| "EnableMemoryComputeArchitecture"
1091+
| "Load"
1092+
| "Store" => Some(Ok(Value::unit())),
10911093
"ConfigurePauliNoise" => {
10921094
let [xv, yv, zv] = &*arg.unwrap_tuple() else {
10931095
panic!("tuple arity for ConfigurePauliNoise intrinsic should be 3");
@@ -1416,7 +1418,9 @@ impl Backend for CliffordSim {
14161418
| "AccountForEstimatesInternal"
14171419
| "BeginRepeatEstimatesInternal"
14181420
| "EndRepeatEstimatesInternal"
1419-
| "EnableMemoryComputeArchitecture" => Some(Ok(Value::unit())),
1421+
| "EnableMemoryComputeArchitecture"
1422+
| "Load"
1423+
| "Store" => Some(Ok(Value::unit())),
14201424
"ConfigurePauliNoise" => Some(Err(
14211425
"dynamic noise configuration not supported in Clifford simulation".to_string(),
14221426
)),

source/compiler/qsc_partial_eval/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,6 +1718,7 @@ impl<'a> PartialEvaluator<'a> {
17181718
Ok((callee_control_flow, args_control_flow))
17191719
}
17201720

1721+
#[allow(clippy::too_many_lines)]
17211722
fn eval_expr_call_to_intrinsic(
17221723
&mut self,
17231724
store_item_id: StoreItemId,
@@ -1799,6 +1800,8 @@ impl<'a> PartialEvaluator<'a> {
17991800
| "BeginRepeatEstimatesInternal"
18001801
| "EndRepeatEstimatesInternal"
18011802
| "EnableMemoryComputeArchitecture"
1803+
| "Load"
1804+
| "Store"
18021805
| "ApplyIdleNoise"
18031806
| "GlobalPhase"
18041807
| "Message"

source/compiler/qsc_partial_eval/src/management.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ impl Backend for QuantumIntrinsicsChecker {
151151
"BeginEstimateCaching" => Some(Ok(Value::Bool(true))),
152152
"EndEstimateCaching"
153153
| "EnableMemoryComputeArchitecture"
154+
| "Load"
155+
| "Store"
154156
| "GlobalPhase"
155157
| "ConfigurePauliNoise"
156158
| "ConfigureQubitLoss"

0 commit comments

Comments
 (0)