microsoft / qsharp-language

Official repository for design of the quantum programming language Q# and its core libraries
MIT License
233 stars 54 forks source link

Reduction loops #117

Open cgranade opened 2 years ago

cgranade commented 2 years ago

Suggestion

Currently, Q# provides several looping structures (for, repeat/until, and while) that specify specific control flow ordering, but does not provide any looping structures allow developers to specify data flow dependencies instead. This proposal introduces a new kind of loop, reduce, in which iterations do not have a strict execution order, but that each contribute values to a larger calculation.

By analogy to existing classical approaches such as OpenMP, such reduction loops can be used to help take advantage of parallelism with respect to classical processors, or even with respect to quantum devices.

Considerations

This proposal extends control-flow loops to include data-flow, allowing Q# to include concepts such as list comprehensions and the flatmap monad, as well as concepts unique to quantum computing such as "shots" of circuit-like subroutines.

There are a number of alternatives one could consider as well:

This proposal advances a more general version of the above based on ideas from OpenMP- and MapReduce-style parallelism. The suggestion made in this proposal can be made either as a statement that includes a new form of binding (adding to let, mutable, use, and borrow), or as an expression that can be used with existing let and mutable bindings as well as with return. Using expression-based notation would be more difficult to implement given Q#'s current statement-driven focus, but would be more parsimonious with other existing syntax such as let and return.

Context

The syntax used in these examples assumes QEP #4 as well as a way of indicating device qubit literals (i.e.: #0).

This proposal recommends that custom reduction functions must be denoted either @AssociativeReduction or @BatchReduction, in either case allowing for parallelism to be split across multiple devices. The examples below present a few custom reduction functions, with more details to be written in a formal proposal.

Examples

Example 1:
Summing over the first 𝑛 squares (statement-based notation).

function SumOfSquares(n : Int) : Int {
    reduce sum = PlusI over _ in 1..n {
        yield idx * idx;
    }
    return sum;
}

Example 2:
Summing over the first 𝑛 squares (expression-based notation).

function SumOfSquares(n : Int) : Int {
    return reduce PlusI over _ in 1..n {
        yield idx * idx;
    };
}

Example 3:
Using reduction expressions as list comprehensions.

// Assuming the following library code, and using QEP 5 syntax:
@BatchReduction()
function Collected<'TElement>(prevStates : [['TElement]], newBatch : ['TElement]) : 'TElement {
    return newBatch + reduce PlusA over state in prevStates { yield state; };
}

// # Basic list comprehension
let ys = reduce Collected over x in 1..10 { yield x * x; };
// Equivalent to Python:
// ys = [x ** 2 for x in range(10)]

// # List comprehension with filtering
let ys = reduce Collected over x in 1..10 { if x % 2 == 0 { yield x * x; } };
// Equivalent to Python:
// ys = [x ** 2 for x in range(10) if x % 2 == 0]

// # Flatmap list comprehensions
let ys = reduce Collected over x in 1..10 {
    yield x * x;
    yield x * x * x;
};
// Equivalent to Python:
// ys = sum([[x ** 2, x ** 3] for x in range(10)], [])

Example 4:
Using statement notation as circuit-like expectation value estimation.

reduce estZExpectation = ExpectationReduction(true) over _ in 1..nShots {
    use qs = Qubit[nQubits];
    Prepare(qs);
    yield Measure(basis, qs);
    ResetAll(qs);
}

// Assuming library code:
function CountReduction<'TYield>(prevBatches : [Int], newBatch : ['TYield]) : Int {
    return SumI(prevBatches) + Length(newBatch);
}

internal function ExpectationReductionStateUpdate(prevBatches : [(Int, Int)], newBatch : [Result]) : (Int, Int) {
    reduce prevNZeros = PlusI over (n, _) in prevBatches { yield n; }
    reduce prevNShots = PlusI over (_, n) in prevBatches { yield n; }

    reduce nZeros = CountReduction over result in newBatch {
        if result == Zero {
            yield; // ← Yields ().
        }
    }
    let nShots = Length(newBatch);
    return (nZeros + prevNZeros, nShots + prevNShots);
}

internal function ExpectationReductionResult(normalizeAsEigenvalue : Bool, finalState : (Int, Int)) : Double {
    let estPr = (IntAsDouble(Fst(finalState)) / IntAsDouble(Snd(finalState)));
    return normalizeAsEigenvalue
           ? 2.0 * estPr - 1.0
           | estPr;
}

@BatchReduction()
function ExpectationReduction(normalizeAsEigenvalue : Bool) : (
    (([(Int, Int)], (Int, Int)) -> (Int, Int)),
    ((Int, Int), Double)
) {
    return (
        ExpectationReductionStateUpdate,
        ExpectationReductionResult(normalizeAsEigenvalue, _)
    );
}

Example 5:
Using statement notation to express QCVV-style applications

operation EstimateSurvivalProbabilityAtLength(nQubits : Int, nShots : Int, nOperations : Int) : Double {
    reduce estPr = ExpectationReduction(false) over _ in 1..nShots {
        use qs = Qubit[nQubits];
        let sequence = DrawRandomSequence(nQubits, nOperations);
        for gate in sequence {
            ApplyGate(gate, qs);
        }
        yield Measure([PauliZ, size=nQubits], qs);
        ResetAll(qs);
    }
    return estPr;
}

Example 6:
Using device qubit literals from within reduction bodies

reduce estZExpectation = ExpectationReduction(true) over _ in 1..nShots {
    Prepare([#0, #1]);
    yield Measure(basis, [#0, #1]);
    ResetAll([#0, #1]);
}

Open questions

Affidavit (please fill out)

Please add ticks by placing a cross in the box:

Please tick all that apply: