diff --git a/src/Simulation/EntryPointDriver.Tests/Tests.fs b/src/Simulation/EntryPointDriver.Tests/Tests.fs index 7206cf9f44e..076887b8ab7 100644 --- a/src/Simulation/EntryPointDriver.Tests/Tests.fs +++ b/src/Simulation/EntryPointDriver.Tests/Tests.fs @@ -486,6 +486,7 @@ let private resourceSummary = T 0 0 Depth 0 0 Width 1 1 + QubitCount 1 1 BorrowedWidth 0 0" [] diff --git a/src/Simulation/QCTraceSimulator/DepthCounter.cs b/src/Simulation/QCTraceSimulator/DepthCounter.cs index 8cd0c58cc71..c169de55d95 100644 --- a/src/Simulation/QCTraceSimulator/DepthCounter.cs +++ b/src/Simulation/QCTraceSimulator/DepthCounter.cs @@ -15,7 +15,7 @@ namespace Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime { public class DepthCounter : IQCTraceSimulatorListener, ICallGraphStatistics { - class OperationCallRecord + private class OperationCallRecord { public HashedString OperationName; public OperationFunctor FunctorSpecialization; @@ -23,11 +23,15 @@ class OperationCallRecord public double MaxOperationStartTime; public double ReleasedQubitsAvailableTime; public double ReturnedQubitsAvailableTime; + public long MaxQubitIdAtStart = -1; public QubitTimeMetrics[] InputQubitMetrics; } StatisticsCollector stats; Stack operationCallStack; + QubitAvailabilityTimeTracker qubitAvailabilityTime = new QubitAvailabilityTimeTracker( + initialCapacity: 128, // Reasonable number to preallocate. + defaultAvailabilityTime: 0.0); public IStatisticCollectorResults Results => stats; @@ -44,25 +48,30 @@ public DepthCounter(IDoubleStatistic[] statisticsToCollect = null) operationCallStack.Push(opRec); } - public class Metrics + public static class Metrics { public const string Depth = "Depth"; public const string StartTimeDifference = "StartTimeDifference"; + public const string Width = "Width"; } - public double[] StatisticsRecord(double Depth, double StartTimeDifference) + public double[] StatisticsRecord(double Depth, double StartTimeDifference, double Width) { - return new double[] { Depth, StartTimeDifference }; + return new double[] { Depth, StartTimeDifference, Width }; } #region IQCTraceSimulatorListener implementation public object NewTracingData(long qubitId) { - return new QubitTimeMetrics(); + return new QubitTimeMetrics(qubitId); } public void OnAllocate(object[] qubitsTraceData) { + foreach (QubitTimeMetrics metric in qubitsTraceData.Cast()) + { + qubitAvailabilityTime.MarkQubitIdUsed(metric.QubitId); + } } public void OnBorrow(object[] qubitsTraceData, long newQubitsAllocated) @@ -74,12 +83,11 @@ public void OnOperationEnd(object[] returnedQubitsTraceData) double maxReturnedQubitsAvailableTime = 0; if ( returnedQubitsTraceData != null ) { - QubitTimeMetrics[] qubitsMetrics = Utils.UnboxAs(returnedQubitsTraceData); - maxReturnedQubitsAvailableTime = QubitsMetricsUtils.MaxQubitAvailableTime(qubitsMetrics); + maxReturnedQubitsAvailableTime = MaxAvailableTime(returnedQubitsTraceData.Cast()); } OperationCallRecord opRec = operationCallStack.Pop(); Debug.Assert(operationCallStack.Count != 0, "Operation call stack must never get empty"); - double inputQubitsAvailableTime = QubitsMetricsUtils.MaxQubitAvailableTime(opRec.InputQubitMetrics); + double inputQubitsAvailableTime = MaxAvailableTime(opRec.InputQubitMetrics); double operationEndTime = Max( Max(maxReturnedQubitsAvailableTime, opRec.ReturnedQubitsAvailableTime), @@ -93,7 +101,8 @@ public void OnOperationEnd(object[] returnedQubitsTraceData) double[] metrics = StatisticsRecord( Depth : operationEndTime - opRec.MaxOperationStartTime, - StartTimeDifference: opRec.MaxOperationStartTime - opRec.MinOperationStartTime ); + StartTimeDifference: opRec.MaxOperationStartTime - opRec.MinOperationStartTime, + Width: qubitAvailabilityTime.GetMaxQubitId() - opRec.MaxQubitIdAtStart ); stats.AddSample(new CallGraphEdge(opRec.OperationName, callerName,opRec.FunctorSpecialization, caller.FunctorSpecialization), metrics); } @@ -105,33 +114,59 @@ public void OnOperationStart(HashedString name, OperationFunctor functorSpeciali opRec.FunctorSpecialization = functorSpecialization; opRec.OperationName = name; opRec.InputQubitMetrics = Utils.UnboxAs(qubitsTraceData); - opRec.MaxOperationStartTime = QubitsMetricsUtils.MaxQubitAvailableTime(opRec.InputQubitMetrics); - opRec.MinOperationStartTime = QubitsMetricsUtils.MinQubitAvailableTime(opRec.InputQubitMetrics); + opRec.MaxOperationStartTime = MaxAvailableTime(opRec.InputQubitMetrics); + opRec.MinOperationStartTime = MinAvailableTime(opRec.InputQubitMetrics); + opRec.MaxQubitIdAtStart = qubitAvailabilityTime.GetMaxQubitId(); operationCallStack.Push(opRec); } + private double MinAvailableTime(IEnumerable qubitTimeMetrics) + { + Debug.Assert(qubitTimeMetrics != null); + double min = Double.MaxValue; + foreach (QubitTimeMetrics metric in qubitTimeMetrics) + { + min = Min(min, qubitAvailabilityTime[metric.QubitId]); + } + return min != Double.MaxValue ? min : 0; + } + + private double MaxAvailableTime(IEnumerable qubitTimeMetrics) + { + Debug.Assert(qubitTimeMetrics != null); + double max = 0; + foreach (QubitTimeMetrics metric in qubitTimeMetrics) + { + max = Max(max, qubitAvailabilityTime[metric.QubitId]); + } + return max; + } + public void OnPrimitiveOperation(int id, object[] qubitsTraceData, double primitiveOperationDuration) { - QubitTimeMetrics[] qubitsMetrics = Utils.UnboxAs(qubitsTraceData); - double startTime = QubitsMetricsUtils.MaxQubitAvailableTime(qubitsMetrics); - foreach (QubitTimeMetrics q in qubitsMetrics ) + IEnumerable qubitsMetrics = qubitsTraceData.Cast(); + + double startTime = MaxAvailableTime(qubitsMetrics); + foreach (QubitTimeMetrics q in qubitsMetrics) { - q.RecordQubitUsage(startTime, primitiveOperationDuration); + qubitAvailabilityTime[q.QubitId] = startTime + primitiveOperationDuration; } } public void OnRelease(object[] qubitsTraceData) { OperationCallRecord opRec = operationCallStack.Peek(); - QubitTimeMetrics[] qubitsMetrics = Utils.UnboxAs(qubitsTraceData); - opRec.ReleasedQubitsAvailableTime = Max(opRec.ReleasedQubitsAvailableTime, QubitsMetricsUtils.MaxQubitAvailableTime(qubitsMetrics)); + opRec.ReleasedQubitsAvailableTime = Max( + opRec.ReleasedQubitsAvailableTime, + MaxAvailableTime(qubitsTraceData.Cast())); } public void OnReturn(object[] qubitsTraceData, long qubitReleased) { OperationCallRecord opRec = operationCallStack.Peek(); - QubitTimeMetrics[] qubitsMetrics = Utils.UnboxAs(qubitsTraceData); - opRec.ReturnedQubitsAvailableTime = Max(opRec.ReturnedQubitsAvailableTime, QubitsMetricsUtils.MaxQubitAvailableTime(qubitsMetrics)); + opRec.ReturnedQubitsAvailableTime = Max( + opRec.ReturnedQubitsAvailableTime, + MaxAvailableTime(qubitsTraceData.Cast())); } /// diff --git a/src/Simulation/QCTraceSimulator/QCTraceSimulatorCore.cs b/src/Simulation/QCTraceSimulator/QCTraceSimulatorCore.cs index d89fae497ca..25eaef0f258 100644 --- a/src/Simulation/QCTraceSimulator/QCTraceSimulatorCore.cs +++ b/src/Simulation/QCTraceSimulator/QCTraceSimulatorCore.cs @@ -30,6 +30,13 @@ public class QCTraceSimulatorCoreConfiguration /// of the call graph. /// public uint CallStackDepthLimit = uint.MaxValue; + + /// + /// Controls if depth or width optimization is favored. + /// If set to true, resulting circuit is optimized for depth by discouraging qubit reuse. + /// If set to false, resulting circuit is optimized for width by encouraging qubit reuse. + /// + public bool OptimizeDepth = false; } /// @@ -101,7 +108,7 @@ QCTraceSimulatorCoreConfiguration config tracingDataInQubitsIsNeeded = listenerNeedsTracingData[i] || tracingDataInQubitsIsNeeded; } - qubitManager = (IQubitManager) new TraceableQubitManager(qubitDataInitializers); + qubitManager = (IQubitManager) new TraceableQubitManager(qubitDataInitializers, configuration.OptimizeDepth); callStackDepthLimit = Math.Max( 1, configuration.CallStackDepthLimit ); } diff --git a/src/Simulation/QCTraceSimulator/QubitAvailabilityTimeTracker.cs b/src/Simulation/QCTraceSimulator/QubitAvailabilityTimeTracker.cs new file mode 100644 index 00000000000..4ca9cc42289 --- /dev/null +++ b/src/Simulation/QCTraceSimulator/QubitAvailabilityTimeTracker.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime +{ + /// + /// Tracks time when qubits were last used and therefore, time when qubits become available. + /// Tracking is done by qubit id, which survives during reuse of qubit. + /// + internal class QubitAvailabilityTimeTracker + { + /// + /// Availability time of all qubits starts at 0. + /// + private double DefaultAvailabilityTime = 0.0; + + /// + /// This tracks time when a qubit was last used, indexed by qubit id. + /// + private List QubitAvailableAt; + + /// + /// Maximum qubit id seen so far. + /// + private long MaxQubitId = -1; + + internal QubitAvailabilityTimeTracker(int initialCapacity, double defaultAvailabilityTime) + { + DefaultAvailabilityTime = defaultAvailabilityTime; + QubitAvailableAt = new List(initialCapacity); + } + + internal double this[long qubitId] + { + get + { + if (qubitId < QubitAvailableAt.Count) + { + return QubitAvailableAt[(int)qubitId]; + } + return DefaultAvailabilityTime; + } + set + { + if (qubitId == QubitAvailableAt.Count) + { + QubitAvailableAt.Add(value); + return; + } + else if (qubitId >= int.MaxValue) + { + throw new IndexOutOfRangeException("Too many qubits to track."); + } + else if (qubitId > QubitAvailableAt.Count) + { + QubitAvailableAt.AddRange(Enumerable.Repeat(DefaultAvailabilityTime, (int)qubitId - QubitAvailableAt.Count + 1)); + } + QubitAvailableAt[(int)qubitId] = value; + } + } + + internal long GetMaxQubitId() + { + return MaxQubitId; + } + + internal void MarkQubitIdUsed(long qubitId) + { + MaxQubitId = System.Math.Max(MaxQubitId, qubitId); + } + + } + +} diff --git a/src/Simulation/QCTraceSimulator/QubitMetrics.cs b/src/Simulation/QCTraceSimulator/QubitMetrics.cs index ca7b1a0bd9c..6e2e7a5478d 100644 --- a/src/Simulation/QCTraceSimulator/QubitMetrics.cs +++ b/src/Simulation/QCTraceSimulator/QubitMetrics.cs @@ -8,24 +8,14 @@ namespace Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime /// public class QubitTimeMetrics { - /// - /// Time when the qubit becomes available - /// - public double AvailableAt { get; private set; } = 0; + // TODO: Qubit Ids are already available in qubits, but DepthCounter doesn't have access to it + // in OnPrimitiveOperation because it's not part of IQCTraceSimulatorListener interface. + // Consider changing architecture to pass qubits rather than metrics in IQCTraceSimulatorListener. + public long QubitId { get; } - public QubitTimeMetrics() + public QubitTimeMetrics(long qubitId) { - } - - /// Beginning of the execution of the primitive operation on the qubit - /// Duration of the primitive operation - public void RecordQubitUsage(double timeAt, double duration) - { - if (timeAt < AvailableAt) - { - throw new QubitTimeMetricsException(); - } - AvailableAt = timeAt + duration; + QubitId = qubitId; } } -} \ No newline at end of file +} diff --git a/src/Simulation/QCTraceSimulator/QubitsMetricsUtils.cs b/src/Simulation/QCTraceSimulator/QubitsMetricsUtils.cs deleted file mode 100644 index 4440e6329ce..00000000000 --- a/src/Simulation/QCTraceSimulator/QubitsMetricsUtils.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime -{ - - using System; - using System.Diagnostics; - - class QubitsMetricsUtils - { - public static double MaxQubitAvailableTime(QubitTimeMetrics[] qubitMetrics) - { - Debug.Assert(qubitMetrics != null); - if (qubitMetrics.Length == 0) - { - return 0; // if there are no qubits to depend on, we start at the beginning e.g. 0 - } - - double maxStartTime = 0; - for (int i = 0; i < qubitMetrics.Length; ++i) - { - maxStartTime = Math.Max(maxStartTime, qubitMetrics[i].AvailableAt); - } - return maxStartTime; - } - - public static double MinQubitAvailableTime(QubitTimeMetrics[] qubitMetrics) - { - Debug.Assert(qubitMetrics != null); - if (qubitMetrics.Length == 0) - { - return 0; // if there are no qubits to depend on, we start at the beginning e.g. 0 - } - - double minStartTime = double.MaxValue; - for (int i = 0; i < qubitMetrics.Length; ++i) - { - minStartTime = Math.Min(minStartTime, qubitMetrics[i].AvailableAt); - } - return minStartTime; - } - } -} \ No newline at end of file diff --git a/src/Simulation/QCTraceSimulator/TraceableQubitManager.cs b/src/Simulation/QCTraceSimulator/TraceableQubitManager.cs index f11d9042056..f386485f527 100644 --- a/src/Simulation/QCTraceSimulator/TraceableQubitManager.cs +++ b/src/Simulation/QCTraceSimulator/TraceableQubitManager.cs @@ -23,8 +23,8 @@ class TraceableQubitManager : QubitManagerTrackingScope /// The qubit manager makes sure that trace data array for qubits /// is initialized with objects created by qubitTraceDataInitializers callbacks /// - public TraceableQubitManager( Func[] qubitTraceDataInitializers ) - : base(NumQubits, mayExtendCapacity : true, disableBorrowing : false) + public TraceableQubitManager( Func[] qubitTraceDataInitializers, bool optimizeDepth ) + : base(NumQubits, mayExtendCapacity : true, disableBorrowing : false, encourageReuse: !optimizeDepth) { this.qubitTraceDataInitializers = qubitTraceDataInitializers.Clone() as Func[]; diff --git a/src/Simulation/QCTraceSimulator/WidthCounter.cs b/src/Simulation/QCTraceSimulator/WidthCounter.cs index 5e7f5f43175..3760c7f0722 100644 --- a/src/Simulation/QCTraceSimulator/WidthCounter.cs +++ b/src/Simulation/QCTraceSimulator/WidthCounter.cs @@ -45,7 +45,7 @@ public WidthCounter( IDoubleStatistic[] statisticsToCollect = null ) AddToCallStack(CallGraphEdge.CallGraphRootHashed, OperationFunctor.Body); } - public class Metrics + public static class Metrics { public const string InputWidth = "InputWidth"; public const string ExtraWidth = "ExtraWidth"; diff --git a/src/Simulation/Simulators.Tests/ResourcesEstimatorTests.cs b/src/Simulation/Simulators.Tests/ResourcesEstimatorTests.cs index 0a60328484d..19cec9f9f7d 100644 --- a/src/Simulation/Simulators.Tests/ResourcesEstimatorTests.cs +++ b/src/Simulation/Simulators.Tests/ResourcesEstimatorTests.cs @@ -90,7 +90,7 @@ public void ToTSVTest() Assert.NotNull(data); var rows = data.Split('\n'); - Assert.Equal(9, rows.Length); + Assert.Equal(10, rows.Length); var cols = rows[0].Split('\t'); Assert.Equal("Metric", cols[0].Trim()); @@ -120,24 +120,41 @@ public void DepthDifferentQubitsTest() } /// - /// Documents that the width and depth statistics reflect independent lower + /// Documents that the QubitCount and Depth statistics reflect independent lower /// bounds for each (two T gates cannot be combined into a circuit of depth - /// one and width one). + /// one and width one). Width on the other hand is compatible with Depth. /// [Fact] public void DepthVersusWidthTest() { - var sim = new ResourcesEstimator(); - + // Operation to execute: // using(q = Qubit()) { T(q); } using(q = Qubit()) { T(q); } (yes, twice) - DepthVersusWidth.Run(sim).Wait(); - var data = sim.Data; + // First run with width optimization + DataTable data = RunDepthVersusWidthTest(optimizeDepth: false); Assert.Equal(2.0, data.Rows.Find("T")["Sum"]); + Assert.Equal(1.0, data.Rows.Find("QubitCount")["Sum"]); Assert.Equal(1.0, data.Rows.Find("Width")["Sum"]); + Assert.Equal(2.0, data.Rows.Find("Depth")["Sum"]); + + // Now run with depth optimization + data = RunDepthVersusWidthTest(optimizeDepth: true); + Assert.Equal(2.0, data.Rows.Find("T")["Sum"]); + Assert.Equal(1.0, data.Rows.Find("QubitCount")["Sum"]); + Assert.Equal(2.0, data.Rows.Find("Width")["Sum"]); Assert.Equal(1.0, data.Rows.Find("Depth")["Sum"]); } + private DataTable RunDepthVersusWidthTest(bool optimizeDepth) + { + QCTraceSimulators.QCTraceSimulatorConfiguration config = ResourcesEstimator.RecommendedConfig(); + config.OptimizeDepth = optimizeDepth; + var sim = new ResourcesEstimator(config); + + DepthVersusWidth.Run(sim).Wait(); + return sim.Data; + } + /// /// Verifies that for multiple separately traced operations, the final /// statistics are cumulative. @@ -155,7 +172,7 @@ public void VerifyTracingMultipleOperationsTest() Assert.Equal(1.0, data1.Rows.Find("T")["Sum"]); Assert.Equal(0.0, data1.Rows.Find("R")["Sum"]); Assert.Equal(0.0, data1.Rows.Find("Measure")["Sum"]); - Assert.Equal(2.0, data1.Rows.Find("Width")["Sum"]); + Assert.Equal(2.0, data1.Rows.Find("QubitCount")["Sum"]); Operation_2_of_2.Run(sim).Wait(); DataTable data2 = sim.Data; @@ -166,14 +183,14 @@ public void VerifyTracingMultipleOperationsTest() Assert.Equal(1.0 + 0.0, data2.Rows.Find("T")["Sum"]); Assert.Equal(0.0 + 1.0, data2.Rows.Find("R")["Sum"]); Assert.Equal(0.0 + 1.0, data2.Rows.Find("Measure")["Sum"]); - Assert.Equal(2.0 + 3.0, data2.Rows.Find("Width")["Sum"]); - Assert.Equal(System.Math.Max(2.0, 3.0), data2.Rows.Find("Width")["Max"]); + Assert.Equal(2.0 + 3.0, data2.Rows.Find("QubitCount")["Sum"]); + Assert.Equal(System.Math.Max(2.0, 3.0), data2.Rows.Find("QubitCount")["Max"]); // Run again to confirm two operations isn't the limit! VerySimpleEstimate.Run(sim).Wait(); DataTable data3 = sim.Data; - Assert.Equal(2.0 + 3.0 + 3.0, data3.Rows.Find("Width")["Sum"]); - Assert.Equal(3.0, data3.Rows.Find("Width")["Max"]); + Assert.Equal(2.0 + 3.0 + 3.0, data3.Rows.Find("QubitCount")["Sum"]); + Assert.Equal(3.0, data3.Rows.Find("QubitCount")["Max"]); } } } diff --git a/src/Simulation/Simulators/QCTraceSimulator/QCTraceSimulator.cs b/src/Simulation/Simulators/QCTraceSimulator/QCTraceSimulator.cs index 9b5ea578132..5ba05575fe9 100644 --- a/src/Simulation/Simulators/QCTraceSimulator/QCTraceSimulator.cs +++ b/src/Simulation/Simulators/QCTraceSimulator/QCTraceSimulator.cs @@ -98,6 +98,14 @@ public class QCTraceSimulatorConfiguration /// public bool UseWidthCounter = false; + /// + /// Controls if depth or width optimization is favored. + /// If set to true, resulting circuit is optimized for depth by discouraging qubit reuse. + /// If set to false, resulting circuit is optimized for width be encouraging qubit reuse. + /// Optimization is only limited to reuse of qubits after they are released by user code. + /// + public bool OptimizeDepth = false; + /// /// Specifies the time it takes to execute each gate. /// In other words, specifies the depth of each primitive operation. diff --git a/src/Simulation/Simulators/QCTraceSimulator/QCTraceSimulatorImpl.cs b/src/Simulation/Simulators/QCTraceSimulator/QCTraceSimulatorImpl.cs index 83a978f8461..f09104eb73c 100644 --- a/src/Simulation/Simulators/QCTraceSimulator/QCTraceSimulatorImpl.cs +++ b/src/Simulation/Simulators/QCTraceSimulator/QCTraceSimulatorImpl.cs @@ -80,9 +80,9 @@ public QCTraceSimulatorImpl(QCTraceSimulatorConfiguration config) gateTimes[i] = config.TraceGateTimes[(PrimitiveOperationsGroups)i]; } - tCoreConfig = new QCTraceSimulatorCoreConfiguration - { - ThrowOnUnconstrainedMeasurement = configuration.ThrowOnUnconstrainedMeasurement + tCoreConfig = new QCTraceSimulatorCoreConfiguration { + ThrowOnUnconstrainedMeasurement = configuration.ThrowOnUnconstrainedMeasurement, + OptimizeDepth = configuration.OptimizeDepth }; tCoreConfig.CallStackDepthLimit = config.CallStackDepthLimit; diff --git a/src/Simulation/Simulators/ResourcesEstimator/ResourcesEstimator.cs b/src/Simulation/Simulators/ResourcesEstimator/ResourcesEstimator.cs index d08847898cf..84fc8ca8f2d 100644 --- a/src/Simulation/Simulators/ResourcesEstimator/ResourcesEstimator.cs +++ b/src/Simulation/Simulators/ResourcesEstimator/ResourcesEstimator.cs @@ -75,9 +75,9 @@ public ResourcesEstimator(QCTraceSimulatorConfiguration config) : base(config) { return null; } - else if (name == MetricsNames.WidthCounter.ExtraWidth) - { - return "Width"; + else if (name == MetricsNames.WidthCounter.ExtraWidth) + { + return "QubitCount"; } else {