Memory Qubits#3159
Conversation
|
Change in memory usage detected by benchmark. Memory Report for fd2595c
|
|
Change in memory usage detected by benchmark. Memory Report for 6c5e7da
|
swernli
left a comment
There was a problem hiding this comment.
Reviewed code so far, and provided some feedback. Also, it looks like the pipeline is blocked due to some pending merge conflicts.
| } | ||
|
|
||
| /// 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) { |
There was a problem hiding this comment.
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?
| 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}"); | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
| // # 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); | ||
| } |
There was a problem hiding this comment.
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" => { |
There was a problem hiding this comment.
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)) | |||
| } | |||
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
This can be combined with the qubit case above.
| 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, | ||
| }; |
There was a problem hiding this comment.
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" => { |
There was a problem hiding this comment.
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?
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>
This PR adds support for new Q# type
MemoryQubitand 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:
usesyntax (similarly to Qubits) and translate it to allocation instrinsic.memory_qubit_allocate,memory_qubit_load,memory_qubit_store). Together with existingqubit_free, this is the interface between the Q# language and backends.