From a50c16a42f5e0f09aff0479bc15e4e1c7bcb5773 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Wed, 10 Jan 2024 00:31:04 +0100 Subject: [PATCH 01/49] wip --- .../Placement/PlacementAttribute.cs | 12 + .../Placement/ResourceOptimizedPlacement.cs | 27 ++ .../ResourceOptimizedPlacementOptions.cs | 81 ++++++ .../ActivationCountPlacementDirector.cs | 1 + .../ResourceOptimizedPlacementDirector.cs | 273 ++++++++++++++++++ 5 files changed, 394 insertions(+) create mode 100644 src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs create mode 100644 src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs create mode 100644 src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs diff --git a/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs b/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs index 8d0d3aa36d..ae10b343d0 100644 --- a/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs +++ b/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs @@ -99,4 +99,16 @@ public sealed class SiloRoleBasedPlacementAttribute : PlacementAttribute base(SiloRoleBasedPlacement.Singleton) { } } + + /// + /// Marks a grain class as using the policy. + /// + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class ResourceOptimizedPlacementAttribute : PlacementAttribute + { + public ResourceOptimizedPlacementAttribute() : + base(ResourceOptimizedPlacement.Singleton) + { } + } } diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs new file mode 100644 index 0000000000..a92c81dcf5 --- /dev/null +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -0,0 +1,27 @@ +namespace Orleans.Runtime; + +/// +/// A placement strategy which attempts to achieve approximately even load based on cluster resources. +/// +/// +/// It assigns weights to runtime statistics to prioritize different properties and calculates a normalized score for each silo. +/// The silo with the highest score is chosen for placing the activation. +/// Normalization ensures that each property contributes proportionally to the overall score. +/// You can adjust the weights based on your specific requirements and priorities for load balancing. +/// In addition to normalization, an online adaptive filter provides a smoothing effect +/// (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. +/// This contributes to avoiding resource saturation on the silos and especially newly joined silos. +/// Details of the properties used to make the placement decisions and their default values are given below: +/// +/// Cpu usage: The default weight (0.3), indicates that CPU usage is important but not the sole determinant in placement decisions. +/// Available memory: The default weight (0.4), emphasizes the importance of nodes with ample available memory. +/// Memory usage: Is important for understanding the current load on a node. The default weight (0.2), ensures consideration without making it overly influential. +/// Total physical memory: Represents the overall capacity of a node. The default weight (0.1), contributes to a more long-term resource planning perspective. +/// +/// This placement strategy is configured by adding the attribute to a grain. +/// +[Immutable, GenerateSerializer, SuppressReferenceTracking] +internal sealed class ResourceOptimizedPlacement : PlacementStrategy +{ + internal static readonly ResourceOptimizedPlacement Singleton = new(); +} diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs new file mode 100644 index 0000000000..eb058362d3 --- /dev/null +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.Options; + +namespace Orleans.Runtime.Configuration.Options; + +/// +/// Settings which regulate the placement of grains across a cluster when using . +/// +public sealed class ResourceOptimizedPlacementOptions +{ + /// + /// The importance of the CPU utilization by the silo. + /// + /// Expressed as percentage. + public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_CPU_USAGE_WEIGHT = 0.3f; + + /// + /// The importance of the amount of memory available to the silo. + /// + /// Expressed as percentage. + public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.4f; + + /// + /// The importance of the used memory by the silo. + /// + /// Expressed as percentage. + public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.2f; + + /// + /// The importance of the total physical memory of the silo. + /// + /// Expressed as percentage. + public float TotalPhysicalMemoryWeight { get; set; } = DEFAULT_TOTAL_PHYSICAL_MEMORY_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_TOTAL_PHYSICAL_MEMORY_WEIGHT = 0.1f; + + /// + /// The specified margin for which: if two silos (one of them being the local silo), have a utilization score that should be considered "the same" within this margin. + /// + /// When this value is 0, then the policy will always favor the silo with the higher utilization score, even if that silo is remote to the current pending activation. + /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . + /// + /// + /// Expressed as percentage. + public float LocalSiloPreferenceMargin { get; set; } + /// + /// The default value of . + /// + public const float DEFAULT_LOCAL_SILO_PREFERENCE_MARGIN = 0.05f; +} + +internal sealed class ResourceOptimizedPlacementOptionsValidator + (IOptions options) : IConfigurationValidator +{ + private readonly ResourceOptimizedPlacementOptions _options = options.Value; + + public void ValidateConfiguration() + { + if (_options.CpuUsageWeight + + _options.MemoryUsageWeight + + _options.AvailableMemoryWeight + + _options.TotalPhysicalMemoryWeight != 1.0f) + { + throw new OrleansConfigurationException( + $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1.0"); + } + } +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/ActivationCountPlacementDirector.cs b/src/Orleans.Runtime/Placement/ActivationCountPlacementDirector.cs index 1384b1c0b6..f07ca8f19e 100644 --- a/src/Orleans.Runtime/Placement/ActivationCountPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ActivationCountPlacementDirector.cs @@ -9,6 +9,7 @@ namespace Orleans.Runtime.Placement { + internal class ActivationCountPlacementDirector : RandomPlacementDirector, ISiloStatisticsChangeListener, IPlacementDirector { private class CachedLocalStat diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs new file mode 100644 index 0000000000..3a66506c06 --- /dev/null +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Orleans.Runtime.Configuration.Options; + +namespace Orleans.Runtime.Placement; + +internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener +{ + readonly record struct ResourceStatistics( + float? CpuUsage, + float? AvailableMemory, + long? MemoryUsage, + long? TotalPhysicalMemory, + bool IsOverloaded); + + Task _cachedLocalSilo; + readonly SiloAddress _localAddress; + readonly ILocalSiloDetails _localSiloDetails; + readonly ResourceOptimizedPlacementOptions _options; + readonly ConcurrentDictionary siloStatistics = []; + + readonly DualModeKalmanFilter _cpuUsageFilter = new(); + readonly DualModeKalmanFilter _availableMemoryFilter = new(); + readonly DualModeKalmanFilter _memoryUsageFilter = new(); + + public ResourceOptimizedPlacementDirector( + ILocalSiloDetails localSiloDetails, + DeploymentLoadPublisher deploymentLoadPublisher, + IOptions options) + { + _localSiloDetails = localSiloDetails; + _options = options.Value; + deploymentLoadPublisher?.SubscribeToStatisticsChangeEvents(this); + } + + public Task OnAddActivation(PlacementStrategy strategy, PlacementTarget target, IPlacementContext context) + { + var compatibleSilos = context.GetCompatibleSilos(target); + + if (IPlacementDirector.GetPlacementHint(target.RequestContextData, compatibleSilos) is { } placementHint) + { + return Task.FromResult(placementHint); + } + + if (compatibleSilos.Length == 0) + { + throw new SiloUnavailableException($"Cannot place grain with Id = [{target.GrainIdentity}], because there are no compatible silos."); + } + + if (compatibleSilos.Length == 1) + { + return Task.FromResult(compatibleSilos[0]); + } + + if (siloStatistics.IsEmpty) + { + return Task.FromResult(compatibleSilos[Random.Shared.Next(compatibleSilos.Length)]); + } + + var selectedSilo = GetSiloWithHighestScore(compatibleSilos); + + + return Task.FromResult(selectedSilo); + } + + SiloAddress GetSiloWithHighestScore(SiloAddress[] compatibleSilos) + { + List> relevantSilos = []; + foreach (var silo in compatibleSilos) + { + if (siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) + { + relevantSilos.Add(new(silo, stats)); + } + } + + int chooseFrom = (int)Math.Ceiling(Math.Sqrt(relevantSilos.Count)); + Dictionary chooseFromSilos = []; + + while (chooseFromSilos.Count < chooseFrom) + { + int index = Random.Shared.Next(relevantSilos.Count); + var pickedSilo = relevantSilos[index]; + + relevantSilos.RemoveAt(index); + + float score = CalculateScore(pickedSilo.Value); + chooseFromSilos.Add(pickedSilo.Key, score); + } + + return chooseFromSilos.OrderByDescending(kv => kv.Value).FirstOrDefault().Key; + } + + float CalculateScore(ResourceStatistics stats) + { + float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; + + if (stats.TotalPhysicalMemory.HasValue) + { + float normalizedAvailableMemory = stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / stats.TotalPhysicalMemory.Value : 0f; + float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / stats.TotalPhysicalMemory.Value : 0f; + float normalizedTotalPhysicalMemory = stats.TotalPhysicalMemory.HasValue ? stats.TotalPhysicalMemory.Value / (1024 * 1024 * 1024) : 0f; + + return _options.CpuUsageWeight * normalizedCpuUsage + + _options.AvailableMemoryWeight * normalizedAvailableMemory + + _options.MemoryUsageWeight * normalizedMemoryUsage + + _options.TotalPhysicalMemoryWeight * normalizedTotalPhysicalMemory; + } + + return _options.CpuUsageWeight * normalizedCpuUsage; + } + + public void RemoveSilo(SiloAddress address) + => siloStatistics.TryRemove(address, out _); + + public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeStatistics statistics) + => siloStatistics.AddOrUpdate( + key: address, + addValue: new ResourceStatistics( + statistics.CpuUsage, + statistics.AvailableMemory, + statistics.MemoryUsage, + statistics.TotalPhysicalMemory, + statistics.IsOverloaded), + updateValueFactory: (_, _) => + { + float estimatedCpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); + float estimatedAvailableMemory = _availableMemoryFilter.Filter(statistics.AvailableMemory); + long estimatedMemoryUsage = _memoryUsageFilter.Filter(statistics.MemoryUsage); + + return new ResourceStatistics( + estimatedCpuUsage, + estimatedAvailableMemory, + estimatedMemoryUsage, + statistics.TotalPhysicalMemory, + statistics.IsOverloaded); + }); + + sealed class DualModeKalmanFilter where T : unmanaged, INumber + { + readonly KalmanFilter _slowFilter = new(T.Zero); + readonly KalmanFilter _fastFilter = new(T.CreateChecked(0.01)); + + FilterRegime _regime = FilterRegime.Slow; + + enum FilterRegime + { + Slow, + Fast + } + + public T Filter(T? measurement) + { + T _measurement = measurement ?? T.Zero; + + T slowEstimate = _slowFilter.Filter(_measurement); + T fastEstimate = _fastFilter.Filter(_measurement); + + if (_measurement > slowEstimate) + { + if (_regime == FilterRegime.Slow) + { + // since we now got a measurement we can use it to set the filter's 'estimate', + // in addition we set the 'error covariance' to 0, indicating we want to fully + // trust the measurement (now the new 'estimate') to reach the actual signal asap. + _fastFilter.SetState(_measurement, T.Zero); + + // ensure we recalculate since we changed the 'error covariance' + fastEstimate = _fastFilter.Filter(_measurement); + + _regime = FilterRegime.Fast; + } + + return fastEstimate; + } + else + { + if (_regime == FilterRegime.Fast) + { + // since the slow filter will accumulate the changes, we want to reset its state + // so that it aligns with the current peak of the fast filter so we get a slower + // decay that is always aligned with the latest fast filter state and not the overall + // accumulated state of the whole signal over its lifetime. + _slowFilter.SetState(_fastFilter.PriorEstimate, _fastFilter.PriorErrorCovariance); + + // ensure we recalculate since we changed both the 'estimate' and 'error covariance' + slowEstimate = _slowFilter.Filter(_measurement); + + _regime = FilterRegime.Slow; + } + + return slowEstimate; + } + } + + sealed class KalmanFilter(T processNoiseCovariance) + { + readonly T _processNoiseCovariance = processNoiseCovariance; + + public T PriorEstimate { get; private set; } = T.Zero; + public T PriorErrorCovariance { get; private set; } = T.One; + + public void SetState(T estimate, T errorCovariance) + { + PriorEstimate = estimate; + PriorErrorCovariance = errorCovariance; + } + + public T Filter(T measurement) + { + #region Prediction Step + + #region Formula + // ^x_k = A * x_k-1 + B * u_k + #endregion + + #region Simplification + // ^x_k = x_k-1 + #endregion + + #region Explanation + // As every resource statistics is a single entity in our case, we have a 1-dimensional signal problem, so every entity in our model is a scalar, not a matrix. + // uk is the control signal which incorporate external information about how the system is expected to behave between measurements. We have no idea how the CPU usage is going to behave between measurements, therefor we have no control signal, so uk = 0. + // B is the control input matrix, but since uk = 0 this means we don't need to bother with it. + // A is the state transition matrix, and we established that we have a 1-dimensional signal problem, so this is now a scalar. Same as with uk, we have no idea how the CPU usage is going to transition, therefor A = 1. + // We just established that A = 1, and since A is a unitary scalar, this means AT which is the transpose of A, is AT = 1. + #endregion + + T estimate = PriorEstimate; + T errorCovariance = PriorErrorCovariance + _processNoiseCovariance; + + #endregion + + #region Correction Step + + #region Formulas + // * K_k = (P_k * H_T) / (H * P_k * H_T + R) + // * ^x_k = x_k + K_k * (z_k - H * x_k) + // * ^P_k = (I - K_k * H) * P_k; + #endregion + + #region Simplifications + // * K_k = P_k / (P_k + 1); + // * ^x_k = x_k + K_k * (z_k - x_k) + // * ^P_k = (1 - K_k) * P_k; + #endregion + + #region Explanation + // Same as with the prediction, we deal only with scalars, not matrices. + // H is the observation matrix, which acts as a bridge between the internal model A, and the external measurements R. We can set H = 1, which indicates that the measurements directly correspond to the state variables without any transformations or scaling factors. + // Since H = 1, it follows that HT = 1. + // R is the measurement covariance matrix, which represents the influence of the measurements relative to the predicted state. We set this value to R = 1, which indicates that all measurements are assumed to have the same level of uncertainty, and there is no correlation between different measurements. + #endregion + + T gain = errorCovariance / (errorCovariance + T.One); + T newEstimate = estimate + gain * (measurement - estimate); + T newErrorCovariance = (T.One - gain) * errorCovariance; + + PriorEstimate = newEstimate; + PriorErrorCovariance = newErrorCovariance; + + #endregion + + return newEstimate; + } + } + } +} From 804d76703890ec28910fb9868825d7c4a03cbd0c Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Wed, 10 Jan 2024 22:22:11 +0100 Subject: [PATCH 02/49] wip --- .../ResourceOptimizedPlacementDirector.cs | 113 ++++++------------ 1 file changed, 38 insertions(+), 75 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 3a66506c06..00b0cdbd43 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -11,15 +11,10 @@ namespace Orleans.Runtime.Placement; internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener { - readonly record struct ResourceStatistics( - float? CpuUsage, - float? AvailableMemory, - long? MemoryUsage, - long? TotalPhysicalMemory, - bool IsOverloaded); + readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory); Task _cachedLocalSilo; - readonly SiloAddress _localAddress; + readonly ILocalSiloDetails _localSiloDetails; readonly ResourceOptimizedPlacementOptions _options; readonly ConcurrentDictionary siloStatistics = []; @@ -62,18 +57,21 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return Task.FromResult(compatibleSilos[Random.Shared.Next(compatibleSilos.Length)]); } - var selectedSilo = GetSiloWithHighestScore(compatibleSilos); - + var bestCandidate = GetBestSiloCandidate(compatibleSilos); + if (IsLocalSiloPreferable(context, compatibleSilos, bestCandidate.Value)) + { + return _cachedLocalSilo ??= Task.FromResult(context.LocalSilo); + } - return Task.FromResult(selectedSilo); + return Task.FromResult(bestCandidate.Key); } - SiloAddress GetSiloWithHighestScore(SiloAddress[] compatibleSilos) + KeyValuePair GetBestSiloCandidate(SiloAddress[] compatibleSilos) { List> relevantSilos = []; foreach (var silo in compatibleSilos) { - if (siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) + if (siloStatistics.TryGetValue(silo, out var stats)) { relevantSilos.Add(new(silo, stats)); } @@ -93,7 +91,7 @@ SiloAddress GetSiloWithHighestScore(SiloAddress[] compatibleSilos) chooseFromSilos.Add(pickedSilo.Key, score); } - return chooseFromSilos.OrderByDescending(kv => kv.Value).FirstOrDefault().Key; + return chooseFromSilos.OrderByDescending(kv => kv.Value).First(); } float CalculateScore(ResourceStatistics stats) @@ -115,6 +113,28 @@ float CalculateScore(ResourceStatistics stats) return _options.CpuUsageWeight * normalizedCpuUsage; } + bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) + { + if (context.LocalSiloStatus != SiloStatus.Active || + !compatibleSilos.Contains(context.LocalSilo)) + { + return false; + } + + if (siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) + { + float localScore = CalculateScore(localStats); + float localScoreMargin = localScore * _options.LocalSiloPreferenceMargin; + + if (localScore + localScoreMargin >= bestCandidateScore) + { + return true; + } + } + + return false; + } + public void RemoveSilo(SiloAddress address) => siloStatistics.TryRemove(address, out _); @@ -125,8 +145,7 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta statistics.CpuUsage, statistics.AvailableMemory, statistics.MemoryUsage, - statistics.TotalPhysicalMemory, - statistics.IsOverloaded), + statistics.TotalPhysicalMemory), updateValueFactory: (_, _) => { float estimatedCpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); @@ -137,10 +156,10 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta estimatedCpuUsage, estimatedAvailableMemory, estimatedMemoryUsage, - statistics.TotalPhysicalMemory, - statistics.IsOverloaded); + statistics.TotalPhysicalMemory); }); + // details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ sealed class DualModeKalmanFilter where T : unmanaged, INumber { readonly KalmanFilter _slowFilter = new(T.Zero); @@ -165,15 +184,9 @@ public T Filter(T? measurement) { if (_regime == FilterRegime.Slow) { - // since we now got a measurement we can use it to set the filter's 'estimate', - // in addition we set the 'error covariance' to 0, indicating we want to fully - // trust the measurement (now the new 'estimate') to reach the actual signal asap. + _regime = FilterRegime.Fast; _fastFilter.SetState(_measurement, T.Zero); - - // ensure we recalculate since we changed the 'error covariance' fastEstimate = _fastFilter.Filter(_measurement); - - _regime = FilterRegime.Fast; } return fastEstimate; @@ -182,16 +195,9 @@ public T Filter(T? measurement) { if (_regime == FilterRegime.Fast) { - // since the slow filter will accumulate the changes, we want to reset its state - // so that it aligns with the current peak of the fast filter so we get a slower - // decay that is always aligned with the latest fast filter state and not the overall - // accumulated state of the whole signal over its lifetime. + _regime = FilterRegime.Slow; _slowFilter.SetState(_fastFilter.PriorEstimate, _fastFilter.PriorErrorCovariance); - - // ensure we recalculate since we changed both the 'estimate' and 'error covariance' slowEstimate = _slowFilter.Filter(_measurement); - - _regime = FilterRegime.Slow; } return slowEstimate; @@ -213,50 +219,9 @@ public void SetState(T estimate, T errorCovariance) public T Filter(T measurement) { - #region Prediction Step - - #region Formula - // ^x_k = A * x_k-1 + B * u_k - #endregion - - #region Simplification - // ^x_k = x_k-1 - #endregion - - #region Explanation - // As every resource statistics is a single entity in our case, we have a 1-dimensional signal problem, so every entity in our model is a scalar, not a matrix. - // uk is the control signal which incorporate external information about how the system is expected to behave between measurements. We have no idea how the CPU usage is going to behave between measurements, therefor we have no control signal, so uk = 0. - // B is the control input matrix, but since uk = 0 this means we don't need to bother with it. - // A is the state transition matrix, and we established that we have a 1-dimensional signal problem, so this is now a scalar. Same as with uk, we have no idea how the CPU usage is going to transition, therefor A = 1. - // We just established that A = 1, and since A is a unitary scalar, this means AT which is the transpose of A, is AT = 1. - #endregion - T estimate = PriorEstimate; T errorCovariance = PriorErrorCovariance + _processNoiseCovariance; - #endregion - - #region Correction Step - - #region Formulas - // * K_k = (P_k * H_T) / (H * P_k * H_T + R) - // * ^x_k = x_k + K_k * (z_k - H * x_k) - // * ^P_k = (I - K_k * H) * P_k; - #endregion - - #region Simplifications - // * K_k = P_k / (P_k + 1); - // * ^x_k = x_k + K_k * (z_k - x_k) - // * ^P_k = (1 - K_k) * P_k; - #endregion - - #region Explanation - // Same as with the prediction, we deal only with scalars, not matrices. - // H is the observation matrix, which acts as a bridge between the internal model A, and the external measurements R. We can set H = 1, which indicates that the measurements directly correspond to the state variables without any transformations or scaling factors. - // Since H = 1, it follows that HT = 1. - // R is the measurement covariance matrix, which represents the influence of the measurements relative to the predicted state. We set this value to R = 1, which indicates that all measurements are assumed to have the same level of uncertainty, and there is no correlation between different measurements. - #endregion - T gain = errorCovariance / (errorCovariance + T.One); T newEstimate = estimate + gain * (measurement - estimate); T newErrorCovariance = (T.One - gain) * errorCovariance; @@ -264,8 +229,6 @@ public T Filter(T measurement) PriorEstimate = newEstimate; PriorErrorCovariance = newErrorCovariance; - #endregion - return newEstimate; } } From 737c1ba89d088a2672a4aafaf086d6a4b73bd09f Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Wed, 10 Jan 2024 22:29:30 +0100 Subject: [PATCH 03/49] added neccessary serives to DefaultSiloServices --- src/Orleans.Runtime/Hosting/DefaultSiloServices.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs b/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs index bd9e25d0fa..ff7647befa 100644 --- a/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs +++ b/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs @@ -42,6 +42,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Orleans.Serialization.Internal; +using Orleans.Runtime.Configuration.Options; namespace Orleans.Hosting { @@ -190,6 +191,7 @@ internal static void AddDefaultServices(ISiloBuilder builder) // Placement services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -207,6 +209,7 @@ internal static void AddDefaultServices(ISiloBuilder builder) services.AddPlacementDirector(); services.AddPlacementDirector(); services.AddPlacementDirector(); + services.AddPlacementDirector(); // Versioning services.TryAddSingleton(); @@ -298,6 +301,7 @@ internal static void AddDefaultServices(ISiloBuilder builder) services.ConfigureFormatter(); services.ConfigureFormatter(); services.ConfigureFormatter(); + services.ConfigureFormatter(); services.ConfigureFormatter(); services.ConfigureFormatter(); services.ConfigureFormatter(); From 621ccf4dbfd65cdcf8ce7fb8d53305208f83cd57 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 00:42:37 +0100 Subject: [PATCH 04/49] wip --- .../Placement/ResourceOptimizedPlacement.cs | 8 --- .../ResourceOptimizedPlacementOptions.cs | 65 +++++++++++-------- .../ResourceOptimizedPlacementDirector.cs | 28 +++++--- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs index a92c81dcf5..22ab15503e 100644 --- a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -11,16 +11,8 @@ namespace Orleans.Runtime; /// In addition to normalization, an online adaptive filter provides a smoothing effect /// (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. /// This contributes to avoiding resource saturation on the silos and especially newly joined silos. -/// Details of the properties used to make the placement decisions and their default values are given below: -/// -/// Cpu usage: The default weight (0.3), indicates that CPU usage is important but not the sole determinant in placement decisions. -/// Available memory: The default weight (0.4), emphasizes the importance of nodes with ample available memory. -/// Memory usage: Is important for understanding the current load on a node. The default weight (0.2), ensures consideration without making it overly influential. -/// Total physical memory: Represents the overall capacity of a node. The default weight (0.1), contributes to a more long-term resource planning perspective. -/// /// This placement strategy is configured by adding the attribute to a grain. /// -[Immutable, GenerateSerializer, SuppressReferenceTracking] internal sealed class ResourceOptimizedPlacement : PlacementStrategy { internal static readonly ResourceOptimizedPlacement Singleton = new(); diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index eb058362d3..2a53303291 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Options; namespace Orleans.Runtime.Configuration.Options; @@ -10,51 +11,41 @@ public sealed class ResourceOptimizedPlacementOptions /// /// The importance of the CPU utilization by the silo. /// - /// Expressed as percentage. + /// Valid range of values are [0-1] public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_CPU_USAGE_WEIGHT = 0.3f; + public const float DEFAULT_CPU_USAGE_WEIGHT = 0.4f; /// - /// The importance of the amount of memory available to the silo. + /// The importance of the memory utilization by the silo. /// - /// Expressed as percentage. - public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; - /// - /// The default value of . - /// - public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.4f; - - /// - /// The importance of the used memory by the silo. - /// - /// Expressed as percentage. + /// Valid range of values are [0-1] public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.2f; + public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.4f; /// - /// The importance of the total physical memory of the silo. + /// The importance of the available memory to the silo. /// - /// Expressed as percentage. - public float TotalPhysicalMemoryWeight { get; set; } = DEFAULT_TOTAL_PHYSICAL_MEMORY_WEIGHT; + /// Valid range of values are [0-1] + public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; /// - /// The default value of . + /// The default value of . /// - public const float DEFAULT_TOTAL_PHYSICAL_MEMORY_WEIGHT = 0.1f; + public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.1f; /// - /// The specified margin for which: if two silos (one of them being the local silo), have a utilization score that should be considered "the same" within this margin. + /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. /// /// When this value is 0, then the policy will always favor the silo with the higher utilization score, even if that silo is remote to the current pending activation. /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// - /// Expressed as percentage. + /// Valid range of values are [0-1] public float LocalSiloPreferenceMargin { get; set; } /// /// The default value of . @@ -69,13 +60,33 @@ internal sealed class ResourceOptimizedPlacementOptionsValidator public void ValidateConfiguration() { - if (_options.CpuUsageWeight + - _options.MemoryUsageWeight + - _options.AvailableMemoryWeight + - _options.TotalPhysicalMemoryWeight != 1.0f) + if (_options.CpuUsageWeight < 0f || _options.CpuUsageWeight > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.CpuUsageWeight)); + } + + if (_options.MemoryUsageWeight < 0f || _options.MemoryUsageWeight > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.MemoryUsageWeight)); + } + + if (_options.AvailableMemoryWeight < 0f || _options.AvailableMemoryWeight > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.AvailableMemoryWeight)); + } + + if (_options.LocalSiloPreferenceMargin < 0f || _options.LocalSiloPreferenceMargin > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); + } + + if (_options.CpuUsageWeight + _options.MemoryUsageWeight + _options.AvailableMemoryWeight != 1f) { throw new OrleansConfigurationException( - $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1.0"); + $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); } + + static void ThrowOutOfRange(string propertyName) + => throw new OrleansConfigurationException($"{propertyName} must be inclusive between [0-1]"); } } \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 00b0cdbd43..4b54db599a 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -58,6 +58,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa } var bestCandidate = GetBestSiloCandidate(compatibleSilos); + if (IsLocalSiloPreferable(context, compatibleSilos, bestCandidate.Value)) { return _cachedLocalSilo ??= Task.FromResult(context.LocalSilo); @@ -91,23 +92,28 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa chooseFromSilos.Add(pickedSilo.Key, score); } - return chooseFromSilos.OrderByDescending(kv => kv.Value).First(); + var orderedByLowestScore = chooseFromSilos.OrderBy(kv => kv.Value); + + // there could be more than 1 silo that has the same score, we pick 1 of them randomly so that we dont continuously pick the first one. + var lowestScore = orderedByLowestScore.First().Value; + var shortListedSilos = orderedByLowestScore.TakeWhile(p => p.Value == lowestScore).ToList(); + var winningSilo = shortListedSilos[Random.Shared.Next(shortListedSilos.Count)]; + + return winningSilo; } float CalculateScore(ResourceStatistics stats) { float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; - if (stats.TotalPhysicalMemory.HasValue) + if (stats.TotalPhysicalMemory is { } physicalMemory && physicalMemory > 0) { - float normalizedAvailableMemory = stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / stats.TotalPhysicalMemory.Value : 0f; - float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / stats.TotalPhysicalMemory.Value : 0f; - float normalizedTotalPhysicalMemory = stats.TotalPhysicalMemory.HasValue ? stats.TotalPhysicalMemory.Value / (1024 * 1024 * 1024) : 0f; + float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / physicalMemory : 0f; + float normalizedAvailableMemory = stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f; return _options.CpuUsageWeight * normalizedCpuUsage + - _options.AvailableMemoryWeight * normalizedAvailableMemory + _options.MemoryUsageWeight * normalizedMemoryUsage + - _options.TotalPhysicalMemoryWeight * normalizedTotalPhysicalMemory; + _options.AvailableMemoryWeight * normalizedAvailableMemory; } return _options.CpuUsageWeight * normalizedCpuUsage; @@ -115,8 +121,7 @@ float CalculateScore(ResourceStatistics stats) bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) { - if (context.LocalSiloStatus != SiloStatus.Active || - !compatibleSilos.Contains(context.LocalSilo)) + if (context.LocalSiloStatus != SiloStatus.Active || !compatibleSilos.Contains(context.LocalSilo)) { return false; } @@ -124,9 +129,12 @@ bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSi if (siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) { float localScore = CalculateScore(localStats); + + float scoreDiff = Math.Abs(localScore - bestCandidateScore); + float localScoreMargin = localScore * _options.LocalSiloPreferenceMargin; - if (localScore + localScoreMargin >= bestCandidateScore) + if (localScore - localScoreMargin <= bestCandidateScore) { return true; } From 8990407d3dc8ef0b5e46d634685eb287b9c3a9ca Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 00:46:13 +0100 Subject: [PATCH 05/49] . --- .../Placement/ResourceOptimizedPlacementDirector.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 4b54db599a..d12e7fc073 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -102,6 +102,9 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return winningSilo; } + /// + /// Always returns a value [0-1] + /// float CalculateScore(ResourceStatistics stats) { float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; @@ -129,12 +132,9 @@ bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSi if (siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) { float localScore = CalculateScore(localStats); - float scoreDiff = Math.Abs(localScore - bestCandidateScore); - float localScoreMargin = localScore * _options.LocalSiloPreferenceMargin; - - if (localScore - localScoreMargin <= bestCandidateScore) + if (_options.LocalSiloPreferenceMargin >= scoreDiff) { return true; } From 76dfc7d572ef269c68f6e3a113941245efa68c68 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 00:59:42 +0100 Subject: [PATCH 06/49] fixing some validations --- .../ResourceOptimizedPlacementOptions.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 2a53303291..4e91dfb465 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -11,32 +11,32 @@ public sealed class ResourceOptimizedPlacementOptions /// /// The importance of the CPU utilization by the silo. /// - /// Valid range of values are [0-1] + /// Valid range of values are [0.00-1.00] public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_CPU_USAGE_WEIGHT = 0.4f; + public const float DEFAULT_CPU_USAGE_WEIGHT = 0.34f; /// /// The importance of the memory utilization by the silo. /// - /// Valid range of values are [0-1] + /// Valid range of values are [0.00-1.00] public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.4f; + public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.33f; /// /// The importance of the available memory to the silo. /// - /// Valid range of values are [0-1] + /// Valid range of values are [0.00-1.00] public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.1f; + public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.33f; /// /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. @@ -45,7 +45,7 @@ public sealed class ResourceOptimizedPlacementOptions /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// - /// Valid range of values are [0-1] + /// Valid range of values are [0.00-1.00] public float LocalSiloPreferenceMargin { get; set; } /// /// The default value of . @@ -80,7 +80,9 @@ public void ValidateConfiguration() ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); } - if (_options.CpuUsageWeight + _options.MemoryUsageWeight + _options.AvailableMemoryWeight != 1f) + if (Truncate(_options.CpuUsageWeight) + + Truncate(_options.MemoryUsageWeight) + + Truncate(_options.AvailableMemoryWeight) != 1) { throw new OrleansConfigurationException( $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); @@ -88,5 +90,8 @@ public void ValidateConfiguration() static void ThrowOutOfRange(string propertyName) => throw new OrleansConfigurationException($"{propertyName} must be inclusive between [0-1]"); + + static double Truncate(double value) + => Math.Floor(value * 100) / 100; } } \ No newline at end of file From 2cdbb098ee15c2dc4f33f70ea0f1c6c5f2a42e61 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 21:49:51 +0100 Subject: [PATCH 07/49] reincoorporated physical ram --- .../ResourceOptimizedPlacementOptions.cs | 44 +++++++++++---- .../ResourceOptimizedPlacementDirector.cs | 54 ++++++++++++------- 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 4e91dfb465..c7a8223b57 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -9,34 +9,57 @@ namespace Orleans.Runtime.Configuration.Options; public sealed class ResourceOptimizedPlacementOptions { /// - /// The importance of the CPU utilization by the silo. + /// The importance of the CPU usage by the silo. /// - /// Valid range of values are [0.00-1.00] + /// + /// A higher value results in the placement favoring silos with lower cpu usage. + /// Valid range is [0.00-1.00] + /// public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_CPU_USAGE_WEIGHT = 0.34f; + public const float DEFAULT_CPU_USAGE_WEIGHT = 0.4f; /// - /// The importance of the memory utilization by the silo. + /// The importance of the memory usage by the silo. /// - /// Valid range of values are [0.00-1.00] + /// + /// A higher value results in the placement favoring silos with lower memory usage. + /// Valid range is [0.00-1.00] + /// public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.33f; + public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.3f; /// /// The importance of the available memory to the silo. /// - /// Valid range of values are [0.00-1.00] + /// + /// A higher values results in the placement favoring silos with higher available memory. + /// Valid range is [0.00-1.00] + /// public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.33f; + public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.2f; + + /// + /// The importance of the physical memory to the silo. + /// + /// + /// A higher values results in the placement favoring silos with higher physical memory. + /// This may have an impact in clusters with resources distributed unevenly across silos. + /// Valid range is [0.00-1.00] + /// + public float PhysicalMemoryWeight { get; set; } = DEFAULT_PHYSICAL_MEMORY_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_PHYSICAL_MEMORY_WEIGHT = 0.1f; /// /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. @@ -45,7 +68,10 @@ public sealed class ResourceOptimizedPlacementOptions /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// - /// Valid range of values are [0.00-1.00] + /// + /// + /// Valid range is [0.00-1.00] + /// public float LocalSiloPreferenceMargin { get; set; } /// /// The default value of . diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index d12e7fc073..93df5f503f 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -23,6 +23,11 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I readonly DualModeKalmanFilter _availableMemoryFilter = new(); readonly DualModeKalmanFilter _memoryUsageFilter = new(); + /// + /// 1 / (1024 * 1024) + /// + const float physicalMemoryScalingFactor = 0.00000095367431640625f; + public ResourceOptimizedPlacementDirector( ILocalSiloDetails localSiloDetails, DeploymentLoadPublisher deploymentLoadPublisher, @@ -102,26 +107,6 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return winningSilo; } - /// - /// Always returns a value [0-1] - /// - float CalculateScore(ResourceStatistics stats) - { - float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; - - if (stats.TotalPhysicalMemory is { } physicalMemory && physicalMemory > 0) - { - float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / physicalMemory : 0f; - float normalizedAvailableMemory = stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f; - - return _options.CpuUsageWeight * normalizedCpuUsage + - _options.MemoryUsageWeight * normalizedMemoryUsage + - _options.AvailableMemoryWeight * normalizedAvailableMemory; - } - - return _options.CpuUsageWeight * normalizedCpuUsage; - } - bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) { if (context.LocalSiloStatus != SiloStatus.Active || !compatibleSilos.Contains(context.LocalSilo)) @@ -143,6 +128,35 @@ bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSi return false; } + /// + /// Always returns a value [0-1] + /// + /// + /// score = cpu_weight * (cpu_usage / 100) + + /// mem_usage_weight * (mem_usage / physical_mem) + + /// mem_avail_weight * [1 - (mem_avail / physical_mem)] + /// physical_mem_weight * (1 / (1024 * 1024 * physical_mem) + /// + /// physical_mem is represented in [MB] to keep the result within [0-1] in cases of silos having physical_mem less than [1GB] + float CalculateScore(ResourceStatistics stats) + { + float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; + + if (stats.TotalPhysicalMemory is { } physicalMemory && physicalMemory > 0) + { + float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / physicalMemory : 0f; + float normalizedAvailableMemory = 1 - (stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f); + float normalizedPhysicalMemory = physicalMemoryScalingFactor * physicalMemory; + + return _options.CpuUsageWeight * normalizedCpuUsage + + _options.MemoryUsageWeight * normalizedMemoryUsage + + _options.AvailableMemoryWeight * normalizedAvailableMemory + + _options.AvailableMemoryWeight * normalizedPhysicalMemory; + } + + return _options.CpuUsageWeight * normalizedCpuUsage; + } + public void RemoveSilo(SiloAddress address) => siloStatistics.TryRemove(address, out _); From ad4b5c28f748e7aa4c445d8421e7f27e620e1dfb Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 22:56:01 +0100 Subject: [PATCH 08/49] some tests for options and validator, and XML docs --- .../Placement/ResourceOptimizedPlacement.cs | 13 ++-- .../ResourceOptimizedPlacementOptions.cs | 11 ++- .../General/PlacementOptionsTest.cs | 71 +++++++++++++++++++ 3 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 test/TesterInternal/General/PlacementOptionsTest.cs diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs index 22ab15503e..16ac7b1cd0 100644 --- a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -1,15 +1,14 @@ namespace Orleans.Runtime; /// -/// A placement strategy which attempts to achieve approximately even load based on cluster resources. +/// A placement strategy which attempts to optimize resource distribution across the cluster. /// /// -/// It assigns weights to runtime statistics to prioritize different properties and calculates a normalized score for each silo. -/// The silo with the highest score is chosen for placing the activation. -/// Normalization ensures that each property contributes proportionally to the overall score. -/// You can adjust the weights based on your specific requirements and priorities for load balancing. -/// In addition to normalization, an online adaptive filter provides a smoothing effect -/// (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. +/// It assigns weights to runtime statistics to prioritize different resources and calculates a normalized score for each silo. +/// The silo with the lowest score is chosen for placing the activation. Normalization ensures that each property contributes proportionally +/// to the overall score. You can adjust the weights based on your specific requirements and priorities for load balancing. +/// In addition to normalization, an online adaptive +/// algorithm provides a smoothing effect (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. /// This contributes to avoiding resource saturation on the silos and especially newly joined silos. /// This placement strategy is configured by adding the attribute to a grain. /// diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index c7a8223b57..b7fc24722d 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -101,6 +101,11 @@ public void ValidateConfiguration() ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.AvailableMemoryWeight)); } + if (_options.PhysicalMemoryWeight < 0f || _options.PhysicalMemoryWeight > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.PhysicalMemoryWeight)); + } + if (_options.LocalSiloPreferenceMargin < 0f || _options.LocalSiloPreferenceMargin > 1f) { ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); @@ -108,10 +113,10 @@ public void ValidateConfiguration() if (Truncate(_options.CpuUsageWeight) + Truncate(_options.MemoryUsageWeight) + - Truncate(_options.AvailableMemoryWeight) != 1) + Truncate(_options.AvailableMemoryWeight + + Truncate(_options.PhysicalMemoryWeight)) != 1) { - throw new OrleansConfigurationException( - $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); + throw new OrleansConfigurationException($"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); } static void ThrowOutOfRange(string propertyName) diff --git a/test/TesterInternal/General/PlacementOptionsTest.cs b/test/TesterInternal/General/PlacementOptionsTest.cs new file mode 100644 index 0000000000..e07f5b5e2d --- /dev/null +++ b/test/TesterInternal/General/PlacementOptionsTest.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Options; +using Orleans.Runtime; +using Orleans.Runtime.Configuration.Options; +using TestExtensions; +using Xunit; + +namespace UnitTests.General +{ + public class PlacementOptionsTest : OrleansTestingBase, IClassFixture + { + [Fact, TestCategory("Placement"), TestCategory("Functional")] + public void ConstantsShouldNotChange() + { + Assert.Equal(0.4f, ResourceOptimizedPlacementOptions.DEFAULT_CPU_USAGE_WEIGHT); + Assert.Equal(0.3f, ResourceOptimizedPlacementOptions.DEFAULT_MEMORY_USAGE_WEIGHT); + Assert.Equal(0.2f, ResourceOptimizedPlacementOptions.DEFAULT_AVAILABLE_MEMORY_WEIGHT); + Assert.Equal(0.1f, ResourceOptimizedPlacementOptions.DEFAULT_PHYSICAL_MEMORY_WEIGHT); + } + + [Theory, TestCategory("Placement"), TestCategory("Functional")] + [InlineData(-0.1f, 0.4f, 0.2f, 0.1f, 0.05f)] + [InlineData(0.3f, 1.1f, 0.2f, 0.1f, 0.05f)] + [InlineData(0.3f, 0.4f, -0.1f, 0.1f, 0.05f)] + [InlineData(0.3f, 0.4f, 0.2f, 1.1f, 0.05f)] + [InlineData(0.3f, 0.4f, 0.2f, 0.1f, -0.05f)] + public void InvalidWeightsShouldThrow(float cpuUsage, float memUsage, float memAvailable, float memPhysical, float prefMargin) + { + var options = Options.Create(new ResourceOptimizedPlacementOptions + { + CpuUsageWeight = cpuUsage, + MemoryUsageWeight = memUsage, + AvailableMemoryWeight = memAvailable, + PhysicalMemoryWeight = memPhysical, + LocalSiloPreferenceMargin = prefMargin + }); + + var validator = new ResourceOptimizedPlacementOptionsValidator(options); + Assert.Throws(validator.ValidateConfiguration); + } + + [Fact, TestCategory("Placement"), TestCategory("Functional")] + public void SumGreaterThanOneShouldThrow() + { + var options = Options.Create(new ResourceOptimizedPlacementOptions + { + CpuUsageWeight = 0.3f, + MemoryUsageWeight = 0.4f, + AvailableMemoryWeight = 0.2f, + PhysicalMemoryWeight = 0.21f // sum > 1 + }); + + var validator = new ResourceOptimizedPlacementOptionsValidator(options); + Assert.Throws(validator.ValidateConfiguration); + } + + [Fact, TestCategory("Placement"), TestCategory("Functional")] + public void SumLessThanOneShouldThrow() + { + var options = Options.Create(new ResourceOptimizedPlacementOptions + { + CpuUsageWeight = 0.3f, + MemoryUsageWeight = 0.4f, + AvailableMemoryWeight = 0.2f, + PhysicalMemoryWeight = 0.19f // sum < 1 + }); + + var validator = new ResourceOptimizedPlacementOptionsValidator(options); + Assert.Throws(validator.ValidateConfiguration); + } + } +} From c3831d2ea8589ea35c26cfa223dcfb9f0d5fa8d8 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 23:01:41 +0100 Subject: [PATCH 09/49] renamed file --- test/TesterInternal/General/PlacementOptionsTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/TesterInternal/General/PlacementOptionsTest.cs b/test/TesterInternal/General/PlacementOptionsTest.cs index e07f5b5e2d..fc08ebf361 100644 --- a/test/TesterInternal/General/PlacementOptionsTest.cs +++ b/test/TesterInternal/General/PlacementOptionsTest.cs @@ -8,7 +8,7 @@ namespace UnitTests.General { public class PlacementOptionsTest : OrleansTestingBase, IClassFixture { - [Fact, TestCategory("Placement"), TestCategory("Functional")] + [Fact, TestCategory("PlacementOptions"), TestCategory("Functional")] public void ConstantsShouldNotChange() { Assert.Equal(0.4f, ResourceOptimizedPlacementOptions.DEFAULT_CPU_USAGE_WEIGHT); @@ -17,7 +17,7 @@ public void ConstantsShouldNotChange() Assert.Equal(0.1f, ResourceOptimizedPlacementOptions.DEFAULT_PHYSICAL_MEMORY_WEIGHT); } - [Theory, TestCategory("Placement"), TestCategory("Functional")] + [Theory, TestCategory("PlacementOptions"), TestCategory("Functional")] [InlineData(-0.1f, 0.4f, 0.2f, 0.1f, 0.05f)] [InlineData(0.3f, 1.1f, 0.2f, 0.1f, 0.05f)] [InlineData(0.3f, 0.4f, -0.1f, 0.1f, 0.05f)] @@ -38,7 +38,7 @@ public void InvalidWeightsShouldThrow(float cpuUsage, float memUsage, float memA Assert.Throws(validator.ValidateConfiguration); } - [Fact, TestCategory("Placement"), TestCategory("Functional")] + [Fact, TestCategory("PlacementOptions"), TestCategory("Functional")] public void SumGreaterThanOneShouldThrow() { var options = Options.Create(new ResourceOptimizedPlacementOptions @@ -53,7 +53,7 @@ public void SumGreaterThanOneShouldThrow() Assert.Throws(validator.ValidateConfiguration); } - [Fact, TestCategory("Placement"), TestCategory("Functional")] + [Fact, TestCategory("PlacementOptions"), TestCategory("Functional")] public void SumLessThanOneShouldThrow() { var options = Options.Create(new ResourceOptimizedPlacementOptions From e52ff54db98e82f1d6677d516bfa3372471fe7c9 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 23:28:26 +0100 Subject: [PATCH 10/49] made ResourceOptimizedPlacement strategy public in case users want to make it a global acting strategy --- .../Placement/ResourceOptimizedPlacement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs index 16ac7b1cd0..87aa665f7d 100644 --- a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -12,7 +12,7 @@ namespace Orleans.Runtime; /// This contributes to avoiding resource saturation on the silos and especially newly joined silos. /// This placement strategy is configured by adding the attribute to a grain. /// -internal sealed class ResourceOptimizedPlacement : PlacementStrategy +public sealed class ResourceOptimizedPlacement : PlacementStrategy { internal static readonly ResourceOptimizedPlacement Singleton = new(); } From aceadec7528559e1e8d8375e7349f44e96ee9693 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Wed, 10 Jan 2024 00:31:04 +0100 Subject: [PATCH 11/49] wip --- .../Placement/PlacementAttribute.cs | 12 + .../Placement/ResourceOptimizedPlacement.cs | 27 ++ .../ResourceOptimizedPlacementOptions.cs | 81 ++++++ .../ActivationCountPlacementDirector.cs | 1 + .../ResourceOptimizedPlacementDirector.cs | 273 ++++++++++++++++++ 5 files changed, 394 insertions(+) create mode 100644 src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs create mode 100644 src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs create mode 100644 src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs diff --git a/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs b/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs index 8d0d3aa36d..ae10b343d0 100644 --- a/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs +++ b/src/Orleans.Core.Abstractions/Placement/PlacementAttribute.cs @@ -99,4 +99,16 @@ public sealed class SiloRoleBasedPlacementAttribute : PlacementAttribute base(SiloRoleBasedPlacement.Singleton) { } } + + /// + /// Marks a grain class as using the policy. + /// + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class ResourceOptimizedPlacementAttribute : PlacementAttribute + { + public ResourceOptimizedPlacementAttribute() : + base(ResourceOptimizedPlacement.Singleton) + { } + } } diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs new file mode 100644 index 0000000000..a92c81dcf5 --- /dev/null +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -0,0 +1,27 @@ +namespace Orleans.Runtime; + +/// +/// A placement strategy which attempts to achieve approximately even load based on cluster resources. +/// +/// +/// It assigns weights to runtime statistics to prioritize different properties and calculates a normalized score for each silo. +/// The silo with the highest score is chosen for placing the activation. +/// Normalization ensures that each property contributes proportionally to the overall score. +/// You can adjust the weights based on your specific requirements and priorities for load balancing. +/// In addition to normalization, an online adaptive filter provides a smoothing effect +/// (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. +/// This contributes to avoiding resource saturation on the silos and especially newly joined silos. +/// Details of the properties used to make the placement decisions and their default values are given below: +/// +/// Cpu usage: The default weight (0.3), indicates that CPU usage is important but not the sole determinant in placement decisions. +/// Available memory: The default weight (0.4), emphasizes the importance of nodes with ample available memory. +/// Memory usage: Is important for understanding the current load on a node. The default weight (0.2), ensures consideration without making it overly influential. +/// Total physical memory: Represents the overall capacity of a node. The default weight (0.1), contributes to a more long-term resource planning perspective. +/// +/// This placement strategy is configured by adding the attribute to a grain. +/// +[Immutable, GenerateSerializer, SuppressReferenceTracking] +internal sealed class ResourceOptimizedPlacement : PlacementStrategy +{ + internal static readonly ResourceOptimizedPlacement Singleton = new(); +} diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs new file mode 100644 index 0000000000..eb058362d3 --- /dev/null +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.Options; + +namespace Orleans.Runtime.Configuration.Options; + +/// +/// Settings which regulate the placement of grains across a cluster when using . +/// +public sealed class ResourceOptimizedPlacementOptions +{ + /// + /// The importance of the CPU utilization by the silo. + /// + /// Expressed as percentage. + public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_CPU_USAGE_WEIGHT = 0.3f; + + /// + /// The importance of the amount of memory available to the silo. + /// + /// Expressed as percentage. + public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.4f; + + /// + /// The importance of the used memory by the silo. + /// + /// Expressed as percentage. + public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.2f; + + /// + /// The importance of the total physical memory of the silo. + /// + /// Expressed as percentage. + public float TotalPhysicalMemoryWeight { get; set; } = DEFAULT_TOTAL_PHYSICAL_MEMORY_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_TOTAL_PHYSICAL_MEMORY_WEIGHT = 0.1f; + + /// + /// The specified margin for which: if two silos (one of them being the local silo), have a utilization score that should be considered "the same" within this margin. + /// + /// When this value is 0, then the policy will always favor the silo with the higher utilization score, even if that silo is remote to the current pending activation. + /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . + /// + /// + /// Expressed as percentage. + public float LocalSiloPreferenceMargin { get; set; } + /// + /// The default value of . + /// + public const float DEFAULT_LOCAL_SILO_PREFERENCE_MARGIN = 0.05f; +} + +internal sealed class ResourceOptimizedPlacementOptionsValidator + (IOptions options) : IConfigurationValidator +{ + private readonly ResourceOptimizedPlacementOptions _options = options.Value; + + public void ValidateConfiguration() + { + if (_options.CpuUsageWeight + + _options.MemoryUsageWeight + + _options.AvailableMemoryWeight + + _options.TotalPhysicalMemoryWeight != 1.0f) + { + throw new OrleansConfigurationException( + $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1.0"); + } + } +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/ActivationCountPlacementDirector.cs b/src/Orleans.Runtime/Placement/ActivationCountPlacementDirector.cs index 1384b1c0b6..f07ca8f19e 100644 --- a/src/Orleans.Runtime/Placement/ActivationCountPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ActivationCountPlacementDirector.cs @@ -9,6 +9,7 @@ namespace Orleans.Runtime.Placement { + internal class ActivationCountPlacementDirector : RandomPlacementDirector, ISiloStatisticsChangeListener, IPlacementDirector { private class CachedLocalStat diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs new file mode 100644 index 0000000000..3a66506c06 --- /dev/null +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Orleans.Runtime.Configuration.Options; + +namespace Orleans.Runtime.Placement; + +internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener +{ + readonly record struct ResourceStatistics( + float? CpuUsage, + float? AvailableMemory, + long? MemoryUsage, + long? TotalPhysicalMemory, + bool IsOverloaded); + + Task _cachedLocalSilo; + readonly SiloAddress _localAddress; + readonly ILocalSiloDetails _localSiloDetails; + readonly ResourceOptimizedPlacementOptions _options; + readonly ConcurrentDictionary siloStatistics = []; + + readonly DualModeKalmanFilter _cpuUsageFilter = new(); + readonly DualModeKalmanFilter _availableMemoryFilter = new(); + readonly DualModeKalmanFilter _memoryUsageFilter = new(); + + public ResourceOptimizedPlacementDirector( + ILocalSiloDetails localSiloDetails, + DeploymentLoadPublisher deploymentLoadPublisher, + IOptions options) + { + _localSiloDetails = localSiloDetails; + _options = options.Value; + deploymentLoadPublisher?.SubscribeToStatisticsChangeEvents(this); + } + + public Task OnAddActivation(PlacementStrategy strategy, PlacementTarget target, IPlacementContext context) + { + var compatibleSilos = context.GetCompatibleSilos(target); + + if (IPlacementDirector.GetPlacementHint(target.RequestContextData, compatibleSilos) is { } placementHint) + { + return Task.FromResult(placementHint); + } + + if (compatibleSilos.Length == 0) + { + throw new SiloUnavailableException($"Cannot place grain with Id = [{target.GrainIdentity}], because there are no compatible silos."); + } + + if (compatibleSilos.Length == 1) + { + return Task.FromResult(compatibleSilos[0]); + } + + if (siloStatistics.IsEmpty) + { + return Task.FromResult(compatibleSilos[Random.Shared.Next(compatibleSilos.Length)]); + } + + var selectedSilo = GetSiloWithHighestScore(compatibleSilos); + + + return Task.FromResult(selectedSilo); + } + + SiloAddress GetSiloWithHighestScore(SiloAddress[] compatibleSilos) + { + List> relevantSilos = []; + foreach (var silo in compatibleSilos) + { + if (siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) + { + relevantSilos.Add(new(silo, stats)); + } + } + + int chooseFrom = (int)Math.Ceiling(Math.Sqrt(relevantSilos.Count)); + Dictionary chooseFromSilos = []; + + while (chooseFromSilos.Count < chooseFrom) + { + int index = Random.Shared.Next(relevantSilos.Count); + var pickedSilo = relevantSilos[index]; + + relevantSilos.RemoveAt(index); + + float score = CalculateScore(pickedSilo.Value); + chooseFromSilos.Add(pickedSilo.Key, score); + } + + return chooseFromSilos.OrderByDescending(kv => kv.Value).FirstOrDefault().Key; + } + + float CalculateScore(ResourceStatistics stats) + { + float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; + + if (stats.TotalPhysicalMemory.HasValue) + { + float normalizedAvailableMemory = stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / stats.TotalPhysicalMemory.Value : 0f; + float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / stats.TotalPhysicalMemory.Value : 0f; + float normalizedTotalPhysicalMemory = stats.TotalPhysicalMemory.HasValue ? stats.TotalPhysicalMemory.Value / (1024 * 1024 * 1024) : 0f; + + return _options.CpuUsageWeight * normalizedCpuUsage + + _options.AvailableMemoryWeight * normalizedAvailableMemory + + _options.MemoryUsageWeight * normalizedMemoryUsage + + _options.TotalPhysicalMemoryWeight * normalizedTotalPhysicalMemory; + } + + return _options.CpuUsageWeight * normalizedCpuUsage; + } + + public void RemoveSilo(SiloAddress address) + => siloStatistics.TryRemove(address, out _); + + public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeStatistics statistics) + => siloStatistics.AddOrUpdate( + key: address, + addValue: new ResourceStatistics( + statistics.CpuUsage, + statistics.AvailableMemory, + statistics.MemoryUsage, + statistics.TotalPhysicalMemory, + statistics.IsOverloaded), + updateValueFactory: (_, _) => + { + float estimatedCpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); + float estimatedAvailableMemory = _availableMemoryFilter.Filter(statistics.AvailableMemory); + long estimatedMemoryUsage = _memoryUsageFilter.Filter(statistics.MemoryUsage); + + return new ResourceStatistics( + estimatedCpuUsage, + estimatedAvailableMemory, + estimatedMemoryUsage, + statistics.TotalPhysicalMemory, + statistics.IsOverloaded); + }); + + sealed class DualModeKalmanFilter where T : unmanaged, INumber + { + readonly KalmanFilter _slowFilter = new(T.Zero); + readonly KalmanFilter _fastFilter = new(T.CreateChecked(0.01)); + + FilterRegime _regime = FilterRegime.Slow; + + enum FilterRegime + { + Slow, + Fast + } + + public T Filter(T? measurement) + { + T _measurement = measurement ?? T.Zero; + + T slowEstimate = _slowFilter.Filter(_measurement); + T fastEstimate = _fastFilter.Filter(_measurement); + + if (_measurement > slowEstimate) + { + if (_regime == FilterRegime.Slow) + { + // since we now got a measurement we can use it to set the filter's 'estimate', + // in addition we set the 'error covariance' to 0, indicating we want to fully + // trust the measurement (now the new 'estimate') to reach the actual signal asap. + _fastFilter.SetState(_measurement, T.Zero); + + // ensure we recalculate since we changed the 'error covariance' + fastEstimate = _fastFilter.Filter(_measurement); + + _regime = FilterRegime.Fast; + } + + return fastEstimate; + } + else + { + if (_regime == FilterRegime.Fast) + { + // since the slow filter will accumulate the changes, we want to reset its state + // so that it aligns with the current peak of the fast filter so we get a slower + // decay that is always aligned with the latest fast filter state and not the overall + // accumulated state of the whole signal over its lifetime. + _slowFilter.SetState(_fastFilter.PriorEstimate, _fastFilter.PriorErrorCovariance); + + // ensure we recalculate since we changed both the 'estimate' and 'error covariance' + slowEstimate = _slowFilter.Filter(_measurement); + + _regime = FilterRegime.Slow; + } + + return slowEstimate; + } + } + + sealed class KalmanFilter(T processNoiseCovariance) + { + readonly T _processNoiseCovariance = processNoiseCovariance; + + public T PriorEstimate { get; private set; } = T.Zero; + public T PriorErrorCovariance { get; private set; } = T.One; + + public void SetState(T estimate, T errorCovariance) + { + PriorEstimate = estimate; + PriorErrorCovariance = errorCovariance; + } + + public T Filter(T measurement) + { + #region Prediction Step + + #region Formula + // ^x_k = A * x_k-1 + B * u_k + #endregion + + #region Simplification + // ^x_k = x_k-1 + #endregion + + #region Explanation + // As every resource statistics is a single entity in our case, we have a 1-dimensional signal problem, so every entity in our model is a scalar, not a matrix. + // uk is the control signal which incorporate external information about how the system is expected to behave between measurements. We have no idea how the CPU usage is going to behave between measurements, therefor we have no control signal, so uk = 0. + // B is the control input matrix, but since uk = 0 this means we don't need to bother with it. + // A is the state transition matrix, and we established that we have a 1-dimensional signal problem, so this is now a scalar. Same as with uk, we have no idea how the CPU usage is going to transition, therefor A = 1. + // We just established that A = 1, and since A is a unitary scalar, this means AT which is the transpose of A, is AT = 1. + #endregion + + T estimate = PriorEstimate; + T errorCovariance = PriorErrorCovariance + _processNoiseCovariance; + + #endregion + + #region Correction Step + + #region Formulas + // * K_k = (P_k * H_T) / (H * P_k * H_T + R) + // * ^x_k = x_k + K_k * (z_k - H * x_k) + // * ^P_k = (I - K_k * H) * P_k; + #endregion + + #region Simplifications + // * K_k = P_k / (P_k + 1); + // * ^x_k = x_k + K_k * (z_k - x_k) + // * ^P_k = (1 - K_k) * P_k; + #endregion + + #region Explanation + // Same as with the prediction, we deal only with scalars, not matrices. + // H is the observation matrix, which acts as a bridge between the internal model A, and the external measurements R. We can set H = 1, which indicates that the measurements directly correspond to the state variables without any transformations or scaling factors. + // Since H = 1, it follows that HT = 1. + // R is the measurement covariance matrix, which represents the influence of the measurements relative to the predicted state. We set this value to R = 1, which indicates that all measurements are assumed to have the same level of uncertainty, and there is no correlation between different measurements. + #endregion + + T gain = errorCovariance / (errorCovariance + T.One); + T newEstimate = estimate + gain * (measurement - estimate); + T newErrorCovariance = (T.One - gain) * errorCovariance; + + PriorEstimate = newEstimate; + PriorErrorCovariance = newErrorCovariance; + + #endregion + + return newEstimate; + } + } + } +} From 7c4cd129c3fab08fc1f6bbfefeca23e883275a4c Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Wed, 10 Jan 2024 22:22:11 +0100 Subject: [PATCH 12/49] wip --- .../ResourceOptimizedPlacementDirector.cs | 113 ++++++------------ 1 file changed, 38 insertions(+), 75 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 3a66506c06..00b0cdbd43 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -11,15 +11,10 @@ namespace Orleans.Runtime.Placement; internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener { - readonly record struct ResourceStatistics( - float? CpuUsage, - float? AvailableMemory, - long? MemoryUsage, - long? TotalPhysicalMemory, - bool IsOverloaded); + readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory); Task _cachedLocalSilo; - readonly SiloAddress _localAddress; + readonly ILocalSiloDetails _localSiloDetails; readonly ResourceOptimizedPlacementOptions _options; readonly ConcurrentDictionary siloStatistics = []; @@ -62,18 +57,21 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return Task.FromResult(compatibleSilos[Random.Shared.Next(compatibleSilos.Length)]); } - var selectedSilo = GetSiloWithHighestScore(compatibleSilos); - + var bestCandidate = GetBestSiloCandidate(compatibleSilos); + if (IsLocalSiloPreferable(context, compatibleSilos, bestCandidate.Value)) + { + return _cachedLocalSilo ??= Task.FromResult(context.LocalSilo); + } - return Task.FromResult(selectedSilo); + return Task.FromResult(bestCandidate.Key); } - SiloAddress GetSiloWithHighestScore(SiloAddress[] compatibleSilos) + KeyValuePair GetBestSiloCandidate(SiloAddress[] compatibleSilos) { List> relevantSilos = []; foreach (var silo in compatibleSilos) { - if (siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) + if (siloStatistics.TryGetValue(silo, out var stats)) { relevantSilos.Add(new(silo, stats)); } @@ -93,7 +91,7 @@ SiloAddress GetSiloWithHighestScore(SiloAddress[] compatibleSilos) chooseFromSilos.Add(pickedSilo.Key, score); } - return chooseFromSilos.OrderByDescending(kv => kv.Value).FirstOrDefault().Key; + return chooseFromSilos.OrderByDescending(kv => kv.Value).First(); } float CalculateScore(ResourceStatistics stats) @@ -115,6 +113,28 @@ float CalculateScore(ResourceStatistics stats) return _options.CpuUsageWeight * normalizedCpuUsage; } + bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) + { + if (context.LocalSiloStatus != SiloStatus.Active || + !compatibleSilos.Contains(context.LocalSilo)) + { + return false; + } + + if (siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) + { + float localScore = CalculateScore(localStats); + float localScoreMargin = localScore * _options.LocalSiloPreferenceMargin; + + if (localScore + localScoreMargin >= bestCandidateScore) + { + return true; + } + } + + return false; + } + public void RemoveSilo(SiloAddress address) => siloStatistics.TryRemove(address, out _); @@ -125,8 +145,7 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta statistics.CpuUsage, statistics.AvailableMemory, statistics.MemoryUsage, - statistics.TotalPhysicalMemory, - statistics.IsOverloaded), + statistics.TotalPhysicalMemory), updateValueFactory: (_, _) => { float estimatedCpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); @@ -137,10 +156,10 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta estimatedCpuUsage, estimatedAvailableMemory, estimatedMemoryUsage, - statistics.TotalPhysicalMemory, - statistics.IsOverloaded); + statistics.TotalPhysicalMemory); }); + // details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ sealed class DualModeKalmanFilter where T : unmanaged, INumber { readonly KalmanFilter _slowFilter = new(T.Zero); @@ -165,15 +184,9 @@ public T Filter(T? measurement) { if (_regime == FilterRegime.Slow) { - // since we now got a measurement we can use it to set the filter's 'estimate', - // in addition we set the 'error covariance' to 0, indicating we want to fully - // trust the measurement (now the new 'estimate') to reach the actual signal asap. + _regime = FilterRegime.Fast; _fastFilter.SetState(_measurement, T.Zero); - - // ensure we recalculate since we changed the 'error covariance' fastEstimate = _fastFilter.Filter(_measurement); - - _regime = FilterRegime.Fast; } return fastEstimate; @@ -182,16 +195,9 @@ public T Filter(T? measurement) { if (_regime == FilterRegime.Fast) { - // since the slow filter will accumulate the changes, we want to reset its state - // so that it aligns with the current peak of the fast filter so we get a slower - // decay that is always aligned with the latest fast filter state and not the overall - // accumulated state of the whole signal over its lifetime. + _regime = FilterRegime.Slow; _slowFilter.SetState(_fastFilter.PriorEstimate, _fastFilter.PriorErrorCovariance); - - // ensure we recalculate since we changed both the 'estimate' and 'error covariance' slowEstimate = _slowFilter.Filter(_measurement); - - _regime = FilterRegime.Slow; } return slowEstimate; @@ -213,50 +219,9 @@ public void SetState(T estimate, T errorCovariance) public T Filter(T measurement) { - #region Prediction Step - - #region Formula - // ^x_k = A * x_k-1 + B * u_k - #endregion - - #region Simplification - // ^x_k = x_k-1 - #endregion - - #region Explanation - // As every resource statistics is a single entity in our case, we have a 1-dimensional signal problem, so every entity in our model is a scalar, not a matrix. - // uk is the control signal which incorporate external information about how the system is expected to behave between measurements. We have no idea how the CPU usage is going to behave between measurements, therefor we have no control signal, so uk = 0. - // B is the control input matrix, but since uk = 0 this means we don't need to bother with it. - // A is the state transition matrix, and we established that we have a 1-dimensional signal problem, so this is now a scalar. Same as with uk, we have no idea how the CPU usage is going to transition, therefor A = 1. - // We just established that A = 1, and since A is a unitary scalar, this means AT which is the transpose of A, is AT = 1. - #endregion - T estimate = PriorEstimate; T errorCovariance = PriorErrorCovariance + _processNoiseCovariance; - #endregion - - #region Correction Step - - #region Formulas - // * K_k = (P_k * H_T) / (H * P_k * H_T + R) - // * ^x_k = x_k + K_k * (z_k - H * x_k) - // * ^P_k = (I - K_k * H) * P_k; - #endregion - - #region Simplifications - // * K_k = P_k / (P_k + 1); - // * ^x_k = x_k + K_k * (z_k - x_k) - // * ^P_k = (1 - K_k) * P_k; - #endregion - - #region Explanation - // Same as with the prediction, we deal only with scalars, not matrices. - // H is the observation matrix, which acts as a bridge between the internal model A, and the external measurements R. We can set H = 1, which indicates that the measurements directly correspond to the state variables without any transformations or scaling factors. - // Since H = 1, it follows that HT = 1. - // R is the measurement covariance matrix, which represents the influence of the measurements relative to the predicted state. We set this value to R = 1, which indicates that all measurements are assumed to have the same level of uncertainty, and there is no correlation between different measurements. - #endregion - T gain = errorCovariance / (errorCovariance + T.One); T newEstimate = estimate + gain * (measurement - estimate); T newErrorCovariance = (T.One - gain) * errorCovariance; @@ -264,8 +229,6 @@ public T Filter(T measurement) PriorEstimate = newEstimate; PriorErrorCovariance = newErrorCovariance; - #endregion - return newEstimate; } } From 74d8c878e23029a3f1b11b93793daa1427a27768 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Wed, 10 Jan 2024 22:29:30 +0100 Subject: [PATCH 13/49] added neccessary serives to DefaultSiloServices --- src/Orleans.Runtime/Hosting/DefaultSiloServices.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs b/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs index bd9e25d0fa..ff7647befa 100644 --- a/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs +++ b/src/Orleans.Runtime/Hosting/DefaultSiloServices.cs @@ -42,6 +42,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Orleans.Serialization.Internal; +using Orleans.Runtime.Configuration.Options; namespace Orleans.Hosting { @@ -190,6 +191,7 @@ internal static void AddDefaultServices(ISiloBuilder builder) // Placement services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -207,6 +209,7 @@ internal static void AddDefaultServices(ISiloBuilder builder) services.AddPlacementDirector(); services.AddPlacementDirector(); services.AddPlacementDirector(); + services.AddPlacementDirector(); // Versioning services.TryAddSingleton(); @@ -298,6 +301,7 @@ internal static void AddDefaultServices(ISiloBuilder builder) services.ConfigureFormatter(); services.ConfigureFormatter(); services.ConfigureFormatter(); + services.ConfigureFormatter(); services.ConfigureFormatter(); services.ConfigureFormatter(); services.ConfigureFormatter(); From e8258190e45c5663a0f8aec7e16f544640d9efdf Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 00:42:37 +0100 Subject: [PATCH 14/49] wip --- .../Placement/ResourceOptimizedPlacement.cs | 8 --- .../ResourceOptimizedPlacementOptions.cs | 65 +++++++++++-------- .../ResourceOptimizedPlacementDirector.cs | 28 +++++--- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs index a92c81dcf5..22ab15503e 100644 --- a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -11,16 +11,8 @@ namespace Orleans.Runtime; /// In addition to normalization, an online adaptive filter provides a smoothing effect /// (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. /// This contributes to avoiding resource saturation on the silos and especially newly joined silos. -/// Details of the properties used to make the placement decisions and their default values are given below: -/// -/// Cpu usage: The default weight (0.3), indicates that CPU usage is important but not the sole determinant in placement decisions. -/// Available memory: The default weight (0.4), emphasizes the importance of nodes with ample available memory. -/// Memory usage: Is important for understanding the current load on a node. The default weight (0.2), ensures consideration without making it overly influential. -/// Total physical memory: Represents the overall capacity of a node. The default weight (0.1), contributes to a more long-term resource planning perspective. -/// /// This placement strategy is configured by adding the attribute to a grain. /// -[Immutable, GenerateSerializer, SuppressReferenceTracking] internal sealed class ResourceOptimizedPlacement : PlacementStrategy { internal static readonly ResourceOptimizedPlacement Singleton = new(); diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index eb058362d3..2a53303291 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Options; namespace Orleans.Runtime.Configuration.Options; @@ -10,51 +11,41 @@ public sealed class ResourceOptimizedPlacementOptions /// /// The importance of the CPU utilization by the silo. /// - /// Expressed as percentage. + /// Valid range of values are [0-1] public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_CPU_USAGE_WEIGHT = 0.3f; + public const float DEFAULT_CPU_USAGE_WEIGHT = 0.4f; /// - /// The importance of the amount of memory available to the silo. + /// The importance of the memory utilization by the silo. /// - /// Expressed as percentage. - public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; - /// - /// The default value of . - /// - public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.4f; - - /// - /// The importance of the used memory by the silo. - /// - /// Expressed as percentage. + /// Valid range of values are [0-1] public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.2f; + public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.4f; /// - /// The importance of the total physical memory of the silo. + /// The importance of the available memory to the silo. /// - /// Expressed as percentage. - public float TotalPhysicalMemoryWeight { get; set; } = DEFAULT_TOTAL_PHYSICAL_MEMORY_WEIGHT; + /// Valid range of values are [0-1] + public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; /// - /// The default value of . + /// The default value of . /// - public const float DEFAULT_TOTAL_PHYSICAL_MEMORY_WEIGHT = 0.1f; + public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.1f; /// - /// The specified margin for which: if two silos (one of them being the local silo), have a utilization score that should be considered "the same" within this margin. + /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. /// /// When this value is 0, then the policy will always favor the silo with the higher utilization score, even if that silo is remote to the current pending activation. /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// - /// Expressed as percentage. + /// Valid range of values are [0-1] public float LocalSiloPreferenceMargin { get; set; } /// /// The default value of . @@ -69,13 +60,33 @@ internal sealed class ResourceOptimizedPlacementOptionsValidator public void ValidateConfiguration() { - if (_options.CpuUsageWeight + - _options.MemoryUsageWeight + - _options.AvailableMemoryWeight + - _options.TotalPhysicalMemoryWeight != 1.0f) + if (_options.CpuUsageWeight < 0f || _options.CpuUsageWeight > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.CpuUsageWeight)); + } + + if (_options.MemoryUsageWeight < 0f || _options.MemoryUsageWeight > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.MemoryUsageWeight)); + } + + if (_options.AvailableMemoryWeight < 0f || _options.AvailableMemoryWeight > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.AvailableMemoryWeight)); + } + + if (_options.LocalSiloPreferenceMargin < 0f || _options.LocalSiloPreferenceMargin > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); + } + + if (_options.CpuUsageWeight + _options.MemoryUsageWeight + _options.AvailableMemoryWeight != 1f) { throw new OrleansConfigurationException( - $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1.0"); + $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); } + + static void ThrowOutOfRange(string propertyName) + => throw new OrleansConfigurationException($"{propertyName} must be inclusive between [0-1]"); } } \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 00b0cdbd43..4b54db599a 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -58,6 +58,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa } var bestCandidate = GetBestSiloCandidate(compatibleSilos); + if (IsLocalSiloPreferable(context, compatibleSilos, bestCandidate.Value)) { return _cachedLocalSilo ??= Task.FromResult(context.LocalSilo); @@ -91,23 +92,28 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa chooseFromSilos.Add(pickedSilo.Key, score); } - return chooseFromSilos.OrderByDescending(kv => kv.Value).First(); + var orderedByLowestScore = chooseFromSilos.OrderBy(kv => kv.Value); + + // there could be more than 1 silo that has the same score, we pick 1 of them randomly so that we dont continuously pick the first one. + var lowestScore = orderedByLowestScore.First().Value; + var shortListedSilos = orderedByLowestScore.TakeWhile(p => p.Value == lowestScore).ToList(); + var winningSilo = shortListedSilos[Random.Shared.Next(shortListedSilos.Count)]; + + return winningSilo; } float CalculateScore(ResourceStatistics stats) { float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; - if (stats.TotalPhysicalMemory.HasValue) + if (stats.TotalPhysicalMemory is { } physicalMemory && physicalMemory > 0) { - float normalizedAvailableMemory = stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / stats.TotalPhysicalMemory.Value : 0f; - float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / stats.TotalPhysicalMemory.Value : 0f; - float normalizedTotalPhysicalMemory = stats.TotalPhysicalMemory.HasValue ? stats.TotalPhysicalMemory.Value / (1024 * 1024 * 1024) : 0f; + float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / physicalMemory : 0f; + float normalizedAvailableMemory = stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f; return _options.CpuUsageWeight * normalizedCpuUsage + - _options.AvailableMemoryWeight * normalizedAvailableMemory + _options.MemoryUsageWeight * normalizedMemoryUsage + - _options.TotalPhysicalMemoryWeight * normalizedTotalPhysicalMemory; + _options.AvailableMemoryWeight * normalizedAvailableMemory; } return _options.CpuUsageWeight * normalizedCpuUsage; @@ -115,8 +121,7 @@ float CalculateScore(ResourceStatistics stats) bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) { - if (context.LocalSiloStatus != SiloStatus.Active || - !compatibleSilos.Contains(context.LocalSilo)) + if (context.LocalSiloStatus != SiloStatus.Active || !compatibleSilos.Contains(context.LocalSilo)) { return false; } @@ -124,9 +129,12 @@ bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSi if (siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) { float localScore = CalculateScore(localStats); + + float scoreDiff = Math.Abs(localScore - bestCandidateScore); + float localScoreMargin = localScore * _options.LocalSiloPreferenceMargin; - if (localScore + localScoreMargin >= bestCandidateScore) + if (localScore - localScoreMargin <= bestCandidateScore) { return true; } From 66a31ec45850792c33304e72c98c5db6be676848 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 00:46:13 +0100 Subject: [PATCH 15/49] . --- .../Placement/ResourceOptimizedPlacementDirector.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 4b54db599a..d12e7fc073 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -102,6 +102,9 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return winningSilo; } + /// + /// Always returns a value [0-1] + /// float CalculateScore(ResourceStatistics stats) { float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; @@ -129,12 +132,9 @@ bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSi if (siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) { float localScore = CalculateScore(localStats); - float scoreDiff = Math.Abs(localScore - bestCandidateScore); - float localScoreMargin = localScore * _options.LocalSiloPreferenceMargin; - - if (localScore - localScoreMargin <= bestCandidateScore) + if (_options.LocalSiloPreferenceMargin >= scoreDiff) { return true; } From 0a7237ca302431c9d3c64671c0c17ffd6367965e Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 00:59:42 +0100 Subject: [PATCH 16/49] fixing some validations --- .../ResourceOptimizedPlacementOptions.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 2a53303291..4e91dfb465 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -11,32 +11,32 @@ public sealed class ResourceOptimizedPlacementOptions /// /// The importance of the CPU utilization by the silo. /// - /// Valid range of values are [0-1] + /// Valid range of values are [0.00-1.00] public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_CPU_USAGE_WEIGHT = 0.4f; + public const float DEFAULT_CPU_USAGE_WEIGHT = 0.34f; /// /// The importance of the memory utilization by the silo. /// - /// Valid range of values are [0-1] + /// Valid range of values are [0.00-1.00] public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.4f; + public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.33f; /// /// The importance of the available memory to the silo. /// - /// Valid range of values are [0-1] + /// Valid range of values are [0.00-1.00] public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.1f; + public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.33f; /// /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. @@ -45,7 +45,7 @@ public sealed class ResourceOptimizedPlacementOptions /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// - /// Valid range of values are [0-1] + /// Valid range of values are [0.00-1.00] public float LocalSiloPreferenceMargin { get; set; } /// /// The default value of . @@ -80,7 +80,9 @@ public void ValidateConfiguration() ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); } - if (_options.CpuUsageWeight + _options.MemoryUsageWeight + _options.AvailableMemoryWeight != 1f) + if (Truncate(_options.CpuUsageWeight) + + Truncate(_options.MemoryUsageWeight) + + Truncate(_options.AvailableMemoryWeight) != 1) { throw new OrleansConfigurationException( $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); @@ -88,5 +90,8 @@ public void ValidateConfiguration() static void ThrowOutOfRange(string propertyName) => throw new OrleansConfigurationException($"{propertyName} must be inclusive between [0-1]"); + + static double Truncate(double value) + => Math.Floor(value * 100) / 100; } } \ No newline at end of file From cb774bd436fe04318f95e20cec051427444548a3 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 21:49:51 +0100 Subject: [PATCH 17/49] reincoorporated physical ram --- .../ResourceOptimizedPlacementOptions.cs | 44 +++++++++++---- .../ResourceOptimizedPlacementDirector.cs | 54 ++++++++++++------- 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 4e91dfb465..c7a8223b57 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -9,34 +9,57 @@ namespace Orleans.Runtime.Configuration.Options; public sealed class ResourceOptimizedPlacementOptions { /// - /// The importance of the CPU utilization by the silo. + /// The importance of the CPU usage by the silo. /// - /// Valid range of values are [0.00-1.00] + /// + /// A higher value results in the placement favoring silos with lower cpu usage. + /// Valid range is [0.00-1.00] + /// public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_CPU_USAGE_WEIGHT = 0.34f; + public const float DEFAULT_CPU_USAGE_WEIGHT = 0.4f; /// - /// The importance of the memory utilization by the silo. + /// The importance of the memory usage by the silo. /// - /// Valid range of values are [0.00-1.00] + /// + /// A higher value results in the placement favoring silos with lower memory usage. + /// Valid range is [0.00-1.00] + /// public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.33f; + public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.3f; /// /// The importance of the available memory to the silo. /// - /// Valid range of values are [0.00-1.00] + /// + /// A higher values results in the placement favoring silos with higher available memory. + /// Valid range is [0.00-1.00] + /// public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.33f; + public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.2f; + + /// + /// The importance of the physical memory to the silo. + /// + /// + /// A higher values results in the placement favoring silos with higher physical memory. + /// This may have an impact in clusters with resources distributed unevenly across silos. + /// Valid range is [0.00-1.00] + /// + public float PhysicalMemoryWeight { get; set; } = DEFAULT_PHYSICAL_MEMORY_WEIGHT; + /// + /// The default value of . + /// + public const float DEFAULT_PHYSICAL_MEMORY_WEIGHT = 0.1f; /// /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. @@ -45,7 +68,10 @@ public sealed class ResourceOptimizedPlacementOptions /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// - /// Valid range of values are [0.00-1.00] + /// + /// + /// Valid range is [0.00-1.00] + /// public float LocalSiloPreferenceMargin { get; set; } /// /// The default value of . diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index d12e7fc073..93df5f503f 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -23,6 +23,11 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I readonly DualModeKalmanFilter _availableMemoryFilter = new(); readonly DualModeKalmanFilter _memoryUsageFilter = new(); + /// + /// 1 / (1024 * 1024) + /// + const float physicalMemoryScalingFactor = 0.00000095367431640625f; + public ResourceOptimizedPlacementDirector( ILocalSiloDetails localSiloDetails, DeploymentLoadPublisher deploymentLoadPublisher, @@ -102,26 +107,6 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return winningSilo; } - /// - /// Always returns a value [0-1] - /// - float CalculateScore(ResourceStatistics stats) - { - float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; - - if (stats.TotalPhysicalMemory is { } physicalMemory && physicalMemory > 0) - { - float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / physicalMemory : 0f; - float normalizedAvailableMemory = stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f; - - return _options.CpuUsageWeight * normalizedCpuUsage + - _options.MemoryUsageWeight * normalizedMemoryUsage + - _options.AvailableMemoryWeight * normalizedAvailableMemory; - } - - return _options.CpuUsageWeight * normalizedCpuUsage; - } - bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) { if (context.LocalSiloStatus != SiloStatus.Active || !compatibleSilos.Contains(context.LocalSilo)) @@ -143,6 +128,35 @@ bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSi return false; } + /// + /// Always returns a value [0-1] + /// + /// + /// score = cpu_weight * (cpu_usage / 100) + + /// mem_usage_weight * (mem_usage / physical_mem) + + /// mem_avail_weight * [1 - (mem_avail / physical_mem)] + /// physical_mem_weight * (1 / (1024 * 1024 * physical_mem) + /// + /// physical_mem is represented in [MB] to keep the result within [0-1] in cases of silos having physical_mem less than [1GB] + float CalculateScore(ResourceStatistics stats) + { + float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; + + if (stats.TotalPhysicalMemory is { } physicalMemory && physicalMemory > 0) + { + float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / physicalMemory : 0f; + float normalizedAvailableMemory = 1 - (stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f); + float normalizedPhysicalMemory = physicalMemoryScalingFactor * physicalMemory; + + return _options.CpuUsageWeight * normalizedCpuUsage + + _options.MemoryUsageWeight * normalizedMemoryUsage + + _options.AvailableMemoryWeight * normalizedAvailableMemory + + _options.AvailableMemoryWeight * normalizedPhysicalMemory; + } + + return _options.CpuUsageWeight * normalizedCpuUsage; + } + public void RemoveSilo(SiloAddress address) => siloStatistics.TryRemove(address, out _); From 2a0622c44fa3f4d626c818e9aa1336eac5a7e847 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 22:56:01 +0100 Subject: [PATCH 18/49] some tests for options and validator, and XML docs --- .../Placement/ResourceOptimizedPlacement.cs | 13 ++-- .../ResourceOptimizedPlacementOptions.cs | 11 ++- .../General/PlacementOptionsTest.cs | 71 +++++++++++++++++++ 3 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 test/TesterInternal/General/PlacementOptionsTest.cs diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs index 22ab15503e..16ac7b1cd0 100644 --- a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -1,15 +1,14 @@ namespace Orleans.Runtime; /// -/// A placement strategy which attempts to achieve approximately even load based on cluster resources. +/// A placement strategy which attempts to optimize resource distribution across the cluster. /// /// -/// It assigns weights to runtime statistics to prioritize different properties and calculates a normalized score for each silo. -/// The silo with the highest score is chosen for placing the activation. -/// Normalization ensures that each property contributes proportionally to the overall score. -/// You can adjust the weights based on your specific requirements and priorities for load balancing. -/// In addition to normalization, an online adaptive filter provides a smoothing effect -/// (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. +/// It assigns weights to runtime statistics to prioritize different resources and calculates a normalized score for each silo. +/// The silo with the lowest score is chosen for placing the activation. Normalization ensures that each property contributes proportionally +/// to the overall score. You can adjust the weights based on your specific requirements and priorities for load balancing. +/// In addition to normalization, an online adaptive +/// algorithm provides a smoothing effect (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. /// This contributes to avoiding resource saturation on the silos and especially newly joined silos. /// This placement strategy is configured by adding the attribute to a grain. /// diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index c7a8223b57..b7fc24722d 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -101,6 +101,11 @@ public void ValidateConfiguration() ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.AvailableMemoryWeight)); } + if (_options.PhysicalMemoryWeight < 0f || _options.PhysicalMemoryWeight > 1f) + { + ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.PhysicalMemoryWeight)); + } + if (_options.LocalSiloPreferenceMargin < 0f || _options.LocalSiloPreferenceMargin > 1f) { ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); @@ -108,10 +113,10 @@ public void ValidateConfiguration() if (Truncate(_options.CpuUsageWeight) + Truncate(_options.MemoryUsageWeight) + - Truncate(_options.AvailableMemoryWeight) != 1) + Truncate(_options.AvailableMemoryWeight + + Truncate(_options.PhysicalMemoryWeight)) != 1) { - throw new OrleansConfigurationException( - $"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); + throw new OrleansConfigurationException($"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); } static void ThrowOutOfRange(string propertyName) diff --git a/test/TesterInternal/General/PlacementOptionsTest.cs b/test/TesterInternal/General/PlacementOptionsTest.cs new file mode 100644 index 0000000000..e07f5b5e2d --- /dev/null +++ b/test/TesterInternal/General/PlacementOptionsTest.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Options; +using Orleans.Runtime; +using Orleans.Runtime.Configuration.Options; +using TestExtensions; +using Xunit; + +namespace UnitTests.General +{ + public class PlacementOptionsTest : OrleansTestingBase, IClassFixture + { + [Fact, TestCategory("Placement"), TestCategory("Functional")] + public void ConstantsShouldNotChange() + { + Assert.Equal(0.4f, ResourceOptimizedPlacementOptions.DEFAULT_CPU_USAGE_WEIGHT); + Assert.Equal(0.3f, ResourceOptimizedPlacementOptions.DEFAULT_MEMORY_USAGE_WEIGHT); + Assert.Equal(0.2f, ResourceOptimizedPlacementOptions.DEFAULT_AVAILABLE_MEMORY_WEIGHT); + Assert.Equal(0.1f, ResourceOptimizedPlacementOptions.DEFAULT_PHYSICAL_MEMORY_WEIGHT); + } + + [Theory, TestCategory("Placement"), TestCategory("Functional")] + [InlineData(-0.1f, 0.4f, 0.2f, 0.1f, 0.05f)] + [InlineData(0.3f, 1.1f, 0.2f, 0.1f, 0.05f)] + [InlineData(0.3f, 0.4f, -0.1f, 0.1f, 0.05f)] + [InlineData(0.3f, 0.4f, 0.2f, 1.1f, 0.05f)] + [InlineData(0.3f, 0.4f, 0.2f, 0.1f, -0.05f)] + public void InvalidWeightsShouldThrow(float cpuUsage, float memUsage, float memAvailable, float memPhysical, float prefMargin) + { + var options = Options.Create(new ResourceOptimizedPlacementOptions + { + CpuUsageWeight = cpuUsage, + MemoryUsageWeight = memUsage, + AvailableMemoryWeight = memAvailable, + PhysicalMemoryWeight = memPhysical, + LocalSiloPreferenceMargin = prefMargin + }); + + var validator = new ResourceOptimizedPlacementOptionsValidator(options); + Assert.Throws(validator.ValidateConfiguration); + } + + [Fact, TestCategory("Placement"), TestCategory("Functional")] + public void SumGreaterThanOneShouldThrow() + { + var options = Options.Create(new ResourceOptimizedPlacementOptions + { + CpuUsageWeight = 0.3f, + MemoryUsageWeight = 0.4f, + AvailableMemoryWeight = 0.2f, + PhysicalMemoryWeight = 0.21f // sum > 1 + }); + + var validator = new ResourceOptimizedPlacementOptionsValidator(options); + Assert.Throws(validator.ValidateConfiguration); + } + + [Fact, TestCategory("Placement"), TestCategory("Functional")] + public void SumLessThanOneShouldThrow() + { + var options = Options.Create(new ResourceOptimizedPlacementOptions + { + CpuUsageWeight = 0.3f, + MemoryUsageWeight = 0.4f, + AvailableMemoryWeight = 0.2f, + PhysicalMemoryWeight = 0.19f // sum < 1 + }); + + var validator = new ResourceOptimizedPlacementOptionsValidator(options); + Assert.Throws(validator.ValidateConfiguration); + } + } +} From 7ee2a95de91dd84d7ff4107d977e5350b716daf7 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 23:01:41 +0100 Subject: [PATCH 19/49] renamed file --- test/TesterInternal/General/PlacementOptionsTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/TesterInternal/General/PlacementOptionsTest.cs b/test/TesterInternal/General/PlacementOptionsTest.cs index e07f5b5e2d..fc08ebf361 100644 --- a/test/TesterInternal/General/PlacementOptionsTest.cs +++ b/test/TesterInternal/General/PlacementOptionsTest.cs @@ -8,7 +8,7 @@ namespace UnitTests.General { public class PlacementOptionsTest : OrleansTestingBase, IClassFixture { - [Fact, TestCategory("Placement"), TestCategory("Functional")] + [Fact, TestCategory("PlacementOptions"), TestCategory("Functional")] public void ConstantsShouldNotChange() { Assert.Equal(0.4f, ResourceOptimizedPlacementOptions.DEFAULT_CPU_USAGE_WEIGHT); @@ -17,7 +17,7 @@ public void ConstantsShouldNotChange() Assert.Equal(0.1f, ResourceOptimizedPlacementOptions.DEFAULT_PHYSICAL_MEMORY_WEIGHT); } - [Theory, TestCategory("Placement"), TestCategory("Functional")] + [Theory, TestCategory("PlacementOptions"), TestCategory("Functional")] [InlineData(-0.1f, 0.4f, 0.2f, 0.1f, 0.05f)] [InlineData(0.3f, 1.1f, 0.2f, 0.1f, 0.05f)] [InlineData(0.3f, 0.4f, -0.1f, 0.1f, 0.05f)] @@ -38,7 +38,7 @@ public void InvalidWeightsShouldThrow(float cpuUsage, float memUsage, float memA Assert.Throws(validator.ValidateConfiguration); } - [Fact, TestCategory("Placement"), TestCategory("Functional")] + [Fact, TestCategory("PlacementOptions"), TestCategory("Functional")] public void SumGreaterThanOneShouldThrow() { var options = Options.Create(new ResourceOptimizedPlacementOptions @@ -53,7 +53,7 @@ public void SumGreaterThanOneShouldThrow() Assert.Throws(validator.ValidateConfiguration); } - [Fact, TestCategory("Placement"), TestCategory("Functional")] + [Fact, TestCategory("PlacementOptions"), TestCategory("Functional")] public void SumLessThanOneShouldThrow() { var options = Options.Create(new ResourceOptimizedPlacementOptions From 8f6d36a4927d4e5ec8a0b174ef9fabd1a7c75fac Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 23:28:26 +0100 Subject: [PATCH 20/49] made ResourceOptimizedPlacement strategy public in case users want to make it a global acting strategy --- .../Placement/ResourceOptimizedPlacement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs index 16ac7b1cd0..87aa665f7d 100644 --- a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -12,7 +12,7 @@ namespace Orleans.Runtime; /// This contributes to avoiding resource saturation on the silos and especially newly joined silos. /// This placement strategy is configured by adding the attribute to a grain. /// -internal sealed class ResourceOptimizedPlacement : PlacementStrategy +public sealed class ResourceOptimizedPlacement : PlacementStrategy { internal static readonly ResourceOptimizedPlacement Singleton = new(); } From 32bc69564e407d88ad75865207ec3ec485769760 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Thu, 11 Jan 2024 23:35:16 +0100 Subject: [PATCH 21/49] removed IlocalSiloDetails as there is no need for it --- .../Placement/ResourceOptimizedPlacementDirector.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 93df5f503f..8f73474592 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -15,7 +15,6 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I Task _cachedLocalSilo; - readonly ILocalSiloDetails _localSiloDetails; readonly ResourceOptimizedPlacementOptions _options; readonly ConcurrentDictionary siloStatistics = []; @@ -29,11 +28,9 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I const float physicalMemoryScalingFactor = 0.00000095367431640625f; public ResourceOptimizedPlacementDirector( - ILocalSiloDetails localSiloDetails, DeploymentLoadPublisher deploymentLoadPublisher, IOptions options) { - _localSiloDetails = localSiloDetails; _options = options.Value; deploymentLoadPublisher?.SubscribeToStatisticsChangeEvents(this); } From 8fce9f88d32cae5219c27067920d8e7c66aceac9 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 00:01:23 +0100 Subject: [PATCH 22/49] xml docs --- .../Configuration/Options/ResourceOptimizedPlacementOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index b7fc24722d..40804c3344 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -64,7 +64,7 @@ public sealed class ResourceOptimizedPlacementOptions /// /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. /// - /// When this value is 0, then the policy will always favor the silo with the higher utilization score, even if that silo is remote to the current pending activation. + /// When this value is 0, then the policy will always favor the silo with the lower resource utilization, even if that silo is remote to the current pending activation. /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// From db27b617ae2845c0aefce504e496d01673b59f32 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 00:03:24 +0100 Subject: [PATCH 23/49] again xml docs --- .../Configuration/Options/ResourceOptimizedPlacementOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 40804c3344..ff37b51ea0 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -69,7 +69,7 @@ public sealed class ResourceOptimizedPlacementOptions /// /// /// - /// + /// Do favor a lower value for this e.g: 1-5 [%] /// Valid range is [0.00-1.00] /// public float LocalSiloPreferenceMargin { get; set; } From cc863e0965fcde513324e7765aa7eb8571516a67 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 00:20:18 +0100 Subject: [PATCH 24/49] incoorporated support for avoiding overloaded silos by definition of load shedding mechanism --- .../Placement/ResourceOptimizedPlacement.cs | 1 + .../Placement/ResourceOptimizedPlacementDirector.cs | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs index 87aa665f7d..5ebdf60474 100644 --- a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -10,6 +10,7 @@ namespace Orleans.Runtime; /// In addition to normalization, an online adaptive /// algorithm provides a smoothing effect (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. /// This contributes to avoiding resource saturation on the silos and especially newly joined silos. +/// Silos which are overloaded by definition of the load shedding mechanism are not considered as candidates for new placements. /// This placement strategy is configured by adding the attribute to a grain. /// public sealed class ResourceOptimizedPlacement : PlacementStrategy diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 8f73474592..9018b20f7f 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -11,7 +11,7 @@ namespace Orleans.Runtime.Placement; internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener { - readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory); + readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); Task _cachedLocalSilo; @@ -74,7 +74,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa List> relevantSilos = []; foreach (var silo in compatibleSilos) { - if (siloStatistics.TryGetValue(silo, out var stats)) + if (siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) { relevantSilos.Add(new(silo, stats)); } @@ -164,7 +164,8 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta statistics.CpuUsage, statistics.AvailableMemory, statistics.MemoryUsage, - statistics.TotalPhysicalMemory), + statistics.TotalPhysicalMemory, + statistics.IsOverloaded), updateValueFactory: (_, _) => { float estimatedCpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); @@ -175,7 +176,8 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta estimatedCpuUsage, estimatedAvailableMemory, estimatedMemoryUsage, - statistics.TotalPhysicalMemory); + statistics.TotalPhysicalMemory, + statistics.IsOverloaded); }); // details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ From 6ecb31d3797a9a73a0ddc0d5443f5686d0cfab80 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 11 Jan 2024 16:27:38 -0800 Subject: [PATCH 25/49] Apply suggestions from code review --- .../ResourceOptimizedPlacementOptions.cs | 5 +++ .../ResourceOptimizedPlacementDirector.cs | 42 +++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index ff37b51ea0..0602542eec 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -16,6 +16,7 @@ public sealed class ResourceOptimizedPlacementOptions /// Valid range is [0.00-1.00] /// public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; + /// /// The default value of . /// @@ -29,6 +30,7 @@ public sealed class ResourceOptimizedPlacementOptions /// Valid range is [0.00-1.00] /// public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; + /// /// The default value of . /// @@ -42,6 +44,7 @@ public sealed class ResourceOptimizedPlacementOptions /// Valid range is [0.00-1.00] /// public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; + /// /// The default value of . /// @@ -56,6 +59,7 @@ public sealed class ResourceOptimizedPlacementOptions /// Valid range is [0.00-1.00] /// public float PhysicalMemoryWeight { get; set; } = DEFAULT_PHYSICAL_MEMORY_WEIGHT; + /// /// The default value of . /// @@ -73,6 +77,7 @@ public sealed class ResourceOptimizedPlacementOptions /// Valid range is [0.00-1.00] /// public float LocalSiloPreferenceMargin { get; set; } + /// /// The default value of . /// diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 9018b20f7f..83989e2c1c 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -13,19 +13,19 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I { readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); - Task _cachedLocalSilo; - - readonly ResourceOptimizedPlacementOptions _options; - readonly ConcurrentDictionary siloStatistics = []; - - readonly DualModeKalmanFilter _cpuUsageFilter = new(); - readonly DualModeKalmanFilter _availableMemoryFilter = new(); - readonly DualModeKalmanFilter _memoryUsageFilter = new(); - /// /// 1 / (1024 * 1024) /// - const float physicalMemoryScalingFactor = 0.00000095367431640625f; + private const float PhysicalMemoryScalingFactor = 0.00000095367431640625f; + + private readonly ResourceOptimizedPlacementOptions _options; + private readonly ConcurrentDictionary siloStatistics = []; + + private readonly DualModeKalmanFilter _cpuUsageFilter = new(); + private readonly DualModeKalmanFilter _availableMemoryFilter = new(); + private readonly DualModeKalmanFilter _memoryUsageFilter = new(); + + private Task _cachedLocalSilo; public ResourceOptimizedPlacementDirector( DeploymentLoadPublisher deploymentLoadPublisher, @@ -69,7 +69,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return Task.FromResult(bestCandidate.Key); } - KeyValuePair GetBestSiloCandidate(SiloAddress[] compatibleSilos) + private KeyValuePair GetBestSiloCandidate(SiloAddress[] compatibleSilos) { List> relevantSilos = []; foreach (var silo in compatibleSilos) @@ -104,7 +104,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return winningSilo; } - bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) + private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) { if (context.LocalSiloStatus != SiloStatus.Active || !compatibleSilos.Contains(context.LocalSilo)) { @@ -135,7 +135,7 @@ bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSi /// physical_mem_weight * (1 / (1024 * 1024 * physical_mem) /// /// physical_mem is represented in [MB] to keep the result within [0-1] in cases of silos having physical_mem less than [1GB] - float CalculateScore(ResourceStatistics stats) + private float CalculateScore(ResourceStatistics stats) { float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; @@ -143,7 +143,7 @@ float CalculateScore(ResourceStatistics stats) { float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / physicalMemory : 0f; float normalizedAvailableMemory = 1 - (stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f); - float normalizedPhysicalMemory = physicalMemoryScalingFactor * physicalMemory; + float normalizedPhysicalMemory = PhysicalMemoryScalingFactor * physicalMemory; return _options.CpuUsageWeight * normalizedCpuUsage + _options.MemoryUsageWeight * normalizedMemoryUsage + @@ -181,14 +181,14 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta }); // details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ - sealed class DualModeKalmanFilter where T : unmanaged, INumber + private sealed class DualModeKalmanFilter where T : unmanaged, INumber { - readonly KalmanFilter _slowFilter = new(T.Zero); - readonly KalmanFilter _fastFilter = new(T.CreateChecked(0.01)); + private readonly KalmanFilter _slowFilter = new(T.Zero); + private readonly KalmanFilter _fastFilter = new(T.CreateChecked(0.01)); - FilterRegime _regime = FilterRegime.Slow; + private FilterRegime _regime = FilterRegime.Slow; - enum FilterRegime + private enum FilterRegime { Slow, Fast @@ -225,9 +225,9 @@ public T Filter(T? measurement) } } - sealed class KalmanFilter(T processNoiseCovariance) + private sealed class KalmanFilter(T processNoiseCovariance) { - readonly T _processNoiseCovariance = processNoiseCovariance; + private readonly T _processNoiseCovariance = processNoiseCovariance; public T PriorEstimate { get; private set; } = T.Zero; public T PriorErrorCovariance { get; private set; } = T.One; From 018443f801362e761303e98ff956611a397af78c Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 13:19:36 +0100 Subject: [PATCH 26/49] resolving some comments from reviewer --- .../Placement/ResourceOptimizedPlacement.cs | 8 ++++---- .../Placement/ResourceOptimizedPlacementDirector.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs index 5ebdf60474..4f6806d059 100644 --- a/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs +++ b/src/Orleans.Core.Abstractions/Placement/ResourceOptimizedPlacement.cs @@ -5,11 +5,11 @@ namespace Orleans.Runtime; /// /// /// It assigns weights to runtime statistics to prioritize different resources and calculates a normalized score for each silo. -/// The silo with the lowest score is chosen for placing the activation. Normalization ensures that each property contributes proportionally +/// Following the power of k-choices algorithm, K silos are picked as potential targets, where K is equal to the square root of the number of silos. +/// Out of those K silos, the one with the lowest score is chosen for placing the activation. Normalization ensures that each property contributes proportionally /// to the overall score. You can adjust the weights based on your specific requirements and priorities for load balancing. -/// In addition to normalization, an online adaptive -/// algorithm provides a smoothing effect (filters out high frequency components) and avoids rapid signal drops by transforming it into a polynomial alike decay process. -/// This contributes to avoiding resource saturation on the silos and especially newly joined silos. +/// In addition to normalization, an online adaptiv algorithm provides a smoothing effect (filters out high frequency components) and avoids rapid signal +/// drops by transforming it into a polynomial-like decay process. This contributes to avoiding resource saturation on the silos and especially newly joined silos. /// Silos which are overloaded by definition of the load shedding mechanism are not considered as candidates for new placements. /// This placement strategy is configured by adding the attribute to a grain. /// diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 83989e2c1c..b9cc4dec92 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -9,6 +9,7 @@ namespace Orleans.Runtime.Placement; +// details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener { readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); @@ -180,7 +181,6 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta statistics.IsOverloaded); }); - // details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ private sealed class DualModeKalmanFilter where T : unmanaged, INumber { private readonly KalmanFilter _slowFilter = new(T.Zero); From b056cf28f1bd7b3e19f23446b573f01ce1046ef6 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 16:35:16 +0100 Subject: [PATCH 27/49] addressing further comments --- .../ResourceOptimizedPlacementDirector.cs | 86 +++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index b9cc4dec92..644cad6839 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -1,8 +1,10 @@ using System; +using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Orleans.Runtime.Configuration.Options; @@ -18,9 +20,10 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I /// 1 / (1024 * 1024) /// private const float PhysicalMemoryScalingFactor = 0.00000095367431640625f; - + private const int OneKiloByte = 1024; + private readonly ResourceOptimizedPlacementOptions _options; - private readonly ConcurrentDictionary siloStatistics = []; + private readonly ConcurrentDictionary _siloStatistics = []; private readonly DualModeKalmanFilter _cpuUsageFilter = new(); private readonly DualModeKalmanFilter _availableMemoryFilter = new(); @@ -55,13 +58,12 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return Task.FromResult(compatibleSilos[0]); } - if (siloStatistics.IsEmpty) + if (_siloStatistics.IsEmpty) { return Task.FromResult(compatibleSilos[Random.Shared.Next(compatibleSilos.Length)]); } var bestCandidate = GetBestSiloCandidate(compatibleSilos); - if (IsLocalSiloPreferable(context, compatibleSilos, bestCandidate.Value)) { return _cachedLocalSilo ??= Task.FromResult(context.LocalSilo); @@ -75,7 +77,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa List> relevantSilos = []; foreach (var silo in compatibleSilos) { - if (siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) + if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) { relevantSilos.Add(new(silo, stats)); } @@ -105,6 +107,74 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return winningSilo; } + private KeyValuePair GetBestSiloCandidate_V2(SiloAddress[] compatibleSilos) + { + KeyValuePair pick; + + int compatibleSilosCount = compatibleSilos.Length; + if (compatibleSilosCount * Unsafe.SizeOf>() <= OneKiloByte) // it is good practice not to allocate more than 1 kilobyte of memory on the stack + { + pick = MakePick(stackalloc KeyValuePair[compatibleSilosCount]); + } + else + { + var relevantSilos = ArrayPool>.Shared.Rent(compatibleSilosCount); + pick = MakePick(relevantSilos.AsSpan()); + ArrayPool>.Shared.Return(relevantSilos); + } + + foreach (var silo in compatibleSilos) + { + if (silo.GetConsistentHashCode() == pick.Key) + { + return new KeyValuePair(silo, pick.Value); + } + } + + // It should never come to this point, unless 'GetConsistentHashCode' isnt consistent, which if its the case, + // this code can act as a 'tester' for that. This would be exceptional, so its better to stop the program. + throw new InvalidOperationException("No hash code from the list of compatible silos matched the picked silo's hash code."); + + KeyValuePair MakePick(Span> relevantSilos) + { + int relevantSilosCount = 0; + foreach (var silo in compatibleSilos) + { + if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) + { + relevantSilos[relevantSilosCount++] = new(silo.GetConsistentHashCode(), stats); + } + } + + int chooseFrom = (int)Math.Ceiling(Math.Sqrt(relevantSilosCount)); + var chooseFromSilos = Random.Shared.GetItems>(relevantSilos, chooseFrom).AsSpan(); + + int cursor = 0; + int addressHashCode = 0; + float lowestScore = 1; + + while (cursor < chooseFrom) + { + var silo = chooseFromSilos[cursor]; + + float siloScore = CalculateScore(silo.Value); + // its very unlikley, but there could be more than 1 silo that has the same score, + // so we apply some jittering to avoid pick the first one in the short-list. + float scoreJitter = Random.Shared.NextSingle() / 100_000; + + if (siloScore + scoreJitter < lowestScore) + { + lowestScore = siloScore; + addressHashCode = silo.Key; + } + + cursor++; + } + + return new(addressHashCode, lowestScore); + } + } + private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) { if (context.LocalSiloStatus != SiloStatus.Active || !compatibleSilos.Contains(context.LocalSilo)) @@ -112,7 +182,7 @@ private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] comp return false; } - if (siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) + if (_siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) { float localScore = CalculateScore(localStats); float scoreDiff = Math.Abs(localScore - bestCandidateScore); @@ -156,10 +226,10 @@ private float CalculateScore(ResourceStatistics stats) } public void RemoveSilo(SiloAddress address) - => siloStatistics.TryRemove(address, out _); + => _siloStatistics.TryRemove(address, out _); public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeStatistics statistics) - => siloStatistics.AddOrUpdate( + => _siloStatistics.AddOrUpdate( key: address, addValue: new ResourceStatistics( statistics.CpuUsage, From 84fb563bf042c14c923552e5863e382c0d70d58e Mon Sep 17 00:00:00 2001 From: ReubenBond Date: Fri, 12 Jan 2024 07:41:30 -0800 Subject: [PATCH 28/49] PR feedback --- .../Placement/ResourceOptimizedPlacementDirector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 644cad6839..a4316c72c6 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -14,7 +14,7 @@ namespace Orleans.Runtime.Placement; // details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener { - readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); + private readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); /// /// 1 / (1024 * 1024) From 32b1a610a14e13dfc826f8d5d70f2b6d38f584a9 Mon Sep 17 00:00:00 2001 From: ReubenBond Date: Fri, 12 Jan 2024 08:31:12 -0800 Subject: [PATCH 29/49] PR feedback --- .../ResourceOptimizedPlacementDirector.cs | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index a4316c72c6..aef804eb16 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; @@ -110,9 +111,10 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa private KeyValuePair GetBestSiloCandidate_V2(SiloAddress[] compatibleSilos) { KeyValuePair pick; - int compatibleSilosCount = compatibleSilos.Length; - if (compatibleSilosCount * Unsafe.SizeOf>() <= OneKiloByte) // it is good practice not to allocate more than 1 kilobyte of memory on the stack + + // It is good practice not to allocate more than 1 kilobyte of memory on the stack + if (compatibleSilosCount * Unsafe.SizeOf>() <= OneKiloByte) { pick = MakePick(stackalloc KeyValuePair[compatibleSilosCount]); } @@ -123,44 +125,35 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa ArrayPool>.Shared.Return(relevantSilos); } - foreach (var silo in compatibleSilos) - { - if (silo.GetConsistentHashCode() == pick.Key) - { - return new KeyValuePair(silo, pick.Value); - } - } - - // It should never come to this point, unless 'GetConsistentHashCode' isnt consistent, which if its the case, - // this code can act as a 'tester' for that. This would be exceptional, so its better to stop the program. - throw new InvalidOperationException("No hash code from the list of compatible silos matched the picked silo's hash code."); + return new KeyValuePair(compatibleSilos[pick.Key], pick.Value); KeyValuePair MakePick(Span> relevantSilos) { int relevantSilosCount = 0; - foreach (var silo in compatibleSilos) + for (var i = 0; i < compatibleSilos.Length; ++i) { + var silo = compatibleSilos[i]; if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) { - relevantSilos[relevantSilosCount++] = new(silo.GetConsistentHashCode(), stats); + relevantSilos[relevantSilosCount++] = new(i, stats); } } - int chooseFrom = (int)Math.Ceiling(Math.Sqrt(relevantSilosCount)); - var chooseFromSilos = Random.Shared.GetItems>(relevantSilos, chooseFrom).AsSpan(); + int candidateCount = (int)Math.Ceiling(Math.Sqrt(relevantSilosCount)); + ShufflePrefix(relevantSilos, candidateCount); + var candidates = relevantSilos[0..candidateCount]; int cursor = 0; int addressHashCode = 0; float lowestScore = 1; - while (cursor < chooseFrom) + foreach (var silo in candidates) { - var silo = chooseFromSilos[cursor]; - float siloScore = CalculateScore(silo.Value); - // its very unlikley, but there could be more than 1 silo that has the same score, + + // It's very unlikely, but there could be more than 1 silo that has the same score, // so we apply some jittering to avoid pick the first one in the short-list. - float scoreJitter = Random.Shared.NextSingle() / 100_000; + float scoreJitter = Random.Shared.NextSingle() / 100_000f; if (siloScore + scoreJitter < lowestScore) { @@ -173,6 +166,22 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return new(addressHashCode, lowestScore); } + + // Variant of the Modern Fisher-Yates shuffle which stops after shuffling the first `prefixLength` elements, + // which are the only elements we are interested in. + // See: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + static void ShufflePrefix(Span> values, int prefixLength) + { + var max = values.Length; + for (var i = 0; i < prefixLength; i++) + { + var chosen = Random.Shared.Next(i, max); + if (chosen != i) + { + (values[chosen], values[i]) = (values[i], values[chosen]); + } + } + } } private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] compatibleSilos, float bestCandidateScore) From 890ef27cbee7da7f4603b4da67b11b6784ee2659 Mon Sep 17 00:00:00 2001 From: ReubenBond Date: Fri, 12 Jan 2024 09:07:46 -0800 Subject: [PATCH 30/49] Clean up changes to GetBestSiloCandidate_V2 --- .../ResourceOptimizedPlacementDirector.cs | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index aef804eb16..e9ff64606f 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -110,25 +110,26 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa private KeyValuePair GetBestSiloCandidate_V2(SiloAddress[] compatibleSilos) { - KeyValuePair pick; + (int Index, float Score) pick; int compatibleSilosCount = compatibleSilos.Length; // It is good practice not to allocate more than 1 kilobyte of memory on the stack - if (compatibleSilosCount * Unsafe.SizeOf>() <= OneKiloByte) + if (compatibleSilosCount * Unsafe.SizeOf<(int, ResourceStatistics)>() <= OneKiloByte) { - pick = MakePick(stackalloc KeyValuePair[compatibleSilosCount]); + pick = MakePick(stackalloc (int, ResourceStatistics)[compatibleSilosCount]); } else { - var relevantSilos = ArrayPool>.Shared.Rent(compatibleSilosCount); + var relevantSilos = ArrayPool<(int, ResourceStatistics)>.Shared.Rent(compatibleSilosCount); pick = MakePick(relevantSilos.AsSpan()); - ArrayPool>.Shared.Return(relevantSilos); + ArrayPool<(int, ResourceStatistics)>.Shared.Return(relevantSilos); } - return new KeyValuePair(compatibleSilos[pick.Key], pick.Value); + return new KeyValuePair(compatibleSilos[pick.Index], pick.Score); - KeyValuePair MakePick(Span> relevantSilos) + (int, float) MakePick(Span<(int SiloIndex, ResourceStatistics SiloStatistics)> relevantSilos) { + // Get all compatible silos int relevantSilosCount = 0; for (var i = 0; i < compatibleSilos.Length; ++i) { @@ -139,17 +140,23 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa } } + // Limit to the number of candidates added. + relevantSilos = relevantSilos[0..relevantSilosCount]; + Debug.Assert(relevantSilos.Length == relevantSilosCount); + + // Pick K silos from the list of compatible silos, where K is equal to the square root of the number of silos. + // Eg, from 10 silos, we choose from 4. int candidateCount = (int)Math.Ceiling(Math.Sqrt(relevantSilosCount)); ShufflePrefix(relevantSilos, candidateCount); var candidates = relevantSilos[0..candidateCount]; int cursor = 0; - int addressHashCode = 0; + int siloIndex = 0; float lowestScore = 1; foreach (var silo in candidates) { - float siloScore = CalculateScore(silo.Value); + float siloScore = CalculateScore(silo.SiloStatistics); // It's very unlikely, but there could be more than 1 silo that has the same score, // so we apply some jittering to avoid pick the first one in the short-list. @@ -158,20 +165,22 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa if (siloScore + scoreJitter < lowestScore) { lowestScore = siloScore; - addressHashCode = silo.Key; + siloIndex = silo.SiloIndex; } cursor++; } - return new(addressHashCode, lowestScore); + return new(siloIndex, lowestScore); } // Variant of the Modern Fisher-Yates shuffle which stops after shuffling the first `prefixLength` elements, // which are the only elements we are interested in. // See: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle - static void ShufflePrefix(Span> values, int prefixLength) + static void ShufflePrefix(Span<(int SiloIndex, ResourceStatistics SiloStatistics)> values, int prefixLength) { + Debug.Assert(prefixLength >= 0); + Debug.Assert(prefixLength <= values.Length); var max = values.Length; for (var i = 0; i < prefixLength; i++) { From fdad669f199774de1efcf443fa91165b31374d1a Mon Sep 17 00:00:00 2001 From: ReubenBond Date: Fri, 12 Jan 2024 09:16:27 -0800 Subject: [PATCH 31/49] More tidying --- .../ResourceOptimizedPlacementDirector.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index e9ff64606f..51cd28a0c7 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -127,7 +127,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa return new KeyValuePair(compatibleSilos[pick.Index], pick.Score); - (int, float) MakePick(Span<(int SiloIndex, ResourceStatistics SiloStatistics)> relevantSilos) + (int, float) MakePick(Span<(int, ResourceStatistics)> relevantSilos) { // Get all compatible silos int relevantSilosCount = 0; @@ -150,28 +150,23 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa ShufflePrefix(relevantSilos, candidateCount); var candidates = relevantSilos[0..candidateCount]; - int cursor = 0; - int siloIndex = 0; - float lowestScore = 1; + (int Index, float Score) pick = (0, 1f); - foreach (var silo in candidates) + foreach (var (index, statistics) in candidates) { - float siloScore = CalculateScore(silo.SiloStatistics); + float score = CalculateScore(statistics); // It's very unlikely, but there could be more than 1 silo that has the same score, // so we apply some jittering to avoid pick the first one in the short-list. float scoreJitter = Random.Shared.NextSingle() / 100_000f; - if (siloScore + scoreJitter < lowestScore) + if (score + scoreJitter < pick.Score) { - lowestScore = siloScore; - siloIndex = silo.SiloIndex; + pick = (index, score); } - - cursor++; } - return new(siloIndex, lowestScore); + return pick; } // Variant of the Modern Fisher-Yates shuffle which stops after shuffling the first `prefixLength` elements, From 0300d95404f76ce7b7dd0bf6170a65eddf97b5f8 Mon Sep 17 00:00:00 2001 From: ReubenBond Date: Fri, 12 Jan 2024 10:12:51 -0800 Subject: [PATCH 32/49] Filter inputs per-silo --- .../ResourceOptimizedPlacementDirector.cs | 89 ++++++++++--------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 51cd28a0c7..e8d1da5ceb 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -15,8 +15,6 @@ namespace Orleans.Runtime.Placement; // details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener { - private readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); - /// /// 1 / (1024 * 1024) /// @@ -24,11 +22,7 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I private const int OneKiloByte = 1024; private readonly ResourceOptimizedPlacementOptions _options; - private readonly ConcurrentDictionary _siloStatistics = []; - - private readonly DualModeKalmanFilter _cpuUsageFilter = new(); - private readonly DualModeKalmanFilter _availableMemoryFilter = new(); - private readonly DualModeKalmanFilter _memoryUsageFilter = new(); + private readonly ConcurrentDictionary _siloStatistics = []; private Task _cachedLocalSilo; @@ -78,9 +72,9 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa List> relevantSilos = []; foreach (var silo in compatibleSilos) { - if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) + if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.Value.IsOverloaded) { - relevantSilos.Add(new(silo, stats)); + relevantSilos.Add(new(silo, stats.Value)); } } @@ -134,9 +128,9 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa for (var i = 0; i < compatibleSilos.Length; ++i) { var silo = compatibleSilos[i]; - if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.IsOverloaded) + if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.Value.IsOverloaded) { - relevantSilos[relevantSilosCount++] = new(i, stats); + relevantSilos[relevantSilosCount++] = new(i, stats.Value); } } @@ -197,7 +191,7 @@ private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] comp if (_siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) { - float localScore = CalculateScore(localStats); + float localScore = CalculateScore(localStats.Value); float scoreDiff = Math.Abs(localScore - bestCandidateScore); if (_options.LocalSiloPreferenceMargin >= scoreDiff) @@ -243,31 +237,44 @@ public void RemoveSilo(SiloAddress address) public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeStatistics statistics) => _siloStatistics.AddOrUpdate( - key: address, - addValue: new ResourceStatistics( - statistics.CpuUsage, - statistics.AvailableMemory, - statistics.MemoryUsage, - statistics.TotalPhysicalMemory, - statistics.IsOverloaded), - updateValueFactory: (_, _) => + address, + addValueFactory: static (_, statistics) => new (statistics), + updateValueFactory: static (_, existing, statistics) => { - float estimatedCpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); - float estimatedAvailableMemory = _availableMemoryFilter.Filter(statistics.AvailableMemory); - long estimatedMemoryUsage = _memoryUsageFilter.Filter(statistics.MemoryUsage); - - return new ResourceStatistics( - estimatedCpuUsage, - estimatedAvailableMemory, - estimatedMemoryUsage, - statistics.TotalPhysicalMemory, - statistics.IsOverloaded); - }); + existing.Update(statistics); + return existing; + }, + statistics); + + private readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); + + private sealed class FilteredSiloStatistics(SiloRuntimeStatistics statistics) + { + private readonly DualModeKalmanFilter _cpuUsageFilter = new(); + private readonly DualModeKalmanFilter _availableMemoryFilter = new(); + private readonly DualModeKalmanFilter _memoryUsageFilter = new(); + + public ResourceStatistics Value { get; private set; } + = new(statistics.CpuUsage, statistics.AvailableMemory, statistics.MemoryUsage, statistics.TotalPhysicalMemory, statistics.IsOverloaded); + + public void Update(SiloRuntimeStatistics statistics) + { + Value = new( + CpuUsage: _cpuUsageFilter.Filter(statistics.CpuUsage), + AvailableMemory: _availableMemoryFilter.Filter(statistics.AvailableMemory), + MemoryUsage: _memoryUsageFilter.Filter(statistics.MemoryUsage), + TotalPhysicalMemory: statistics.TotalPhysicalMemory, + IsOverloaded: statistics.IsOverloaded); + } + } + private sealed class DualModeKalmanFilter where T : unmanaged, INumber { - private readonly KalmanFilter _slowFilter = new(T.Zero); - private readonly KalmanFilter _fastFilter = new(T.CreateChecked(0.01)); + private static T SlowFilterProcessNoiseCovariance => T.Zero; + private static T FastFilterProcessNoiseCovariance => T.CreateChecked(0.01); + private KalmanFilter _slowFilter = new(); + private KalmanFilter _fastFilter = new(); private FilterRegime _regime = FilterRegime.Slow; @@ -281,8 +288,8 @@ public T Filter(T? measurement) { T _measurement = measurement ?? T.Zero; - T slowEstimate = _slowFilter.Filter(_measurement); - T fastEstimate = _fastFilter.Filter(_measurement); + T slowEstimate = _slowFilter.Filter(_measurement, SlowFilterProcessNoiseCovariance); + T fastEstimate = _fastFilter.Filter(_measurement, FastFilterProcessNoiseCovariance); if (_measurement > slowEstimate) { @@ -290,7 +297,7 @@ public T Filter(T? measurement) { _regime = FilterRegime.Fast; _fastFilter.SetState(_measurement, T.Zero); - fastEstimate = _fastFilter.Filter(_measurement); + fastEstimate = _fastFilter.Filter(_measurement, FastFilterProcessNoiseCovariance); } return fastEstimate; @@ -301,17 +308,15 @@ public T Filter(T? measurement) { _regime = FilterRegime.Slow; _slowFilter.SetState(_fastFilter.PriorEstimate, _fastFilter.PriorErrorCovariance); - slowEstimate = _slowFilter.Filter(_measurement); + slowEstimate = _slowFilter.Filter(_measurement, SlowFilterProcessNoiseCovariance); } return slowEstimate; } } - private sealed class KalmanFilter(T processNoiseCovariance) + private struct KalmanFilter() { - private readonly T _processNoiseCovariance = processNoiseCovariance; - public T PriorEstimate { get; private set; } = T.Zero; public T PriorErrorCovariance { get; private set; } = T.One; @@ -321,10 +326,10 @@ public void SetState(T estimate, T errorCovariance) PriorErrorCovariance = errorCovariance; } - public T Filter(T measurement) + public T Filter(T measurement, T processNoiseCovariance) { T estimate = PriorEstimate; - T errorCovariance = PriorErrorCovariance + _processNoiseCovariance; + T errorCovariance = PriorErrorCovariance + processNoiseCovariance; T gain = errorCovariance / (errorCovariance + T.One); T newEstimate = estimate + gain * (measurement - estimate); From 66f0380410a2396245d055558ba5b2b23978be0f Mon Sep 17 00:00:00 2001 From: ReubenBond Date: Fri, 12 Jan 2024 10:30:04 -0800 Subject: [PATCH 33/49] More cleanup --- .../ResourceOptimizedPlacementDirector.cs | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index e8d1da5ceb..918eb37fbe 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Orleans.Runtime.Configuration.Options; @@ -72,9 +73,13 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa List> relevantSilos = []; foreach (var silo in compatibleSilos) { - if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.Value.IsOverloaded) + if (_siloStatistics.TryGetValue(silo, out var stats)) { - relevantSilos.Add(new(silo, stats.Value)); + var filteredValue = stats.Value; + if (!filteredValue.IsOverloaded) + { + relevantSilos.Add(new(silo, filteredValue)); + } } } @@ -128,9 +133,13 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa for (var i = 0; i < compatibleSilos.Length; ++i) { var silo = compatibleSilos[i]; - if (_siloStatistics.TryGetValue(silo, out var stats) && !stats.Value.IsOverloaded) + if (_siloStatistics.TryGetValue(silo, out var stats)) { - relevantSilos[relevantSilosCount++] = new(i, stats.Value); + var filteredStats = stats.Value; + if (!filteredStats.IsOverloaded) + { + relevantSilos[relevantSilosCount++] = new(i, filteredStats); + } } } @@ -254,25 +263,26 @@ private sealed class FilteredSiloStatistics(SiloRuntimeStatistics statistics) private readonly DualModeKalmanFilter _availableMemoryFilter = new(); private readonly DualModeKalmanFilter _memoryUsageFilter = new(); - public ResourceStatistics Value { get; private set; } - = new(statistics.CpuUsage, statistics.AvailableMemory, statistics.MemoryUsage, statistics.TotalPhysicalMemory, statistics.IsOverloaded); + private float? _cpuUsage = statistics.CpuUsage; + private float? _availableMemory = statistics.AvailableMemory; + private long? _memoryUsage = statistics.MemoryUsage; + private long? _totalPhysicalMemory = statistics.TotalPhysicalMemory; + private bool _isOverloaded = statistics.IsOverloaded; + + public ResourceStatistics Value => new(_cpuUsage, _availableMemory, _memoryUsage, _totalPhysicalMemory, _isOverloaded); public void Update(SiloRuntimeStatistics statistics) { - Value = new( - CpuUsage: _cpuUsageFilter.Filter(statistics.CpuUsage), - AvailableMemory: _availableMemoryFilter.Filter(statistics.AvailableMemory), - MemoryUsage: _memoryUsageFilter.Filter(statistics.MemoryUsage), - TotalPhysicalMemory: statistics.TotalPhysicalMemory, - IsOverloaded: statistics.IsOverloaded); + _cpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); + _availableMemory = _availableMemoryFilter.Filter(statistics.AvailableMemory); + _memoryUsage = _memoryUsageFilter.Filter(statistics.MemoryUsage); + _totalPhysicalMemory = statistics.TotalPhysicalMemory; + _isOverloaded = statistics.IsOverloaded; } } - private sealed class DualModeKalmanFilter where T : unmanaged, INumber { - private static T SlowFilterProcessNoiseCovariance => T.Zero; - private static T FastFilterProcessNoiseCovariance => T.CreateChecked(0.01); private KalmanFilter _slowFilter = new(); private KalmanFilter _fastFilter = new(); @@ -288,8 +298,11 @@ public T Filter(T? measurement) { T _measurement = measurement ?? T.Zero; - T slowEstimate = _slowFilter.Filter(_measurement, SlowFilterProcessNoiseCovariance); - T fastEstimate = _fastFilter.Filter(_measurement, FastFilterProcessNoiseCovariance); + T slowCovariance = T.Zero; + T fastCovariance = T.CreateChecked(0.01); + + T slowEstimate = _slowFilter.Filter(_measurement, slowCovariance); + T fastEstimate = _fastFilter.Filter(_measurement, fastCovariance); if (_measurement > slowEstimate) { @@ -297,7 +310,7 @@ public T Filter(T? measurement) { _regime = FilterRegime.Fast; _fastFilter.SetState(_measurement, T.Zero); - fastEstimate = _fastFilter.Filter(_measurement, FastFilterProcessNoiseCovariance); + fastEstimate = _fastFilter.Filter(_measurement, fastCovariance); } return fastEstimate; @@ -308,7 +321,7 @@ public T Filter(T? measurement) { _regime = FilterRegime.Slow; _slowFilter.SetState(_fastFilter.PriorEstimate, _fastFilter.PriorErrorCovariance); - slowEstimate = _slowFilter.Filter(_measurement, SlowFilterProcessNoiseCovariance); + slowEstimate = _slowFilter.Filter(_measurement, slowCovariance); } return slowEstimate; From f74edfa5af3ba7760e9271f9566f969456c6c04c Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 20:26:00 +0100 Subject: [PATCH 34/49] fixed comment --- .../Configuration/Options/ResourceOptimizedPlacementOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 0602542eec..71fefed40f 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -69,7 +69,7 @@ public sealed class ResourceOptimizedPlacementOptions /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. /// /// When this value is 0, then the policy will always favor the silo with the lower resource utilization, even if that silo is remote to the current pending activation. - /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . + /// When this value is 1, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// /// From f05a584232dd4421378ab01aedb36c373bcee01b Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 20:27:55 +0100 Subject: [PATCH 35/49] more comments --- .../Configuration/Options/ResourceOptimizedPlacementOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 71fefed40f..b149a236ab 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -73,7 +73,7 @@ public sealed class ResourceOptimizedPlacementOptions /// /// /// - /// Do favor a lower value for this e.g: 1-5 [%] + /// Do favor a lower value for this e.g: 0.01-0.05 /// Valid range is [0.00-1.00] /// public float LocalSiloPreferenceMargin { get; set; } From f85d341bbe98790d9e86bf906b68964f73a87e33 Mon Sep 17 00:00:00 2001 From: ReubenBond Date: Fri, 12 Jan 2024 11:46:19 -0800 Subject: [PATCH 36/49] clarify comment --- .../Placement/ResourceOptimizedPlacementDirector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 918eb37fbe..e07e2e3342 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -128,7 +128,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa (int, float) MakePick(Span<(int, ResourceStatistics)> relevantSilos) { - // Get all compatible silos + // Get all compatible silos which aren't overloaded int relevantSilosCount = 0; for (var i = 0; i < compatibleSilos.Length; ++i) { From 1c648541a96a4e595f9bd8a39fa1fa5d20ad7fc8 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 20:54:43 +0100 Subject: [PATCH 37/49] comment on the rational behind using a dual-mode KF as opposed to single KF --- .../Placement/ResourceOptimizedPlacementDirector.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 918eb37fbe..20dc909dc4 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -281,6 +281,9 @@ public void Update(SiloRuntimeStatistics statistics) } } + // The rational behind using a dual-mode KF, is that we want the input signal to follow a trajectory that + // decays with a slower rate than the origianl one, but also tracks the signal in case of signal increases + // (which represent potential of overloading). Both are important, but they are inversely correlated to each other. private sealed class DualModeKalmanFilter where T : unmanaged, INumber { private KalmanFilter _slowFilter = new(); From 279982406fba093def6fe255650837f0a66a4d66 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 22:23:11 +0100 Subject: [PATCH 38/49] some more changes --- .../Placement/ResourceOptimizedPlacementDirector.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index b821be4e33..59a1c97419 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Orleans.Runtime.Configuration.Options; @@ -20,7 +19,7 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I /// 1 / (1024 * 1024) /// private const float PhysicalMemoryScalingFactor = 0.00000095367431640625f; - private const int OneKiloByte = 1024; + private const int FourKiloByte = 4096; private readonly ResourceOptimizedPlacementOptions _options; private readonly ConcurrentDictionary _siloStatistics = []; @@ -113,7 +112,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa int compatibleSilosCount = compatibleSilos.Length; // It is good practice not to allocate more than 1 kilobyte of memory on the stack - if (compatibleSilosCount * Unsafe.SizeOf<(int, ResourceStatistics)>() <= OneKiloByte) + if (compatibleSilosCount * Unsafe.SizeOf<(int, ResourceStatistics)>() <= FourKiloByte) { pick = MakePick(stackalloc (int, ResourceStatistics)[compatibleSilosCount]); } @@ -261,7 +260,7 @@ private sealed class FilteredSiloStatistics(SiloRuntimeStatistics statistics) { private readonly DualModeKalmanFilter _cpuUsageFilter = new(); private readonly DualModeKalmanFilter _availableMemoryFilter = new(); - private readonly DualModeKalmanFilter _memoryUsageFilter = new(); + private readonly DualModeKalmanFilter _memoryUsageFilter = new(); private float? _cpuUsage = statistics.CpuUsage; private float? _availableMemory = statistics.AvailableMemory; @@ -275,7 +274,7 @@ public void Update(SiloRuntimeStatistics statistics) { _cpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); _availableMemory = _availableMemoryFilter.Filter(statistics.AvailableMemory); - _memoryUsage = _memoryUsageFilter.Filter(statistics.MemoryUsage); + _memoryUsage = (long)_memoryUsageFilter.Filter((double)statistics.MemoryUsage); _totalPhysicalMemory = statistics.TotalPhysicalMemory; _isOverloaded = statistics.IsOverloaded; } From 053de964b9b48353f5874a3c72f7ef9c7184a5c7 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 22:28:54 +0100 Subject: [PATCH 39/49] PhysicalMemoryWeight --- .../Placement/ResourceOptimizedPlacementDirector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 59a1c97419..92a432deae 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -234,7 +234,7 @@ private float CalculateScore(ResourceStatistics stats) return _options.CpuUsageWeight * normalizedCpuUsage + _options.MemoryUsageWeight * normalizedMemoryUsage + _options.AvailableMemoryWeight * normalizedAvailableMemory + - _options.AvailableMemoryWeight * normalizedPhysicalMemory; + _options.PhysicalMemoryWeight * normalizedPhysicalMemory; } return _options.CpuUsageWeight * normalizedCpuUsage; From eafe948720c8521d4ec54191db3a05c2cb1d6d9f Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 23:22:34 +0100 Subject: [PATCH 40/49] switched to GetBestSiloCandidate v2 --- .../ResourceOptimizedPlacementDirector.cs | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 92a432deae..f9e334334b 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -68,45 +68,6 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa } private KeyValuePair GetBestSiloCandidate(SiloAddress[] compatibleSilos) - { - List> relevantSilos = []; - foreach (var silo in compatibleSilos) - { - if (_siloStatistics.TryGetValue(silo, out var stats)) - { - var filteredValue = stats.Value; - if (!filteredValue.IsOverloaded) - { - relevantSilos.Add(new(silo, filteredValue)); - } - } - } - - int chooseFrom = (int)Math.Ceiling(Math.Sqrt(relevantSilos.Count)); - Dictionary chooseFromSilos = []; - - while (chooseFromSilos.Count < chooseFrom) - { - int index = Random.Shared.Next(relevantSilos.Count); - var pickedSilo = relevantSilos[index]; - - relevantSilos.RemoveAt(index); - - float score = CalculateScore(pickedSilo.Value); - chooseFromSilos.Add(pickedSilo.Key, score); - } - - var orderedByLowestScore = chooseFromSilos.OrderBy(kv => kv.Value); - - // there could be more than 1 silo that has the same score, we pick 1 of them randomly so that we dont continuously pick the first one. - var lowestScore = orderedByLowestScore.First().Value; - var shortListedSilos = orderedByLowestScore.TakeWhile(p => p.Value == lowestScore).ToList(); - var winningSilo = shortListedSilos[Random.Shared.Next(shortListedSilos.Count)]; - - return winningSilo; - } - - private KeyValuePair GetBestSiloCandidate_V2(SiloAddress[] compatibleSilos) { (int Index, float Score) pick; int compatibleSilosCount = compatibleSilos.Length; From bd04f7737e2aa4364078469c69cdfedbeef2ac20 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Fri, 12 Jan 2024 23:22:59 +0100 Subject: [PATCH 41/49] # --- .../Placement/ResourceOptimizedPlacementDirector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index f9e334334b..2e9438f61c 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -12,7 +12,7 @@ namespace Orleans.Runtime.Placement; -// details: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ +// See: https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, ISiloStatisticsChangeListener { /// From 77414bc7ec3d7a79e7755ca841a3f58c3f9a2aaf Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Sat, 13 Jan 2024 00:06:26 +0100 Subject: [PATCH 42/49] switched to non-generic CDM-KF --- .../ResourceOptimizedPlacementDirector.cs | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 2e9438f61c..c80ab63193 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Numerics; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.Extensions.Options; @@ -207,7 +206,7 @@ public void RemoveSilo(SiloAddress address) public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeStatistics statistics) => _siloStatistics.AddOrUpdate( address, - addValueFactory: static (_, statistics) => new (statistics), + addValueFactory: static (_, statistics) => new(statistics), updateValueFactory: static (_, existing, statistics) => { existing.Update(statistics); @@ -219,9 +218,9 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta private sealed class FilteredSiloStatistics(SiloRuntimeStatistics statistics) { - private readonly DualModeKalmanFilter _cpuUsageFilter = new(); - private readonly DualModeKalmanFilter _availableMemoryFilter = new(); - private readonly DualModeKalmanFilter _memoryUsageFilter = new(); + private readonly DualModeKalmanFilter _cpuUsageFilter = new(); + private readonly DualModeKalmanFilter _availableMemoryFilter = new(); + private readonly DualModeKalmanFilter _memoryUsageFilter = new(); private float? _cpuUsage = statistics.CpuUsage; private float? _availableMemory = statistics.AvailableMemory; @@ -235,7 +234,7 @@ public void Update(SiloRuntimeStatistics statistics) { _cpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); _availableMemory = _availableMemoryFilter.Filter(statistics.AvailableMemory); - _memoryUsage = (long)_memoryUsageFilter.Filter((double)statistics.MemoryUsage); + _memoryUsage = (long)_memoryUsageFilter.Filter((float)statistics.MemoryUsage); _totalPhysicalMemory = statistics.TotalPhysicalMemory; _isOverloaded = statistics.IsOverloaded; } @@ -244,11 +243,14 @@ public void Update(SiloRuntimeStatistics statistics) // The rational behind using a dual-mode KF, is that we want the input signal to follow a trajectory that // decays with a slower rate than the origianl one, but also tracks the signal in case of signal increases // (which represent potential of overloading). Both are important, but they are inversely correlated to each other. - private sealed class DualModeKalmanFilter where T : unmanaged, INumber + private sealed class DualModeKalmanFilter { + private const float SlowProcessNoiseCovariance = 0f; + private const float FastProcessNoiseCovariance = 0.01f; + private KalmanFilter _slowFilter = new(); private KalmanFilter _fastFilter = new(); - + private FilterRegime _regime = FilterRegime.Slow; private enum FilterRegime @@ -257,23 +259,20 @@ private enum FilterRegime Fast } - public T Filter(T? measurement) + public float Filter(float? measurement) { - T _measurement = measurement ?? T.Zero; - - T slowCovariance = T.Zero; - T fastCovariance = T.CreateChecked(0.01); + float _measurement = measurement ?? 0f; - T slowEstimate = _slowFilter.Filter(_measurement, slowCovariance); - T fastEstimate = _fastFilter.Filter(_measurement, fastCovariance); + float slowEstimate = _slowFilter.Filter(_measurement, SlowProcessNoiseCovariance); + float fastEstimate = _fastFilter.Filter(_measurement, FastProcessNoiseCovariance); if (_measurement > slowEstimate) { if (_regime == FilterRegime.Slow) { _regime = FilterRegime.Fast; - _fastFilter.SetState(_measurement, T.Zero); - fastEstimate = _fastFilter.Filter(_measurement, fastCovariance); + _fastFilter.SetState(_measurement, 0f); + fastEstimate = _fastFilter.Filter(_measurement, FastProcessNoiseCovariance); } return fastEstimate; @@ -284,7 +283,7 @@ public T Filter(T? measurement) { _regime = FilterRegime.Slow; _slowFilter.SetState(_fastFilter.PriorEstimate, _fastFilter.PriorErrorCovariance); - slowEstimate = _slowFilter.Filter(_measurement, slowCovariance); + slowEstimate = _slowFilter.Filter(_measurement, SlowProcessNoiseCovariance); } return slowEstimate; @@ -293,23 +292,23 @@ public T Filter(T? measurement) private struct KalmanFilter() { - public T PriorEstimate { get; private set; } = T.Zero; - public T PriorErrorCovariance { get; private set; } = T.One; + public float PriorEstimate { get; private set; } = 0f; + public float PriorErrorCovariance { get; private set; } = 1f; - public void SetState(T estimate, T errorCovariance) + public void SetState(float estimate, float errorCovariance) { PriorEstimate = estimate; PriorErrorCovariance = errorCovariance; } - public T Filter(T measurement, T processNoiseCovariance) + public float Filter(float measurement, float processNoiseCovariance) { - T estimate = PriorEstimate; - T errorCovariance = PriorErrorCovariance + processNoiseCovariance; + float estimate = PriorEstimate; + float errorCovariance = PriorErrorCovariance + processNoiseCovariance; - T gain = errorCovariance / (errorCovariance + T.One); - T newEstimate = estimate + gain * (measurement - estimate); - T newErrorCovariance = (T.One - gain) * errorCovariance; + float gain = errorCovariance / (errorCovariance + 1f); + float newEstimate = estimate + gain * (measurement - estimate); + float newErrorCovariance = (1f - gain) * errorCovariance; PriorEstimate = newEstimate; PriorErrorCovariance = newErrorCovariance; @@ -318,4 +317,4 @@ public T Filter(T measurement, T processNoiseCovariance) } } } -} +} \ No newline at end of file From 1a1c5aa8f3f1a5224f92803f315009efbcb9ddca Mon Sep 17 00:00:00 2001 From: ReubenBond Date: Fri, 12 Jan 2024 17:21:27 -0800 Subject: [PATCH 43/49] Normalize weights & fix LocalSiloPreferenceMargin --- .../ResourceOptimizedPlacementOptions.cs | 11 ----- .../ResourceOptimizedPlacementDirector.cs | 41 +++++++++++++------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index b149a236ab..117c5d0d2f 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -116,18 +116,7 @@ public void ValidateConfiguration() ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); } - if (Truncate(_options.CpuUsageWeight) + - Truncate(_options.MemoryUsageWeight) + - Truncate(_options.AvailableMemoryWeight + - Truncate(_options.PhysicalMemoryWeight)) != 1) - { - throw new OrleansConfigurationException($"The total sum across all the weights of {nameof(ResourceOptimizedPlacementOptions)} must equal 1"); - } - static void ThrowOutOfRange(string propertyName) => throw new OrleansConfigurationException($"{propertyName} must be inclusive between [0-1]"); - - static double Truncate(double value) - => Math.Floor(value * 100) / 100; } } \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index c80ab63193..f6e9ab7c93 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -20,7 +20,8 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I private const float PhysicalMemoryScalingFactor = 0.00000095367431640625f; private const int FourKiloByte = 4096; - private readonly ResourceOptimizedPlacementOptions _options; + private readonly NormalizedWeights _options; + private readonly float _localSiloPreferenceMargin; private readonly ConcurrentDictionary _siloStatistics = []; private Task _cachedLocalSilo; @@ -29,8 +30,19 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I DeploymentLoadPublisher deploymentLoadPublisher, IOptions options) { - _options = options.Value; - deploymentLoadPublisher?.SubscribeToStatisticsChangeEvents(this); + _options = NormalizeWeights(options.Value); + deploymentLoadPublisher.SubscribeToStatisticsChangeEvents(this); + } + + private static NormalizedWeights NormalizeWeights(ResourceOptimizedPlacementOptions input) + { + var totalWeight = input.CpuUsageWeight + input.MemoryUsageWeight + input.PhysicalMemoryWeight + input.AvailableMemoryWeight; + + return new ( + CpuUsageWeight: input.CpuUsageWeight / totalWeight, + MemoryUsageWeight: input.MemoryUsageWeight / totalWeight, + PhysicalMemoryWeight: input.PhysicalMemoryWeight / totalWeight, + AvailableMemoryWeight: input.AvailableMemoryWeight / totalWeight); } public Task OnAddActivation(PlacementStrategy strategy, PlacementTarget target, IPlacementContext context) @@ -157,18 +169,19 @@ private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] comp return false; } - if (_siloStatistics.TryGetValue(context.LocalSilo, out var localStats)) + if (!_siloStatistics.TryGetValue(context.LocalSilo, out var local)) { - float localScore = CalculateScore(localStats.Value); - float scoreDiff = Math.Abs(localScore - bestCandidateScore); + return false; + } - if (_options.LocalSiloPreferenceMargin >= scoreDiff) - { - return true; - } + var statistics = local.Value; + if (statistics.IsOverloaded) + { + return false; } - return false; + var localSiloScore = CalculateScore(statistics); + return localSiloScore - _localSiloPreferenceMargin <= bestCandidateScore; } /// @@ -240,8 +253,8 @@ public void Update(SiloRuntimeStatistics statistics) } } - // The rational behind using a dual-mode KF, is that we want the input signal to follow a trajectory that - // decays with a slower rate than the origianl one, but also tracks the signal in case of signal increases + // The rationale behind using a dual-mode KF, is that we want the input signal to follow a trajectory that + // decays with a slower rate than the original one, but also tracks the signal in case of signal increases // (which represent potential of overloading). Both are important, but they are inversely correlated to each other. private sealed class DualModeKalmanFilter { @@ -317,4 +330,6 @@ public float Filter(float measurement, float processNoiseCovariance) } } } + + private readonly record struct NormalizedWeights(float CpuUsageWeight, float MemoryUsageWeight, float AvailableMemoryWeight, float PhysicalMemoryWeight); } \ No newline at end of file From 407a7efa0f7598ddfd1dfb899b978ad5410ba3aa Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Sat, 13 Jan 2024 14:48:01 +0100 Subject: [PATCH 44/49] added some comments + fixed _localSiloPreferenceMargin not being set on ctor --- .../ResourceOptimizedPlacementDirector.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index f6e9ab7c93..421f9811e7 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -20,7 +20,7 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I private const float PhysicalMemoryScalingFactor = 0.00000095367431640625f; private const int FourKiloByte = 4096; - private readonly NormalizedWeights _options; + private readonly NormalizedWeights _weights; private readonly float _localSiloPreferenceMargin; private readonly ConcurrentDictionary _siloStatistics = []; @@ -30,14 +30,15 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I DeploymentLoadPublisher deploymentLoadPublisher, IOptions options) { - _options = NormalizeWeights(options.Value); + _weights = NormalizeWeights(options.Value); + _localSiloPreferenceMargin = options.Value.LocalSiloPreferenceMargin; deploymentLoadPublisher.SubscribeToStatisticsChangeEvents(this); } private static NormalizedWeights NormalizeWeights(ResourceOptimizedPlacementOptions input) { var totalWeight = input.CpuUsageWeight + input.MemoryUsageWeight + input.PhysicalMemoryWeight + input.AvailableMemoryWeight; - + return new ( CpuUsageWeight: input.CpuUsageWeight / totalWeight, MemoryUsageWeight: input.MemoryUsageWeight / totalWeight, @@ -83,7 +84,9 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa (int Index, float Score) pick; int compatibleSilosCount = compatibleSilos.Length; - // It is good practice not to allocate more than 1 kilobyte of memory on the stack + // It is good practice not to allocate more than 1[KB] on the stack + // but the size of (int, ResourceStatistics) = 64 in (64-bit architecture), by increasing + // the limit to 4[KB] we can stackalloc for up to 4096 / 64 = 64 silos in a cluster. if (compatibleSilosCount * Unsafe.SizeOf<(int, ResourceStatistics)>() <= FourKiloByte) { pick = MakePick(stackalloc (int, ResourceStatistics)[compatibleSilosCount]); @@ -204,13 +207,13 @@ private float CalculateScore(ResourceStatistics stats) float normalizedAvailableMemory = 1 - (stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f); float normalizedPhysicalMemory = PhysicalMemoryScalingFactor * physicalMemory; - return _options.CpuUsageWeight * normalizedCpuUsage + - _options.MemoryUsageWeight * normalizedMemoryUsage + - _options.AvailableMemoryWeight * normalizedAvailableMemory + - _options.PhysicalMemoryWeight * normalizedPhysicalMemory; + return _weights.CpuUsageWeight * normalizedCpuUsage + + _weights.MemoryUsageWeight * normalizedMemoryUsage + + _weights.AvailableMemoryWeight * normalizedAvailableMemory + + _weights.PhysicalMemoryWeight * normalizedPhysicalMemory; } - return _options.CpuUsageWeight * normalizedCpuUsage; + return _weights.CpuUsageWeight * normalizedCpuUsage; } public void RemoveSilo(SiloAddress address) @@ -228,6 +231,7 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta statistics); private readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); + private readonly record struct NormalizedWeights(float CpuUsageWeight, float MemoryUsageWeight, float AvailableMemoryWeight, float PhysicalMemoryWeight); private sealed class FilteredSiloStatistics(SiloRuntimeStatistics statistics) { @@ -330,6 +334,4 @@ public float Filter(float measurement, float processNoiseCovariance) } } } - - private readonly record struct NormalizedWeights(float CpuUsageWeight, float MemoryUsageWeight, float AvailableMemoryWeight, float PhysicalMemoryWeight); } \ No newline at end of file From 28e306812cf3399f6f46c52dcd66abf7ccae6345 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Sat, 13 Jan 2024 15:04:25 +0100 Subject: [PATCH 45/49] rearranged CalculateScore code a bit, and added assertion for score >= 0f && score <= 1f --- .../ResourceOptimizedPlacementDirector.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index 421f9811e7..ec80c273ef 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -151,8 +151,8 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa // See: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle static void ShufflePrefix(Span<(int SiloIndex, ResourceStatistics SiloStatistics)> values, int prefixLength) { - Debug.Assert(prefixLength >= 0); - Debug.Assert(prefixLength <= values.Length); + Debug.Assert(prefixLength >= 0 && prefixLength <= values.Length); + var max = values.Length; for (var i = 0; i < prefixLength; i++) { @@ -200,6 +200,7 @@ private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] comp private float CalculateScore(ResourceStatistics stats) { float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; + float score = _weights.CpuUsageWeight * normalizedCpuUsage; if (stats.TotalPhysicalMemory is { } physicalMemory && physicalMemory > 0) { @@ -207,13 +208,14 @@ private float CalculateScore(ResourceStatistics stats) float normalizedAvailableMemory = 1 - (stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f); float normalizedPhysicalMemory = PhysicalMemoryScalingFactor * physicalMemory; - return _weights.CpuUsageWeight * normalizedCpuUsage + - _weights.MemoryUsageWeight * normalizedMemoryUsage + - _weights.AvailableMemoryWeight * normalizedAvailableMemory + - _weights.PhysicalMemoryWeight * normalizedPhysicalMemory; + score += _weights.MemoryUsageWeight * normalizedMemoryUsage + + _weights.AvailableMemoryWeight * normalizedAvailableMemory + + _weights.PhysicalMemoryWeight * normalizedPhysicalMemory; } - return _weights.CpuUsageWeight * normalizedCpuUsage; + Debug.Assert(score >= 0f && score <= 1f); + + return score; } public void RemoveSilo(SiloAddress address) From a807d8a9e7ff442f282f16d42542f63d2eec0877 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Sat, 13 Jan 2024 21:58:55 +0100 Subject: [PATCH 46/49] fix potential for a 'DivideByZeroException' --- .../Options/ResourceOptimizedPlacementOptions.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 117c5d0d2f..9f79c613b4 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -116,6 +116,15 @@ public void ValidateConfiguration() ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); } + if (_options.CpuUsageWeight + + _options.MemoryUsageWeight + + _options.AvailableMemoryWeight + + _options.PhysicalMemoryWeight == 0f) + { + throw new OrleansConfigurationException( + $"The sum of all the weights in {nameof(ResourceOptimizedPlacementOptions)} must be a value greater than 0"); + } + static void ThrowOutOfRange(string propertyName) => throw new OrleansConfigurationException($"{propertyName} must be inclusive between [0-1]"); } From 965e01e362b1a17b8bcb029cad8a68b16d724946 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Sat, 13 Jan 2024 22:42:26 +0100 Subject: [PATCH 47/49] fixed tests + changed Options class to use int instead of float to make it easier for the users to understand + add comments to explain that weights are relative to each other + modified the director to take into account potential totalWeight = 0 + removed config exception throwing if sum = 0; as the score will be 0 but due to the jitter it will act as it were RandomPlacement --- .../ResourceOptimizedPlacementOptions.cs | 57 ++++++++----------- .../ResourceOptimizedPlacementDirector.cs | 15 ++--- .../General/PlacementOptionsTest.cs | 50 ++++------------ 3 files changed, 42 insertions(+), 80 deletions(-) diff --git a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs index 9f79c613b4..1987e04f9b 100644 --- a/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs +++ b/src/Orleans.Runtime/Configuration/Options/ResourceOptimizedPlacementOptions.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.Options; namespace Orleans.Runtime.Configuration.Options; @@ -6,6 +5,7 @@ namespace Orleans.Runtime.Configuration.Options; /// /// Settings which regulate the placement of grains across a cluster when using . /// +/// All 'weight' properties, are relative to each other. public sealed class ResourceOptimizedPlacementOptions { /// @@ -13,42 +13,42 @@ public sealed class ResourceOptimizedPlacementOptions /// /// /// A higher value results in the placement favoring silos with lower cpu usage. - /// Valid range is [0.00-1.00] + /// Valid range is [0-100] /// - public float CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; + public int CpuUsageWeight { get; set; } = DEFAULT_CPU_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_CPU_USAGE_WEIGHT = 0.4f; + public const int DEFAULT_CPU_USAGE_WEIGHT = 40; /// /// The importance of the memory usage by the silo. /// /// /// A higher value results in the placement favoring silos with lower memory usage. - /// Valid range is [0.00-1.00] + /// Valid range is [0-100] /// - public float MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; + public int MemoryUsageWeight { get; set; } = DEFAULT_MEMORY_USAGE_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_MEMORY_USAGE_WEIGHT = 0.3f; + public const int DEFAULT_MEMORY_USAGE_WEIGHT = 30; /// /// The importance of the available memory to the silo. /// /// /// A higher values results in the placement favoring silos with higher available memory. - /// Valid range is [0.00-1.00] + /// Valid range is [0-100] /// - public float AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; + public int AvailableMemoryWeight { get; set; } = DEFAULT_AVAILABLE_MEMORY_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_AVAILABLE_MEMORY_WEIGHT = 0.2f; + public const int DEFAULT_AVAILABLE_MEMORY_WEIGHT = 20; /// /// The importance of the physical memory to the silo. @@ -56,32 +56,32 @@ public sealed class ResourceOptimizedPlacementOptions /// /// A higher values results in the placement favoring silos with higher physical memory. /// This may have an impact in clusters with resources distributed unevenly across silos. - /// Valid range is [0.00-1.00] + /// Valid range is [0-100] /// - public float PhysicalMemoryWeight { get; set; } = DEFAULT_PHYSICAL_MEMORY_WEIGHT; + public int PhysicalMemoryWeight { get; set; } = DEFAULT_PHYSICAL_MEMORY_WEIGHT; /// /// The default value of . /// - public const float DEFAULT_PHYSICAL_MEMORY_WEIGHT = 0.1f; + public const int DEFAULT_PHYSICAL_MEMORY_WEIGHT = 10; /// /// The specified margin for which: if two silos (one of them being the local to the current pending activation), have a utilization score that should be considered "the same" within this margin. /// /// When this value is 0, then the policy will always favor the silo with the lower resource utilization, even if that silo is remote to the current pending activation. - /// When this value is 1, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . + /// When this value is 100, then the policy will always favor the local silo, regardless of its relative utilization score. This policy essentially becomes equivalent to . /// /// /// - /// Do favor a lower value for this e.g: 0.01-0.05 - /// Valid range is [0.00-1.00] + /// Do favor a lower value for this e.g: 5-10 + /// Valid range is [0-100] /// - public float LocalSiloPreferenceMargin { get; set; } + public int LocalSiloPreferenceMargin { get; set; } = DEFAULT_LOCAL_SILO_PREFERENCE_MARGIN; /// /// The default value of . /// - public const float DEFAULT_LOCAL_SILO_PREFERENCE_MARGIN = 0.05f; + public const int DEFAULT_LOCAL_SILO_PREFERENCE_MARGIN = 5; } internal sealed class ResourceOptimizedPlacementOptionsValidator @@ -91,41 +91,32 @@ internal sealed class ResourceOptimizedPlacementOptionsValidator public void ValidateConfiguration() { - if (_options.CpuUsageWeight < 0f || _options.CpuUsageWeight > 1f) + if (_options.CpuUsageWeight < 0 || _options.CpuUsageWeight > 100) { ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.CpuUsageWeight)); } - if (_options.MemoryUsageWeight < 0f || _options.MemoryUsageWeight > 1f) + if (_options.MemoryUsageWeight < 0 || _options.MemoryUsageWeight > 100) { ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.MemoryUsageWeight)); } - if (_options.AvailableMemoryWeight < 0f || _options.AvailableMemoryWeight > 1f) + if (_options.AvailableMemoryWeight < 0 || _options.AvailableMemoryWeight > 100) { ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.AvailableMemoryWeight)); } - if (_options.PhysicalMemoryWeight < 0f || _options.PhysicalMemoryWeight > 1f) + if (_options.PhysicalMemoryWeight < 0 || _options.PhysicalMemoryWeight > 100) { ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.PhysicalMemoryWeight)); } - if (_options.LocalSiloPreferenceMargin < 0f || _options.LocalSiloPreferenceMargin > 1f) + if (_options.LocalSiloPreferenceMargin < 0 || _options.LocalSiloPreferenceMargin > 100) { ThrowOutOfRange(nameof(ResourceOptimizedPlacementOptions.LocalSiloPreferenceMargin)); } - if (_options.CpuUsageWeight + - _options.MemoryUsageWeight + - _options.AvailableMemoryWeight + - _options.PhysicalMemoryWeight == 0f) - { - throw new OrleansConfigurationException( - $"The sum of all the weights in {nameof(ResourceOptimizedPlacementOptions)} must be a value greater than 0"); - } - static void ThrowOutOfRange(string propertyName) - => throw new OrleansConfigurationException($"{propertyName} must be inclusive between [0-1]"); + => throw new OrleansConfigurationException($"{propertyName} must be inclusive between [0-100]"); } } \ No newline at end of file diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index ec80c273ef..d51ed32d24 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -31,19 +31,20 @@ internal sealed class ResourceOptimizedPlacementDirector : IPlacementDirector, I IOptions options) { _weights = NormalizeWeights(options.Value); - _localSiloPreferenceMargin = options.Value.LocalSiloPreferenceMargin; + _localSiloPreferenceMargin = (float)options.Value.LocalSiloPreferenceMargin / 100; deploymentLoadPublisher.SubscribeToStatisticsChangeEvents(this); } private static NormalizedWeights NormalizeWeights(ResourceOptimizedPlacementOptions input) { - var totalWeight = input.CpuUsageWeight + input.MemoryUsageWeight + input.PhysicalMemoryWeight + input.AvailableMemoryWeight; + int totalWeight = input.CpuUsageWeight + input.MemoryUsageWeight + input.PhysicalMemoryWeight + input.AvailableMemoryWeight; - return new ( - CpuUsageWeight: input.CpuUsageWeight / totalWeight, - MemoryUsageWeight: input.MemoryUsageWeight / totalWeight, - PhysicalMemoryWeight: input.PhysicalMemoryWeight / totalWeight, - AvailableMemoryWeight: input.AvailableMemoryWeight / totalWeight); + return totalWeight == 0 ? new(0f, 0f, 0f, 0f) : + new ( + CpuUsageWeight: (float)input.CpuUsageWeight / totalWeight, + MemoryUsageWeight: (float)input.MemoryUsageWeight / totalWeight, + PhysicalMemoryWeight: (float)input.PhysicalMemoryWeight / totalWeight, + AvailableMemoryWeight: (float)input.AvailableMemoryWeight / totalWeight); } public Task OnAddActivation(PlacementStrategy strategy, PlacementTarget target, IPlacementContext context) diff --git a/test/TesterInternal/General/PlacementOptionsTest.cs b/test/TesterInternal/General/PlacementOptionsTest.cs index fc08ebf361..f0085d7509 100644 --- a/test/TesterInternal/General/PlacementOptionsTest.cs +++ b/test/TesterInternal/General/PlacementOptionsTest.cs @@ -11,19 +11,19 @@ public class PlacementOptionsTest : OrleansTestingBase, IClassFixture(validator.ValidateConfiguration); } - - [Fact, TestCategory("PlacementOptions"), TestCategory("Functional")] - public void SumGreaterThanOneShouldThrow() - { - var options = Options.Create(new ResourceOptimizedPlacementOptions - { - CpuUsageWeight = 0.3f, - MemoryUsageWeight = 0.4f, - AvailableMemoryWeight = 0.2f, - PhysicalMemoryWeight = 0.21f // sum > 1 - }); - - var validator = new ResourceOptimizedPlacementOptionsValidator(options); - Assert.Throws(validator.ValidateConfiguration); - } - - [Fact, TestCategory("PlacementOptions"), TestCategory("Functional")] - public void SumLessThanOneShouldThrow() - { - var options = Options.Create(new ResourceOptimizedPlacementOptions - { - CpuUsageWeight = 0.3f, - MemoryUsageWeight = 0.4f, - AvailableMemoryWeight = 0.2f, - PhysicalMemoryWeight = 0.19f // sum < 1 - }); - - var validator = new ResourceOptimizedPlacementOptionsValidator(options); - Assert.Throws(validator.ValidateConfiguration); - } } } From 145c38d5f9ad7b574bdf761a1bb0ffd8c26f5422 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Sun, 14 Jan 2024 23:38:22 +0100 Subject: [PATCH 48/49] perf improvements --- .../ResourceOptimizedPlacementDirector.cs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index d51ed32d24..ad8a77f934 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Orleans.Runtime.Configuration.Options; @@ -86,8 +87,8 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa int compatibleSilosCount = compatibleSilos.Length; // It is good practice not to allocate more than 1[KB] on the stack - // but the size of (int, ResourceStatistics) = 64 in (64-bit architecture), by increasing - // the limit to 4[KB] we can stackalloc for up to 4096 / 64 = 64 silos in a cluster. + // but the size of ValueTuple = 32 bytes, by increasing + // the limit to 4[KB] we can stackalloc for up to 4096 / 32 = 128 silos in a cluster. if (compatibleSilosCount * Unsafe.SizeOf<(int, ResourceStatistics)>() <= FourKiloByte) { pick = MakePick(stackalloc (int, ResourceStatistics)[compatibleSilosCount]); @@ -200,13 +201,15 @@ private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] comp /// physical_mem is represented in [MB] to keep the result within [0-1] in cases of silos having physical_mem less than [1GB] private float CalculateScore(ResourceStatistics stats) { - float normalizedCpuUsage = stats.CpuUsage.HasValue ? stats.CpuUsage.Value / 100f : 0f; + float normalizedCpuUsage = stats.CpuUsage / 100f; float score = _weights.CpuUsageWeight * normalizedCpuUsage; - if (stats.TotalPhysicalMemory is { } physicalMemory && physicalMemory > 0) + if (stats.TotalPhysicalMemory > 0) { - float normalizedMemoryUsage = stats.MemoryUsage.HasValue ? stats.MemoryUsage.Value / physicalMemory : 0f; - float normalizedAvailableMemory = 1 - (stats.AvailableMemory.HasValue ? stats.AvailableMemory.Value / physicalMemory : 0f); + long physicalMemory = stats.TotalPhysicalMemory; // cache locally + + float normalizedMemoryUsage = stats.MemoryUsage / physicalMemory; + float normalizedAvailableMemory = 1 - stats.AvailableMemory / physicalMemory; float normalizedPhysicalMemory = PhysicalMemoryScalingFactor * physicalMemory; score += _weights.MemoryUsageWeight * normalizedMemoryUsage + @@ -233,7 +236,14 @@ public void SiloStatisticsChangeNotification(SiloAddress address, SiloRuntimeSta }, statistics); - private readonly record struct ResourceStatistics(float? CpuUsage, float? AvailableMemory, long? MemoryUsage, long? TotalPhysicalMemory, bool IsOverloaded); + // This struct has a total of 32 bytes: 4 (float) + 4 (float) + 8 (long) + 8 (long) + 1 (bool) + 7 (padding) + // Padding is added becuase by default it gets aligned by the largest element of the struct (our 'long'), so 1 + 7 = 8. + // As this will be created very frequenty, we shave off the extra 7 bytes, bringing its size down to 25 bytes. + // It will help increase the number of ValueTuple (see inside 'MakePick') that can be stack allocated. + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private readonly record struct ResourceStatistics(float CpuUsage, float AvailableMemory, long MemoryUsage, long TotalPhysicalMemory, bool IsOverloaded); + + // No need to touch 'NormalizedWeights' as its created only once and is the same for all silos in the cluster. private readonly record struct NormalizedWeights(float CpuUsageWeight, float MemoryUsageWeight, float AvailableMemoryWeight, float PhysicalMemoryWeight); private sealed class FilteredSiloStatistics(SiloRuntimeStatistics statistics) @@ -242,10 +252,10 @@ private sealed class FilteredSiloStatistics(SiloRuntimeStatistics statistics) private readonly DualModeKalmanFilter _availableMemoryFilter = new(); private readonly DualModeKalmanFilter _memoryUsageFilter = new(); - private float? _cpuUsage = statistics.CpuUsage; - private float? _availableMemory = statistics.AvailableMemory; - private long? _memoryUsage = statistics.MemoryUsage; - private long? _totalPhysicalMemory = statistics.TotalPhysicalMemory; + private float _cpuUsage = statistics.CpuUsage ?? 0; + private float _availableMemory = statistics.AvailableMemory ?? 0; + private long _memoryUsage = statistics.MemoryUsage ?? 0; + private long _totalPhysicalMemory = statistics.TotalPhysicalMemory ?? 0; private bool _isOverloaded = statistics.IsOverloaded; public ResourceStatistics Value => new(_cpuUsage, _availableMemory, _memoryUsage, _totalPhysicalMemory, _isOverloaded); @@ -255,7 +265,7 @@ public void Update(SiloRuntimeStatistics statistics) _cpuUsage = _cpuUsageFilter.Filter(statistics.CpuUsage); _availableMemory = _availableMemoryFilter.Filter(statistics.AvailableMemory); _memoryUsage = (long)_memoryUsageFilter.Filter((float)statistics.MemoryUsage); - _totalPhysicalMemory = statistics.TotalPhysicalMemory; + _totalPhysicalMemory = statistics.TotalPhysicalMemory ?? 0; _isOverloaded = statistics.IsOverloaded; } } From e707edc6efb51da707696e8cc235320522c750c7 Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Mon, 15 Jan 2024 15:28:48 +0100 Subject: [PATCH 49/49] added 'in' modifier to CalculateScore to avoid potential defensive copying --- .../Placement/ResourceOptimizedPlacementDirector.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs index ad8a77f934..b70d1693b5 100644 --- a/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs +++ b/src/Orleans.Runtime/Placement/ResourceOptimizedPlacementDirector.cs @@ -133,7 +133,7 @@ public Task OnAddActivation(PlacementStrategy strategy, PlacementTa foreach (var (index, statistics) in candidates) { - float score = CalculateScore(statistics); + float score = CalculateScore(in statistics); // It's very unlikely, but there could be more than 1 silo that has the same score, // so we apply some jittering to avoid pick the first one in the short-list. @@ -185,7 +185,7 @@ private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] comp return false; } - var localSiloScore = CalculateScore(statistics); + var localSiloScore = CalculateScore(in statistics); return localSiloScore - _localSiloPreferenceMargin <= bestCandidateScore; } @@ -199,7 +199,7 @@ private bool IsLocalSiloPreferable(IPlacementContext context, SiloAddress[] comp /// physical_mem_weight * (1 / (1024 * 1024 * physical_mem) /// /// physical_mem is represented in [MB] to keep the result within [0-1] in cases of silos having physical_mem less than [1GB] - private float CalculateScore(ResourceStatistics stats) + private float CalculateScore(in ResourceStatistics stats) // as size of ResourceStatistics > IntPtr, we pass it by reference to avoid potential defensive copying { float normalizedCpuUsage = stats.CpuUsage / 100f; float score = _weights.CpuUsageWeight * normalizedCpuUsage;