diff --git a/src/Qir/Runtime/lib/QIR/CMakeLists.txt b/src/Qir/Runtime/lib/QIR/CMakeLists.txt index 9dbd2b9cd74..c9812d73d6b 100644 --- a/src/Qir/Runtime/lib/QIR/CMakeLists.txt +++ b/src/Qir/Runtime/lib/QIR/CMakeLists.txt @@ -19,6 +19,7 @@ set(rt_sup_source_files delegated.cpp strings.cpp utils.cpp + QubitManager.cpp ) # Produce object lib we'll use to create a shared lib (so/dll) later on diff --git a/src/Qir/Runtime/lib/QIR/QubitManager.cpp b/src/Qir/Runtime/lib/QIR/QubitManager.cpp new file mode 100644 index 00000000000..7e200aa5c44 --- /dev/null +++ b/src/Qir/Runtime/lib/QIR/QubitManager.cpp @@ -0,0 +1,522 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "QubitManager.hpp" +#include "QirRuntime.hpp" // For quantum__rt__fail_cstr +#include // For memcpy + +namespace Microsoft +{ +namespace Quantum +{ + +// +// Failing in case of errors +// + +[[noreturn]] static void FailNow(const char* message) +{ + quantum__rt__fail_cstr(message); +} + +static void FailIf(bool condition, const char* message) +{ + if (condition) + { + quantum__rt__fail_cstr(message); + } +} + +// +// QubitListInSharedArray +// + +CQubitManager::QubitListInSharedArray::QubitListInSharedArray( + QubitIdType startId, + QubitIdType endId, + QubitIdType* sharedQubitStatusArray): + firstElement(startId), + lastElement(endId) +{ + FailIf(startId > endId || startId < 0 || endId == MaximumQubitCapacity, + "Incorrect boundaries in the linked list initialization."); + FailIf(sharedQubitStatusArray == nullptr, "Shared status array is not provided."); + + for (QubitIdType i = startId; i < endId; i++) { + sharedQubitStatusArray[i] = i + 1; // Current element points to the next element. + } + sharedQubitStatusArray[endId] = NoneMarker; // Last element ends the chain. +} + +bool CQubitManager::QubitListInSharedArray::IsEmpty() const +{ + return firstElement == NoneMarker; +} + +void CQubitManager::QubitListInSharedArray::AddQubit(QubitIdType id, bool addToFront, QubitIdType* sharedQubitStatusArray) +{ + FailIf(id == NoneMarker, "Incorrect qubit id, cannot add it to the list."); + FailIf(sharedQubitStatusArray == nullptr, "Shared status array is not provided."); + + // If the list is empty, we initialize it with the new element. + if (IsEmpty()) + { + firstElement = id; + lastElement = id; + sharedQubitStatusArray[id] = NoneMarker; // List with a single elemenet in the chain. + return; + } + + if (addToFront) + { + sharedQubitStatusArray[id] = firstElement; // The new element will point to the former first element. + firstElement = id; // The new element is now the first in the chain. + } else + { + sharedQubitStatusArray[lastElement] = id; // The last element will point to the new element. + sharedQubitStatusArray[id] = NoneMarker; // The new element will end the chain. + lastElement = id; // The new element will be the last element in the chain. + } +} + +CQubitManager::QubitIdType CQubitManager::QubitListInSharedArray::TakeQubitFromFront(QubitIdType* sharedQubitStatusArray) +{ + FailIf(sharedQubitStatusArray == nullptr, "Shared status array is not provided."); + + // First element will be returned. It is 'NoneMarker' if the list is empty. + QubitIdType result = firstElement; + + // Advance list start to the next element if list is not empty. + if (!IsEmpty()) + { + firstElement = sharedQubitStatusArray[firstElement]; // The second element will be the first. + } + + // Drop pointer to the last element if list becomes empty. + if (IsEmpty()) + { + lastElement = NoneMarker; + } + + if (result != NoneMarker) + { + sharedQubitStatusArray[result] = AllocatedMarker; + } + + return result; +} + +void CQubitManager::QubitListInSharedArray::MoveAllQubitsFrom(QubitListInSharedArray& source, QubitIdType* sharedQubitStatusArray) +{ + FailIf(sharedQubitStatusArray == nullptr, "Shared status array is not provided."); + + // No need to do anthing if source is empty. + if (source.IsEmpty()) + { + return; + } + + if (this->IsEmpty()) + { + // If this list is empty, we'll just set it to the source list. + lastElement = source.lastElement; + } else + { + // Attach source at the beginning of the list if both lists aren't empty. + sharedQubitStatusArray[source.lastElement] = firstElement; // The last element of the source chain will point to the first element of this chain. + } + firstElement = source.firstElement; // The first element from the source chain will be the first element of this chain. + + // Remove all elements from source. + source.firstElement = NoneMarker; + source.lastElement = NoneMarker; +} + + +// +// RestrictedReuseArea +// + +CQubitManager::RestrictedReuseArea::RestrictedReuseArea( + QubitListInSharedArray freeQubits): + FreeQubitsReuseProhibited(), // Default costructor + FreeQubitsReuseAllowed(freeQubits) // Default shallow copy. +{ +} + + +// +// CRestrictedReuseAreaStack +// + +void CQubitManager::CRestrictedReuseAreaStack::PushToBack(RestrictedReuseArea area) +{ + FailIf(Count() >= std::numeric_limits::max(), "Too many nested restricted reuse areas."); + this->insert(this->end(), area); +} + +CQubitManager::RestrictedReuseArea CQubitManager::CRestrictedReuseAreaStack::PopFromBack() +{ + FailIf(this->empty(), "Cannot remove restricted reuse area from an empty set."); + RestrictedReuseArea result = this->back(); + this->pop_back(); + return result; +} + +CQubitManager::RestrictedReuseArea& CQubitManager::CRestrictedReuseAreaStack::PeekBack() +{ + return this->back(); +} + +int32_t CQubitManager::CRestrictedReuseAreaStack::Count() const +{ + // The size should never exceed int32_t. + return static_cast(this->size()); +} + +// +// CQubitManager +// + +CQubitManager::CQubitManager( + QubitIdType initialQubitCapacity, + bool mayExtendCapacity, + bool encourageReuse): + mayExtendCapacity(mayExtendCapacity), + encourageReuse(encourageReuse), + qubitCapacity(initialQubitCapacity) +{ + FailIf(qubitCapacity <= 0, "Qubit capacity must be positive."); + sharedQubitStatusArray = new QubitIdType[qubitCapacity]; + + // These objects are passed by value (copies are created) + QubitListInSharedArray FreeQubitsFresh(0, qubitCapacity - 1, sharedQubitStatusArray); + RestrictedReuseArea outermostArea(FreeQubitsFresh); + freeQubitsInAreas.PushToBack(outermostArea); + + freeQubitCount = qubitCapacity; +} + +CQubitManager::~CQubitManager() +{ + if (sharedQubitStatusArray != nullptr) + { + delete[] sharedQubitStatusArray; + sharedQubitStatusArray = nullptr; + } + // freeQubitsInAreas - direct member of the class, no need to delete. +} + +// Although it is not necessary to pass area IDs to these functions, such support may be added for extra checks. +void CQubitManager::StartRestrictedReuseArea() +{ + RestrictedReuseArea newArea; + freeQubitsInAreas.PushToBack(newArea); +} + +void CQubitManager::NextRestrictedReuseSegment() +{ + FailIf(freeQubitsInAreas.Count() <= 0, "Internal error! No reuse areas."); + FailIf(freeQubitsInAreas.Count() == 1, "NextRestrictedReuseSegment() without an active area."); + RestrictedReuseArea& currentArea = freeQubitsInAreas.PeekBack(); + // When new segment starts, reuse of all free qubits in the current area becomes prohibited. + currentArea.FreeQubitsReuseProhibited.MoveAllQubitsFrom(currentArea.FreeQubitsReuseAllowed, sharedQubitStatusArray); +} + +void CQubitManager::EndRestrictedReuseArea() +{ + FailIf(freeQubitsInAreas.Count() < 2, "EndRestrictedReuseArea() without an active area."); + RestrictedReuseArea areaAboutToEnd = freeQubitsInAreas.PopFromBack(); + RestrictedReuseArea& containingArea = freeQubitsInAreas.PeekBack(); + // When area ends, reuse of all free qubits from this area becomes allowed. + containingArea.FreeQubitsReuseAllowed.MoveAllQubitsFrom(areaAboutToEnd.FreeQubitsReuseProhibited, sharedQubitStatusArray); + containingArea.FreeQubitsReuseAllowed.MoveAllQubitsFrom(areaAboutToEnd.FreeQubitsReuseAllowed, sharedQubitStatusArray); +} + +Qubit CQubitManager::Allocate() +{ + QubitIdType newQubitId = AllocateQubitId(); + FailIf(newQubitId == NoneMarker, "Not enough qubits."); + return CreateQubitObject(newQubitId); +} + +void CQubitManager::Allocate(Qubit* qubitsToAllocate, int32_t qubitCountToAllocate) +{ + if (qubitCountToAllocate == 0) + { + return; + } + FailIf(qubitCountToAllocate < 0, "Cannot allocate negative number of qubits."); + FailIf(qubitsToAllocate == nullptr, "No array provided for qubits to be allocated."); + + // Consider optimization for initial allocation of a large array at once + for (int32_t i = 0; i < qubitCountToAllocate; i++) + { + QubitIdType newQubitId = AllocateQubitId(); + if (newQubitId == NoneMarker) + { + for (int32_t k = 0; k < i; k++) + { + Release(qubitsToAllocate[k]); + } + FailNow("Not enough qubits."); + } + qubitsToAllocate[i] = CreateQubitObject(newQubitId); + } +} + +void CQubitManager::Release(Qubit qubit) +{ + FailIf(!IsValidQubit(qubit), "Qubit is not valid."); + ReleaseQubitId(QubitToId(qubit)); + DeleteQubitObject(qubit); +} + +void CQubitManager::Release(Qubit* qubitsToRelease, int32_t qubitCountToRelease) { + if (qubitCountToRelease == 0) + { + return; + } + FailIf(qubitCountToRelease < 0, "Cannot release negative number of qubits."); + FailIf(qubitsToRelease == nullptr, "No array provided with qubits to be released."); + + for (int32_t i = 0; i < qubitCountToRelease; i++) + { + Release(qubitsToRelease[i]); + qubitsToRelease[i] = nullptr; + } +} + +Qubit CQubitManager::Borrow() +{ + // We don't support true borrowing/returning at the moment. + return Allocate(); +} + +void CQubitManager::Borrow(Qubit* qubitsToBorrow, int32_t qubitCountToBorrow) +{ + // We don't support true borrowing/returning at the moment. + return Allocate(qubitsToBorrow, qubitCountToBorrow); +} + +void CQubitManager::Return(Qubit qubit) +{ + // We don't support true borrowing/returning at the moment. + Release(qubit); +} + +void CQubitManager::Return(Qubit* qubitsToReturn, int32_t qubitCountToReturn) +{ + // We don't support true borrowing/returning at the moment. + Release(qubitsToReturn, qubitCountToReturn); +} + +void CQubitManager::Disable(Qubit qubit) +{ + FailIf(!IsValidQubit(qubit), "Qubit is not valid."); + QubitIdType id = QubitToId(qubit); + + // We can only disable explicitly allocated qubits that were not borrowed. + FailIf(!IsExplicitlyAllocatedId(id), "Cannot disable qubit that is not explicitly allocated."); + sharedQubitStatusArray[id] = DisabledMarker; + + disabledQubitCount++; + FailIf(disabledQubitCount <= 0, "Incorrect disabled qubit count."); + allocatedQubitCount--; + FailIf(allocatedQubitCount < 0, "Incorrect allocated qubit count."); +} + +void CQubitManager::Disable(Qubit* qubitsToDisable, int32_t qubitCountToDisable) +{ + if (qubitCountToDisable == 0) + { + return; + } + FailIf(qubitCountToDisable < 0, "Cannot disable negative number of qubits."); + FailIf(qubitsToDisable == nullptr, "No array provided with qubits to be disabled."); + + for (int32_t i = 0; i < qubitCountToDisable; i++) + { + Disable(qubitsToDisable[i]); + } +} + +bool CQubitManager::IsValidId(QubitIdType id) const +{ + return (id >= 0) && (id < qubitCapacity); +} + +bool CQubitManager::IsDisabledId(QubitIdType id) const +{ + return sharedQubitStatusArray[id] == DisabledMarker; +} + +bool CQubitManager::IsExplicitlyAllocatedId(QubitIdType id) const +{ + return sharedQubitStatusArray[id] == AllocatedMarker; +} + +bool CQubitManager::IsFreeId(QubitIdType id) const +{ + return sharedQubitStatusArray[id] >= 0; +} + + +bool CQubitManager::IsValidQubit(Qubit qubit) const +{ + return IsValidId(QubitToId(qubit)); +} + +bool CQubitManager::IsDisabledQubit(Qubit qubit) const +{ + return IsValidQubit(qubit) && IsDisabledId(QubitToId(qubit)); +} + +bool CQubitManager::IsExplicitlyAllocatedQubit(Qubit qubit) const +{ + return IsValidQubit(qubit) && IsExplicitlyAllocatedId(QubitToId(qubit)); +} + +bool CQubitManager::IsFreeQubitId(QubitIdType id) const +{ + return IsValidId(id) && IsFreeId(id); +} + +CQubitManager::QubitIdType CQubitManager::GetQubitId(Qubit qubit) const +{ + FailIf(!IsValidQubit(qubit), "Not a valid qubit."); + return QubitToId(qubit); +} + + +Qubit CQubitManager::CreateQubitObject(QubitIdType id) +{ + // Make sure the static_cast won't overflow: + FailIf(id < 0 || id > std::numeric_limits::max(), "Qubit id is out of range."); + intptr_t pointerSizedId = static_cast(id); + return reinterpret_cast(pointerSizedId); +} + +void CQubitManager::DeleteQubitObject(Qubit qubit) +{ + // Do nothing. By default we store qubit Id in place of a pointer to a qubit. +} + +CQubitManager::QubitIdType CQubitManager::QubitToId(Qubit qubit) const +{ + intptr_t pointerSizedId = reinterpret_cast(qubit); + // Make sure the static_cast won't overflow: + FailIf(pointerSizedId < 0 || pointerSizedId > std::numeric_limits::max(), "Qubit id is out of range."); + return static_cast(pointerSizedId); +} + +void CQubitManager::EnsureCapacity(QubitIdType requestedCapacity) +{ + FailIf(requestedCapacity <= 0, "Requested qubit capacity must be positive."); + if (requestedCapacity <= qubitCapacity) + { + return; + } + // We need to reallocate shared status array, but there's no need to adjust + // existing values (NonMarker or indexes in the array). + + // Prepare new shared status array + QubitIdType* newStatusArray = new QubitIdType[requestedCapacity]; + memcpy(newStatusArray, sharedQubitStatusArray, qubitCapacity * sizeof(newStatusArray[0])); + QubitListInSharedArray newFreeQubits(qubitCapacity, requestedCapacity - 1, newStatusArray); + + // Set new data. All fresh new qubits are added to the free qubits in the outermost area. + freeQubitCount += requestedCapacity - qubitCapacity; + FailIf(freeQubitCount <= 0, "Incorrect free qubit count."); + delete[] sharedQubitStatusArray; + sharedQubitStatusArray = newStatusArray; + qubitCapacity = requestedCapacity; + freeQubitsInAreas[0].FreeQubitsReuseAllowed.MoveAllQubitsFrom(newFreeQubits, sharedQubitStatusArray); +} + +CQubitManager::QubitIdType CQubitManager::TakeFreeQubitId() +{ + // Possible future optimization: we may store and maintain links to the next + // area with non-empty free list. Need to check amortized complexity... + + QubitIdType id = NoneMarker; + if (encourageReuse) + { + // When reuse is encouraged, we start with the innermost area + for (CRestrictedReuseAreaStack::reverse_iterator rit = freeQubitsInAreas.rbegin(); rit != freeQubitsInAreas.rend(); ++rit) + { + id = rit->FreeQubitsReuseAllowed.TakeQubitFromFront(sharedQubitStatusArray); + if (id != NoneMarker) + { + break; + } + } + } else + { + // When reuse is discouraged, we start with the outermost area + for (CRestrictedReuseAreaStack::iterator it = freeQubitsInAreas.begin(); it != freeQubitsInAreas.end(); ++it) + { + id = it->FreeQubitsReuseAllowed.TakeQubitFromFront(sharedQubitStatusArray); + if (id != NoneMarker) + { + break; + } + } + } + if (id != NoneMarker) { + FailIf(id < 0 || id >= qubitCapacity, "Internal Error: Allocated invalid qubit."); + allocatedQubitCount++; + FailIf(allocatedQubitCount <= 0, "Incorrect allocated qubit count."); + freeQubitCount--; + FailIf(freeQubitCount < 0, "Incorrect free qubit count."); + } + return id; +} + +CQubitManager::QubitIdType CQubitManager::AllocateQubitId() +{ + QubitIdType newQubitId = TakeFreeQubitId(); + if (newQubitId == NoneMarker && mayExtendCapacity) + { + QubitIdType newQubitCapacity = qubitCapacity * 2; + FailIf(newQubitCapacity <= qubitCapacity, "Cannot extend capacity."); + EnsureCapacity(newQubitCapacity); + newQubitId = TakeFreeQubitId(); + } + return newQubitId; +} + +void CQubitManager::ReleaseQubitId(QubitIdType id) +{ + FailIf(id < 0 || id >= qubitCapacity, "Internal Error: Cannot release an invalid qubit."); + if (IsDisabledId(id)) + { + // Nothing to do. Qubit will stay disabled. + return; + } + + FailIf(!IsExplicitlyAllocatedId(id), "Attempt to free qubit that has not been allocated."); + + if (mayExtendCapacity && !encourageReuse) + { + // We can extend capcity and don't want reuse => Qubits will never be reused => Discard qubit. + // We put it in its own "free" list, this list will never be found again and qubit will not be reused. + sharedQubitStatusArray[id] = NoneMarker; + } else + { + // Released qubits are added to reuse area/segment in which they were released + // (rather than area/segment where they are allocated). + // Although counterintuitive, this makes code simple. + // This is reasonable because qubits should be allocated and released in the same segment. (This is not enforced) + freeQubitsInAreas.PeekBack().FreeQubitsReuseAllowed.AddQubit(id, encourageReuse, sharedQubitStatusArray); + } + + freeQubitCount++; + FailIf(freeQubitCount <= 0, "Incorrect free qubit count."); + allocatedQubitCount--; + FailIf(allocatedQubitCount < 0, "Incorrect allocated qubit count."); +} + + +} +} diff --git a/src/Qir/Runtime/lib/Simulators/FullstateSimulator.cpp b/src/Qir/Runtime/lib/Simulators/FullstateSimulator.cpp index 6d9103d6779..b6da62d9443 100644 --- a/src/Qir/Runtime/lib/Simulators/FullstateSimulator.cpp +++ b/src/Qir/Runtime/lib/Simulators/FullstateSimulator.cpp @@ -17,6 +17,7 @@ #include "QSharpSimApi_I.hpp" #include "SimFactory.hpp" #include "OutputStream.hpp" +#include "QubitManager.hpp" #ifdef _WIN32 #include @@ -107,11 +108,13 @@ namespace Quantum const QUANTUM_SIMULATOR handle = 0; unsigned simulatorId = -1; - unsigned nextQubitId = 0; // the QuantumSimulator expects contiguous ids, starting from 0 + // the QuantumSimulator expects contiguous ids, starting from 0 + std::unique_ptr qubitManager; unsigned GetQubitId(Qubit qubit) const { - return static_cast(reinterpret_cast(qubit)); + // Qubit manager uses unsigned range of int32_t for qubit ids. + return static_cast(qubitManager->GetQubitId(qubit)); } std::vector GetQubitIds(long num, Qubit* qubits) const @@ -120,7 +123,7 @@ namespace Quantum ids.reserve(num); for (long i = 0; i < num; i++) { - ids.push_back(static_cast(reinterpret_cast(qubits[i]))); + ids.push_back(GetQubitId(qubits[i])); } return ids; } @@ -156,6 +159,7 @@ namespace Quantum typedef unsigned (*TInit)(); static TInit initSimulatorInstance = reinterpret_cast(this->GetProc("init")); + qubitManager = std::make_unique(); this->simulatorId = initSimulatorInstance(); } ~CFullstateSimulator() @@ -195,10 +199,10 @@ namespace Quantum typedef void (*TAllocateQubit)(unsigned, unsigned); static TAllocateQubit allocateQubit = reinterpret_cast(this->GetProc("allocateQubit")); - const unsigned id = this->nextQubitId; - allocateQubit(this->simulatorId, id); - this->nextQubitId++; - return reinterpret_cast(id); + Qubit q = qubitManager->Allocate(); // Allocate qubit in qubit manager. + unsigned id = GetQubitId(q); // Get its id. + allocateQubit(this->simulatorId, id); // Allocate it in the simulator. + return q; } void ReleaseQubit(Qubit q) override @@ -206,7 +210,8 @@ namespace Quantum typedef void (*TReleaseQubit)(unsigned, unsigned); static TReleaseQubit releaseQubit = reinterpret_cast(this->GetProc("release")); - releaseQubit(this->simulatorId, GetQubitId(q)); + releaseQubit(this->simulatorId, GetQubitId(q)); // Release qubit in the simulator. + qubitManager->Release(q); // Release it in the qubit manager. } Result Measure(long numBases, PauliId bases[], long numTargets, Qubit targets[]) override diff --git a/src/Qir/Runtime/public/QubitManager.hpp b/src/Qir/Runtime/public/QubitManager.hpp new file mode 100644 index 00000000000..c45a74ce292 --- /dev/null +++ b/src/Qir/Runtime/public/QubitManager.hpp @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include + +#include "CoreTypes.hpp" + +namespace Microsoft +{ +namespace Quantum +{ + // CQubitManager maintains mapping between user qubit objects and + // underlying qubit identifiers (Ids). When user program allocates + // a qubit, Qubit Manager decides whether to allocate a fresh id or + // reuse existing id that was previously freed. When user program + // releases a qubit, Qubit Manager tracks it as a free qubit id. + // Decision to reuse a qubit id is influenced by restricted reuse + // areas. When a qubit id is freed in one section of a restricted + // reuse area, it cannot be reused in other sections of the same area. + // True borrowing of qubits is not supported and is currently + // implemented as a plain allocation. + class QIR_SHARED_API CQubitManager + { + public: + using QubitIdType = ::int32_t; + + // We want status array to be reasonably large. + constexpr static QubitIdType DefaultQubitCapacity = 8; + + // Indexes in the status array can potentially be in range 0 .. QubitIdType.MaxValue-1. + // This gives maximum capacity as QubitIdType.MaxValue. Actual configured capacity may be less than this. + // Index equal to QubitIdType.MaxValue doesn't exist and is reserved for 'NoneMarker' - list terminator. + constexpr static QubitIdType MaximumQubitCapacity = std::numeric_limits::max(); + + public: + CQubitManager( + QubitIdType initialQubitCapacity = DefaultQubitCapacity, + bool mayExtendCapacity = true, + bool encourageReuse = true); + + // No complex scenarios for now. Don't need to support copying/moving. + CQubitManager(const CQubitManager&) = delete; + CQubitManager& operator = (const CQubitManager&) = delete; + virtual ~CQubitManager(); + + // Restricted reuse area control + void StartRestrictedReuseArea(); + void NextRestrictedReuseSegment(); + void EndRestrictedReuseArea(); + + // Allocate a qubit. Extend capacity if necessary and possible. + // Fail if the qubit cannot be allocated. + // Computation complexity is O(number of nested restricted reuse areas). + Qubit Allocate(); + // Allocate qubitCountToAllocate qubits and store them in the provided array. Extend manager capacity if necessary and possible. + // Fail without allocating any qubits if the qubits cannot be allocated. + // Caller is responsible for providing array of sufficient size to hold qubitCountToAllocate. + void Allocate(Qubit* qubitsToAllocate, int32_t qubitCountToAllocate); + + // Releases a given qubit. + void Release(Qubit qubit); + // Releases qubitCountToRelease qubits in the provided array. + // Caller is responsible for managing memory used by the array itself (i.e. delete[] array if it was dynamically allocated). + void Release(Qubit* qubitsToRelease, int32_t qubitCountToRelease); + + // Borrow (We treat borrowing as allocation currently) + Qubit Borrow(); + void Borrow(Qubit* qubitsToBorrow, int32_t qubitCountToBorrow); + // Return (We treat returning as release currently) + void Return(Qubit qubit); + void Return(Qubit* qubitsToReturn, int32_t qubitCountToReturn); + + // Disables a given qubit. + // Once a qubit is disabled it can never be "enabled" or reallocated. + void Disable(Qubit qubit); + // Disables a set of given qubits. + // Once a qubit is disabled it can never be "enabled" or reallocated. + void Disable(Qubit* qubitsToDisable, int32_t qubitCountToDisable); + + bool IsValidQubit(Qubit qubit) const; + bool IsDisabledQubit(Qubit qubit) const; + bool IsExplicitlyAllocatedQubit(Qubit qubit) const; + bool IsFreeQubitId(QubitIdType id) const; + + QubitIdType GetQubitId(Qubit qubit) const; + + // Qubit counts: + + // Number of qubits that are disabled. When an explicitly allocated qubit + // gets disabled, it is removed from allocated count and is added to + // disabled count immediately. Subsequent Release doesn't affect counts. + int32_t GetDisabledQubitCount() const { return disabledQubitCount; } + + // Number of qubits that are explicitly allocated. This counter gets + // increased on allocation of a qubit and decreased on release of a qubit. + // Note that we treat borrowing as allocation now. + int32_t GetAllocatedQubitCount() const { return allocatedQubitCount; } + + // Number of free qubits that are currently tracked by this qubit manager. + // Note that when qubit manager may extend capacity, this doesn't account + // for qubits that may be potentially added in future via capacity extension. + // If qubit manager may extend capacity and reuse is discouraged, released + // qubits still increase this number even though they cannot be reused. + int32_t GetFreeQubitCount() const { return freeQubitCount; } + + // Total number of qubits that are currently tracked by this qubit manager. + int32_t GetQubitCapacity() const { return qubitCapacity; } + bool GetMayExtendCapacity() const { return mayExtendCapacity; } + bool GetEncourageReuse() const { return encourageReuse; } + + protected: + // May be overriden to create a custom Qubit object. + // When not overriden, it just stores qubit Id in place of a pointer to a qubit. + // id: unique qubit id + // Returns a newly instantiated qubit. + virtual Qubit CreateQubitObject(QubitIdType id); + + // May be overriden to delete a custom Qubit object. + // Must be overriden if CreateQubitObject is overriden. + // When not overriden, it does nothing. + // qubit: pointer to QUBIT + virtual void DeleteQubitObject(Qubit qubit); + + // May be overriden to get a qubit id from a custom qubit object. + // Must be overriden if CreateQubitObject is overriden. + // When not overriden, it just reinterprets pointer to qubit as a qubit id. + // qubit: pointer to QUBIT + // Returns id of a qubit pointed to by qubit. + virtual QubitIdType QubitToId(Qubit qubit) const; + + private: + // The end of free lists are marked with NoneMarker value. It is used like null for pointers. + // This value is non-negative just like other values in the free lists. See sharedQubitStatusArray. + constexpr static QubitIdType NoneMarker = std::numeric_limits::max(); + + // Explicitly allocated qubits are marked with AllocatedMarker value. + // If borrowing is implemented, negative values may be used for refcounting. + // See sharedQubitStatusArray. + constexpr static QubitIdType AllocatedMarker = std::numeric_limits::min(); + + // Disabled qubits are marked with this value. See sharedQubitStatusArray. + constexpr static QubitIdType DisabledMarker = -1; + + // QubitListInSharedArray implements a singly-linked list with "pointers" + // to the first and the last element stored. Pointers are the indexes + // in a single shared array. Shared array isn't sotored in this class + // because it can be reallocated. This class maintains status of elements + // in the list by virtue of linking them as part of this list. This class + // sets Allocated status of elementes taken from the list (via TakeQubitFromFront). + // This class is small, contains no C++ pointers and relies on default shallow copying/destruction. + struct QubitListInSharedArray final + { + private: + QubitIdType firstElement = NoneMarker; + QubitIdType lastElement = NoneMarker; + // We are not storing pointer to shared array because it can be reallocated. + // Indexes and special values remain the same on such reallocations. + + public: + // Initialize empty list + QubitListInSharedArray() = default; + + // Initialize as a list with sequential elements from startId to endId inclusve. + QubitListInSharedArray(QubitIdType startId, QubitIdType endId, QubitIdType* sharedQubitStatusArray); + + bool IsEmpty() const; + void AddQubit(QubitIdType id, bool addToFront, QubitIdType* sharedQubitStatusArray); + QubitIdType TakeQubitFromFront(QubitIdType* sharedQubitStatusArray); + void MoveAllQubitsFrom(QubitListInSharedArray& source, QubitIdType* sharedQubitStatusArray); + }; + + // Restricted reuse area consists of multiple segments. Qubits released + // in one segment cannot be reused in another. One restricted reuse area + // can be nested in a segment of another restricted reuse area. This class + // tracks current segment of an area by maintaining a list of free qubits + // in a shared status array FreeQubitsReuseAllowed. Previous segments are + // tracked collectively (not individually) by maintaining FreeQubitsReuseProhibited. + // This class is small, contains no C++ pointers and relies on default shallow copying/destruction. + struct RestrictedReuseArea final + { + public: + QubitListInSharedArray FreeQubitsReuseProhibited; + QubitListInSharedArray FreeQubitsReuseAllowed; + + RestrictedReuseArea() = default; + RestrictedReuseArea(QubitListInSharedArray freeQubits); + }; + + // This is NOT a pure stack! We modify it only by push/pop, but we also iterate over elements. + class CRestrictedReuseAreaStack final : public std::vector + { + public: + // No complex scenarios for now. Don't need to support copying/moving. + CRestrictedReuseAreaStack() = default; + CRestrictedReuseAreaStack(const CRestrictedReuseAreaStack&) = delete; + CRestrictedReuseAreaStack& operator = (const CRestrictedReuseAreaStack&) = delete; + ~CRestrictedReuseAreaStack() = default; + + void PushToBack(RestrictedReuseArea area); + RestrictedReuseArea PopFromBack(); + RestrictedReuseArea& PeekBack(); + int32_t Count() const; + }; + + private: + void EnsureCapacity(QubitIdType requestedCapacity); + + // Take free qubit id from a free list without extending capacity. + // First non-empty free list among nested restricted reuse areas are considered. + QubitIdType TakeFreeQubitId(); + // Allocate free qubit id extending capacity if necessary and possible. + QubitIdType AllocateQubitId(); + // Put qubit id back into a free list for the current restricted reuse area. + void ReleaseQubitId(QubitIdType id); + + bool IsValidId(QubitIdType id) const; + bool IsDisabledId(QubitIdType id) const; + bool IsFreeId(QubitIdType id) const; + bool IsExplicitlyAllocatedId(QubitIdType id) const; + + // Configuration Properties: + bool mayExtendCapacity = true; + bool encourageReuse = true; + + // State: + // sharedQubitStatusArray is used to store statuses of all known qubits. + // Integer value at the index of the qubit id represents the status of that qubit. + // (Ex: sharedQubitStatusArray[4] is the status of qubit with id = 4). + // Therefore qubit ids are in the range of [0..qubitCapacity). + // Capacity may be extended if MayExtendCapacity = true. + // If qubit X is allocated, sharedQubitStatusArray[X] = AllocatedMarker (negative number) + // If qubit X is disabled, sharedQubitStatusArray[X] = DisabledMarker (negative number) + // If qubit X is free, sharedQubitStatusArray[X] is a non-negative number, denote it Next(X). + // Next(X) is either the index of the next element in the list or the list terminator - NoneMarker. + // All free qubits form disjoint singly linked lists bound to to respective resricted reuse areas. + // Each area has two lists of free qubits - see RestrictedReuseArea. + QubitIdType* sharedQubitStatusArray = nullptr; + // qubitCapacity is always equal to the array size. + QubitIdType qubitCapacity = 0; + // All nested restricted reuse areas at the current moment. + // Fresh Free Qubits are added to the outermost area: freeQubitsInAreas[0].FreeQubitsReuseAllowed + CRestrictedReuseAreaStack freeQubitsInAreas; + + // Counts: + int32_t disabledQubitCount = 0; + int32_t allocatedQubitCount = 0; + int32_t freeQubitCount = 0; + }; + +} +} diff --git a/src/Qir/Runtime/unittests/CMakeLists.txt b/src/Qir/Runtime/unittests/CMakeLists.txt index 40c41776d2a..664fc359a8f 100644 --- a/src/Qir/Runtime/unittests/CMakeLists.txt +++ b/src/Qir/Runtime/unittests/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(qir-runtime-unittests QirRuntimeTests.cpp ToffoliTests.cpp TracerTests.cpp + QubitManagerTests.cpp $ $ $ diff --git a/src/Qir/Runtime/unittests/QubitManagerTests.cpp b/src/Qir/Runtime/unittests/QubitManagerTests.cpp new file mode 100644 index 00000000000..966b36f9645 --- /dev/null +++ b/src/Qir/Runtime/unittests/QubitManagerTests.cpp @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include + +#include "catch.hpp" + +#include "QubitManager.hpp" + +using namespace Microsoft::Quantum; + +TEST_CASE("Simple allocation and release of one qubit", "[QubitManagerBasic]") +{ + std::unique_ptr qm = std::make_unique(); + Qubit q = qm->Allocate(); + qm->Release(q); +} + +TEST_CASE("Allocation and reallocation of one qubit", "[QubitManagerBasic]") +{ + std::unique_ptr qm = std::make_unique(1, false, true); + REQUIRE(qm->GetFreeQubitCount() == 1); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + Qubit q = qm->Allocate(); + REQUIRE(qm->GetQubitId(q) == 0); + REQUIRE(qm->GetFreeQubitCount() == 0); + REQUIRE(qm->GetAllocatedQubitCount() == 1); + REQUIRE_THROWS(qm->Allocate()); + REQUIRE(qm->GetFreeQubitCount() == 0); + REQUIRE(qm->GetAllocatedQubitCount() == 1); + qm->Release(q); + REQUIRE(qm->GetFreeQubitCount() == 1); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + Qubit q0 = qm->Allocate(); + REQUIRE(qm->GetQubitId(q0) == 0); + REQUIRE(qm->GetFreeQubitCount() == 0); + REQUIRE(qm->GetAllocatedQubitCount() == 1); + qm->Release(q0); + REQUIRE(qm->GetFreeQubitCount() == 1); + REQUIRE(qm->GetAllocatedQubitCount() == 0); +} + +TEST_CASE("Qubit Status", "[QubitManagerBasic]") +{ + std::unique_ptr qm = std::make_unique(2, false, true); + + Qubit q0 = qm->Allocate(); + CQubitManager::QubitIdType q0id = qm->GetQubitId(q0); + REQUIRE(qm->IsValidQubit(q0)); + REQUIRE(qm->IsExplicitlyAllocatedQubit(q0)); + REQUIRE(!qm->IsDisabledQubit(q0)); + REQUIRE(!qm->IsFreeQubitId(q0id)); + REQUIRE(qm->GetAllocatedQubitCount() == 1); + + qm->Disable(q0); + REQUIRE(qm->IsValidQubit(q0)); + REQUIRE(!qm->IsExplicitlyAllocatedQubit(q0)); + REQUIRE(qm->IsDisabledQubit(q0)); + REQUIRE(!qm->IsFreeQubitId(q0id)); + REQUIRE(qm->GetDisabledQubitCount() == 1); + + qm->Release(q0); + REQUIRE(!qm->IsFreeQubitId(q0id)); + REQUIRE(qm->GetFreeQubitCount() == 1); + + Qubit q1 = qm->Allocate(); + CQubitManager::QubitIdType q1id = qm->GetQubitId(q1); + REQUIRE(q0id != q1id); + + REQUIRE(qm->IsValidQubit(q1)); + REQUIRE(qm->IsExplicitlyAllocatedQubit(q1)); + REQUIRE(!qm->IsDisabledQubit(q1)); + REQUIRE(!qm->IsFreeQubitId(q1id)); + + REQUIRE(qm->GetAllocatedQubitCount() == 1); + REQUIRE(qm->GetDisabledQubitCount() == 1); + REQUIRE(qm->GetFreeQubitCount() == 0); + + qm->Release(q1); + REQUIRE(qm->IsFreeQubitId(q1id)); +} + +TEST_CASE("Qubit Counts", "[QubitManagerBasic]") +{ + constexpr int totalQubitCount = 100; + constexpr int disabledQubitCount = 29; + constexpr int extraQubitCount = 43; + static_assert(extraQubitCount <= totalQubitCount); + static_assert(disabledQubitCount <= totalQubitCount); + // We don't want capacity to be extended at first... + static_assert(extraQubitCount + disabledQubitCount <= totalQubitCount); + + std::unique_ptr qm = std::make_unique(totalQubitCount, true, true); + REQUIRE(qm->GetQubitCapacity() == totalQubitCount); + REQUIRE(qm->GetFreeQubitCount() == totalQubitCount); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + REQUIRE(qm->GetDisabledQubitCount() == 0); + + Qubit* qubits = new Qubit[disabledQubitCount]; + qm->Allocate(qubits, disabledQubitCount); + REQUIRE(qm->GetQubitCapacity() == totalQubitCount); + REQUIRE(qm->GetFreeQubitCount() == totalQubitCount-disabledQubitCount); + REQUIRE(qm->GetAllocatedQubitCount() == disabledQubitCount); + REQUIRE(qm->GetDisabledQubitCount() == 0); + + qm->Disable(qubits, disabledQubitCount); + REQUIRE(qm->GetQubitCapacity() == totalQubitCount); + REQUIRE(qm->GetFreeQubitCount() == totalQubitCount-disabledQubitCount); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + REQUIRE(qm->GetDisabledQubitCount() == disabledQubitCount); + + qm->Release(qubits, disabledQubitCount); + REQUIRE(qm->GetQubitCapacity() == totalQubitCount); + REQUIRE(qm->GetFreeQubitCount() == totalQubitCount-disabledQubitCount); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + REQUIRE(qm->GetDisabledQubitCount() == disabledQubitCount); + delete[] qubits; + + qubits = new Qubit[extraQubitCount]; + qm->Allocate(qubits, extraQubitCount); + REQUIRE(qm->GetQubitCapacity() == totalQubitCount); + REQUIRE(qm->GetFreeQubitCount() == totalQubitCount-disabledQubitCount-extraQubitCount); + REQUIRE(qm->GetAllocatedQubitCount() == extraQubitCount); + REQUIRE(qm->GetDisabledQubitCount() == disabledQubitCount); + + qm->Release(qubits, extraQubitCount); + REQUIRE(qm->GetQubitCapacity() == totalQubitCount); + REQUIRE(qm->GetFreeQubitCount() == totalQubitCount-disabledQubitCount); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + REQUIRE(qm->GetDisabledQubitCount() == disabledQubitCount); + delete[] qubits; + + qubits = new Qubit[totalQubitCount]; + qm->Allocate(qubits, totalQubitCount); + REQUIRE(qm->GetQubitCapacity() > totalQubitCount); + REQUIRE(qm->GetAllocatedQubitCount() == totalQubitCount); + REQUIRE(qm->GetDisabledQubitCount() == disabledQubitCount); + + qm->Release(qubits, totalQubitCount); + REQUIRE(qm->GetQubitCapacity() > totalQubitCount); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + REQUIRE(qm->GetDisabledQubitCount() == disabledQubitCount); + delete[] qubits; +} + +TEST_CASE("Allocation of released qubits when reuse is encouraged", "[QubitManagerBasic]") +{ + std::unique_ptr qm = std::make_unique(2, false, true); + REQUIRE(qm->GetFreeQubitCount() == 2); + Qubit q0 = qm->Allocate(); + Qubit q1 = qm->Allocate(); + REQUIRE(qm->GetQubitId(q0) == 0); // Qubit ids should be in order + REQUIRE(qm->GetQubitId(q1) == 1); + REQUIRE_THROWS(qm->Allocate()); + REQUIRE(qm->GetFreeQubitCount() == 0); + REQUIRE(qm->GetAllocatedQubitCount() == 2); + + qm->Release(q0); + Qubit q0a = qm->Allocate(); + REQUIRE(qm->GetQubitId(q0a) == 0); // It was the only one available + REQUIRE_THROWS(qm->Allocate()); + + qm->Release(q1); + qm->Release(q0a); + REQUIRE(qm->GetFreeQubitCount() == 2); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + + Qubit q0b = qm->Allocate(); + Qubit q1a = qm->Allocate(); + REQUIRE(qm->GetQubitId(q0b) == 0); // By default reuse is encouraged, LIFO is used + REQUIRE(qm->GetQubitId(q1a) == 1); + REQUIRE_THROWS(qm->Allocate()); + REQUIRE(qm->GetFreeQubitCount() == 0); + REQUIRE(qm->GetAllocatedQubitCount() == 2); + + qm->Release(q0b); + qm->Release(q1a); +} + +TEST_CASE("Extending capacity", "[QubitManager]") +{ + std::unique_ptr qm = std::make_unique(1, true, true); + + Qubit q0 = qm->Allocate(); + REQUIRE(qm->GetQubitId(q0) == 0); + Qubit q1 = qm->Allocate(); // This should double capacity + REQUIRE(qm->GetQubitId(q1) == 1); + REQUIRE(qm->GetFreeQubitCount() == 0); + REQUIRE(qm->GetAllocatedQubitCount() == 2); + + qm->Release(q0); + Qubit q0a = qm->Allocate(); + REQUIRE(qm->GetQubitId(q0a) == 0); + Qubit q2 = qm->Allocate(); // This should double capacity again + REQUIRE(qm->GetQubitId(q2) == 2); + REQUIRE(qm->GetFreeQubitCount() == 1); + REQUIRE(qm->GetAllocatedQubitCount() == 3); + + qm->Release(q1); + qm->Release(q0a); + qm->Release(q2); + REQUIRE(qm->GetFreeQubitCount() == 4); + REQUIRE(qm->GetAllocatedQubitCount() == 0); + + Qubit* qqq = new Qubit[3]; + qm->Allocate(qqq, 3); + REQUIRE(qm->GetFreeQubitCount() == 1); + REQUIRE(qm->GetAllocatedQubitCount() == 3); + qm->Release(qqq, 3); + delete[] qqq; + REQUIRE(qm->GetFreeQubitCount() == 4); + REQUIRE(qm->GetAllocatedQubitCount() == 0); +} + +TEST_CASE("Restricted Area", "[QubitManager]") +{ + std::unique_ptr qm = std::make_unique(3, false, true); + + Qubit q0 = qm->Allocate(); + REQUIRE(qm->GetQubitId(q0) == 0); + + qm->StartRestrictedReuseArea(); + + // Allocates fresh qubit + Qubit q1 = qm->Allocate(); + REQUIRE(qm->GetQubitId(q1) == 1); + qm->Release(q1); // Released, but cannot be used in the next segment. + + qm->NextRestrictedReuseSegment(); + + // Allocates fresh qubit, q1 cannot be reused - it belongs to a differen segment. + Qubit q2 = qm->Allocate(); + REQUIRE(qm->GetQubitId(q2) == 2); + qm->Release(q2); + + Qubit q2a = qm->Allocate(); // Getting the same one as the one that's just released. + REQUIRE(qm->GetQubitId(q2a) == 2); + qm->Release(q2a); // Released, but cannot be used in the next segment. + + qm->NextRestrictedReuseSegment(); + + // There's no qubits left here. q0 is allocated, q1 and q2 are from different segments. + REQUIRE_THROWS(qm->Allocate()); + + qm->EndRestrictedReuseArea(); + + // Qubits 1 and 2 are available here again. + Qubit* qqq = new Qubit[2]; + qm->Allocate(qqq, 2); + // OK to destruct qubit manager while qubits are still allocated. + REQUIRE_THROWS(qm->Allocate()); +} + diff --git a/src/Qir/Tests/FullstateSimulator/FullstateSimulatorTests.cpp b/src/Qir/Tests/FullstateSimulator/FullstateSimulatorTests.cpp index f9369c87fa6..7fe5e789d80 100644 --- a/src/Qir/Tests/FullstateSimulator/FullstateSimulatorTests.cpp +++ b/src/Qir/Tests/FullstateSimulator/FullstateSimulatorTests.cpp @@ -77,6 +77,36 @@ TEST_CASE("Fullstate simulator: X and measure", "[fullstate_simulator]") sim->ReleaseResult(r2); } +TEST_CASE("Fullstate simulator: X, M, reuse, M", "[fullstate_simulator]") +{ + std::unique_ptr sim = CreateFullstateSimulator(); + IQuantumGateSet* iqa = dynamic_cast(sim.get()); + + Qubit q = sim->AllocateQubit(); + Result r1 = MZ(iqa, q); + REQUIRE(Result_Zero == sim->GetResultValue(r1)); + REQUIRE(sim->AreEqualResults(r1, sim->UseZero())); + + iqa->X(q); + Result r2 = MZ(iqa, q); + REQUIRE(Result_One == sim->GetResultValue(r2)); + REQUIRE(sim->AreEqualResults(r2, sim->UseOne())); + + sim->ReleaseQubit(q); + sim->ReleaseResult(r1); + sim->ReleaseResult(r2); + + Qubit qq = sim->AllocateQubit(); + Result r3 = MZ(iqa, qq); + // Allocated qubit should always be in |0> state even though we released + // q in |1> state, and qq is likely reusing the same underlying qubit as q. + REQUIRE(Result_Zero == sim->GetResultValue(r3)); + REQUIRE(sim->AreEqualResults(r3, sim->UseZero())); + + sim->ReleaseQubit(qq); + sim->ReleaseResult(r3); +} + TEST_CASE("Fullstate simulator: measure Bell state", "[fullstate_simulator]") { std::unique_ptr sim = CreateFullstateSimulator();