From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 2467E1FF133 for ; Mon, 27 Apr 2026 15:20:42 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0C0C01E40D; Mon, 27 Apr 2026 15:20:41 +0200 (CEST) From: Dominik Rusovac To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox 1/7] resource-scheduling: clamp imbalance value to unit interval Date: Mon, 27 Apr 2026 15:20:25 +0200 Message-ID: <20260427132031.220468-2-d.rusovac@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260427132031.220468-1-d.rusovac@proxmox.com> References: <20260427132031.220468-1-d.rusovac@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1777295942564 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.458 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: EUGKVZ66BTZGU4MKIJ7OANWRMVXWB4JE X-Message-ID-Hash: EUGKVZ66BTZGU4MKIJ7OANWRMVXWB4JE X-MailFrom: d.rusovac@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The currently used load imbalance value is given as the so-called coefficient of variation (CV), a value that may exceed 1. As such, the CV value alone lacks meaning. A CV value of 0.0 means no imbalance, but what does a value of, say, 1.7 mean? Relative to the number of nodes in a cluster, it is possible to determine the upper bound of the CV value [0][1]. By dividing the CV value by its upper bound, the load imbalance can be represented as a value that varies between 0 and 1. Expressing the CV as a percentage makes the concept of load imbalance easier to interpret. [0] https://repositorio.ipbeja.pt/server/api/core/bitstreams/8ed9a444-dbe0-402f-9d2f-90c5bf6e418c/content [1] https://stats.stackexchange.com/questions/18621/maximum-value-of-coefficient-of-variation-for-bounded-data-set Signed-off-by: Dominik Rusovac --- proxmox-resource-scheduling/src/scheduler.rs | 33 +++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/proxmox-resource-scheduling/src/scheduler.rs b/proxmox-resource-scheduling/src/scheduler.rs index 49d16f9f..4eacbff9 100644 --- a/proxmox-resource-scheduling/src/scheduler.rs +++ b/proxmox-resource-scheduling/src/scheduler.rs @@ -17,17 +17,23 @@ pub struct NodeUsage { pub stats: NodeStats, } -/// Returns the load imbalance among the nodes. +/// Returns the load imbalance among the nodes, which is a value between 0 and 1 that describes the +/// statistical dispersion of the individual node loads around the mean node load. The lower the +/// value, the better. /// -/// The load balance is measured as the statistical dispersion of the individual node loads. -/// -/// The current implementation uses the dimensionless coefficient of variation, which expresses the -/// standard deviation in relation to the average mean of the node loads. -/// -/// The coefficient of variation is not robust, which is a desired property here, because outliers -/// should be detected as much as possible. +/// In more detail, the current implementation computes the so-called coefficient of variation (CV), +/// which is the ratio of the standard deviation to the mean of the given node loads. The lower +/// bound of the CV is reached if all node loads are equal. The upper bound is reached if all nodes +/// except one are idle. To present the CV as a value between 0 and 1, it's being divided by the +/// upper bound of the CV for the given number of nodes. fn calculate_node_imbalance(nodes: &[NodeUsage], to_load: impl Fn(&NodeUsage) -> f64) -> f64 { - let node_count = nodes.len(); + let node_count = nodes.len() as f64; + + // imbalance is perfect for less than 2 nodes + if node_count < 2.0 { + return 0.0; + } + let node_loads = nodes.iter().map(to_load).collect::>(); let load_sum = node_loads.iter().sum::(); @@ -36,14 +42,17 @@ fn calculate_node_imbalance(nodes: &[NodeUsage], to_load: impl Fn(&NodeUsage) -> if load_sum == 0.0 { 0.0 } else { - let load_mean = load_sum / node_count as f64; + let load_mean = load_sum / node_count; let squared_diff_sum = node_loads .iter() .fold(0.0, |sum, node_load| sum + (node_load - load_mean).powi(2)); - let load_sd = (squared_diff_sum / node_count as f64).sqrt(); + let load_sd = (squared_diff_sum / node_count).sqrt(); + + let max_cv = (node_count - 1.0).sqrt(); + let cv = load_sd / load_mean; - load_sd / load_mean + cv / max_cv } } -- 2.47.3