microsoft / qsharp

Azure Quantum Development Kit, including the Q# programming language, resource estimator, and Quantum Katas
https://microsoft.github.io/qsharp/
MIT License
371 stars 74 forks source link

In Jupyter notebook, if simulation ended with runtime exception, allocated qubits are not cleared up #1265

Open tcNickolas opened 3 months ago

tcNickolas commented 3 months ago

Describe the bug

In Jupyter notebook, if simulation ended with runtime exception, allocated qubits stay around. They show up in the next DumpMachine outputs, even though there's no way to access them or clear them up.

To Reproduce

open Microsoft.Quantum.Diagnostics;

operation Demo() : Unit {
    use q = Qubit();
    X(q);
    DumpMachine();
}

Demo()

As you re-run this code, it throws an exception each time, and the number of allocated qubits grows by 1 each time, all of them in |1> state.

Expected behavior

I expect the allocated qubits to not show up in consecutive runs.

System information

minestarks commented 3 months ago

@swernli I can see how the current behavior is by design, and technically, continuing to execute statements after a runtime error is inadvisable -- but this is super common to encounter as you're iterating on code and it's mildly annoying. I think it's simple enough to accommodate.

Is it worth fixing this by just calling sim.qubit_release() before throwing a runtime failure?

https://github.com/microsoft/qsharp/blob/f5b4cb5e7c37c940841fda6c8e62a59ac195c2a2/compiler/qsc_eval/src/intrinsic.rs#L108-L113

swernli commented 3 months ago

For this specific case of qubits released in a non-zero state, I think that is an excellent solution. We are already in the process of releasing the qubits, and the language makes it hard to continue referring to them to even have a way to clean them up manually, so we should just release unconditionally. Maybe even something like:

let is_zero = qubit_is_zero(qubit);
sim.qubit_release(qubit);
if is_zero {
    Ok(Value::unit())
} else {
    Err(Error::ReleasedQubitNotZero(qubit, arg_span))
}

It's worth noting that this would fix a very common problem but not all the problems, so code like:

open Microsoft.Quantum.Diagnostics;

operation Demo() : Unit {
    use q = Qubit();
    X(q);
    let _ = 1 / 0;
}

Demo()

Would still leak a qubit each time.

tcNickolas commented 3 months ago

Can we do qubit_release upon any error, as part of error processing?

swernli commented 3 months ago

Can we do qubit_release upon any error, as part of error processing?

There might be mechanisms we can employ, but it's tricky to differentiate between qubits in a contained scope that should be released:

{
    use q = Qubit();
    X(q);
    let _ = 1 / 0;
}

vs qubits from top-level statements that shouldn't be released:

use q = Qubit();
X(q);
let _ = 1 / 0;

The evaluator doesn't do any special tracking of qubits to know which ones are in scope, but rather depends on one of the transformation passes that runs during compilation that turns qubit statements into explicit calls to allocate and release. So for example, the first snippet above with the explicit scope will internally be transformed into:

{
    let q = __quantum__rt__qubit_allocate();
    X(q);
    let _ = 1 / 0;
    __quantum__rt__qubit_release(q);
}

That final statement is what performs the release, not any special tracking in the evaluator, and the error short-circuits that statement.

minestarks commented 3 months ago

I think the feature suggestion here is essentially "stack unwinding" - making fails behave like exceptions do, deallocating resources as the stack gets unwound.

Currently fails in Q# don't have exception semantics: they don't unwind the stack, they can't be caught. Rather they're more of an "abort": program simply terminates (it's supposed to, anyway). This works fine in a standalone Q# program. But in a REPL/notebook environment, we get into weird "undefined behavior" territory. You can continue to execute statements, but we're essentially limping along at this point and can't guarantee a consistent program state.

Another hamfisted suggestion I have is to actually abort the program in Python when a fail occurs: throw away the Q# interpreter and reinitialize it. The downside is you may need to re-run some cells you've previously. The upside is that the state will always be consistent, no limping along.

swernli commented 2 months ago

I think the feature suggestion here is essentially "stack unwinding" - making fails behave like exceptions do, deallocating resources as the stack gets unwound.

Yeah, that's what we'd need, and it would need to be smart enough to know not to deallocate qubits from the top-level/global scope of the notebook.

Another hamfisted suggestion I have is to actually abort the program in Python when a fail occurs: throw away the Q# interpreter and reinitialize it. The downside is you may need to re-run some cells you've previously. The upside is that the state will always be consistent, no limping along.

I think from the users perspective this would look awfully similar to a crash, so if we went this route we'd want to have a very clear message that guides them on how to recuperate their state (rerun appropriate notebook cells but don't rerun all if there's job submissions, for example).