From: "Dominik Rusovac" <d.rusovac@proxmox.com>
To: "Daniel Kral" <d.kral@proxmox.com>, <pve-devel@lists.proxmox.com>
Subject: Re: [PATCH proxmox 03/15] fix #7993: resource-scheduling: lift to weighted load balancing
Date: Fri, 23 Oct 2026 12:00:11 +0200 [thread overview]
Message-ID: <DMC4P5RBFXN5.O2N9237DAOM4@proxmox.com> (raw)
In-Reply-To: <DMB94I0AIRBP.3F5YB1ZG94JZ1@proxmox.com>
thx a lot for your valuable feedback!
will send a v2
On Thu Oct 22, 2026 at 11:15 AM CEST, Daniel Kral wrote:
> Thanks a lot for working on this, nice work! Especially the cover letter
> is a very interesting analysis and read!
>
> Looks generally sound, though there are some comments inline on having
> backwards and forwards compatibility and some patch cleanup.
thx! see my comments inline
>
> On Wed Oct 21, 2026 at 10:59 AM CEST, Dominik Rusovac wrote:
>> Blend weights into node load computation, effectively replacing
[snip]
>>
>> Refactor existing tests.
>>
>> [0] https://en.wikipedia.org/wiki/Arithmetic_mean
>> [1] https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
>> [2] https://lore.proxmox.com/all/20221110143800.98047-3-f.ebner@proxmox.com/
>
> Changes which were applied should be referenced as commit refs rather
> than the patch on the mailing list, i.e. here
>
> commit 2b325aa5 ("add pve_static module")
>
ack, thx!
>>
>> Signed-off-by: Dominik Rusovac <d.rusovac@proxmox.com>
>> ---
>> proxmox-resource-scheduling/src/node.rs | 27 +-
>> proxmox-resource-scheduling/src/pve_static.rs | 3 +-
>> proxmox-resource-scheduling/src/scheduler.rs | 248 +++++++++++++-----
>> .../tests/scheduler.rs | 136 ++++++++--
>> 4 files changed, 327 insertions(+), 87 deletions(-)
>>
>> diff --git a/proxmox-resource-scheduling/src/node.rs b/proxmox-resource-scheduling/src/node.rs
>> index 3c7ed393..f9dc5b16 100644
>> --- a/proxmox-resource-scheduling/src/node.rs
>> +++ b/proxmox-resource-scheduling/src/node.rs
>> @@ -51,9 +51,30 @@ impl NodeStats {
>> self.mem as f64 / self.maxmem as f64
>> }
>>
>> - /// Returns a combined node usage as a percentage.
>> - pub fn load(&self) -> f64 {
>> - (self.cpu_load() + self.mem_load()) / 2.0
>> + /// Returns the node load as the [weighted arithmetic mean] of [`Self::cpu_load`] and
>> + /// [`Self::mem_load`].
>> + ///
>> + /// `cpu_weight` controls how much [`Self::cpu_load`] contributes to the load, analogously,
>> + /// `mem_weight` controls how much [`Self::mem_load`] contributes to the load; the higher the
>> + /// weight, the more that resource dominates the result.
>
> nit: can be documented in struct NodeStatsWeights, see below for more
>
>> + ///
>> + /// NOTE:
>> + /// - the weights have to be positive
>> + /// - only the weights' ratio matters, not their absolute magnitude
>> + /// - a weight of 0.0 means no contribution to the load
>> + /// - [weighted arithmetic mean] is the same as [arithmetic mean] if the weights are equal
>> + ///
>> + /// [weighted arithmetic mean]: https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
>> + /// [arithmetic mean]: https://en.wikipedia.org/wiki/Arithmetic_mean
>> + pub fn load(&self, cpu_weight: f64, mem_weight: f64) -> f64 {
>> + let weights_sum = cpu_weight + mem_weight;
>> +
>> + if weights_sum == 0.0 {
>> + // both, cpu and mem, should not contribute to the load, so there is no load
>> + return 0.0;
>> + }
>> +
>> + (self.cpu_load() * cpu_weight + self.mem_load() * mem_weight) / weights_sum
>> }
>
> Would be nice to have a separate weighted_load() next to the existing
> load() for backwards compatibility.
>
> And for some type of forwards compatibility, maybe something like
>
> #[derive(Copy, Clone, Debug)]
> pub struct NodeStatsWeights {
> pub cpu_weight: f64,
> pub mem_weight: f64,
> }
>
> impl Default for NodeStatsWeights {
> fn default() -> Self {
> Self {
> cpu_weight: 1,
> mem_weight: 1,
> }
> }
> }
>
> pub fn weighted_load(&self, weights: &NodeStatsWeights) -> f64 {}
>
> ?
I will likely take the 'weights as part of Scheduler' route
>
> The NodeStatsWeights struct can then be used in all callers of
> weighted_load() and therefore the members and their documentation do not
> need to be repeated there.
>
> Though we'd probably need a default for TOPSIS as well, because there it
> should be 1 : 5 with cpu : mem for backwards compatibility...
>
true, atm I am not entirely sure how to approach the different defaults
requirement
will see what I can do and hopefully find a convenient solution in v2
[snip]
>>
>> -criteria_struct! {
>> - /// A given alternative.
>> - struct PveTopsisAlternative {
>> - #[criterion("average CPU", -1.0)]
>> - average_cpu: f64,
>> - #[criterion("highest CPU", -2.0)]
>> - highest_cpu: f64,
>> - #[criterion("average memory", -5.0)]
>> - average_memory: f64,
>> - #[criterion("highest memory", -10.0)]
>> - highest_memory: f64,
>> - }
>> -
>> - const N_CRITERIA;
>> - static PVE_HA_TOPSIS_CRITERIA;
>> -}
>> -
>
> would prefer keeping the struct, so there's still a type giving the
> numbers some name.
>
> struct PveTopsisAlternative {
> average_cpu: f64,
> highest_cpu: f64,
> average_memory: f64,
> highest_memory: f64,
> }
>
> const N_CRITERIA = 4;
>
> impl From<PveTopsisAlternative> for [f64; N_CRITERIA] {
> fn from(alternative: PveTopsisAlternative) -> Self {
> [
> alternative.average_cpu,
> alternative.highest_cpu,
> alternative.average_memory,
> alternative.highest_memory,
> ]
> }
> }
>
ack
>> #[derive(Clone, Debug)]
>> pub struct Scheduler {
>> nodes: Vec<NodeUsage>,
>> @@ -111,12 +97,20 @@ pub struct ScoredMigration {
>> pub migration: Migration,
>> /// The expected node imbalance after the migration.
>> pub imbalance: f64,
>> + /// The score.
>> + ///
>> + /// - corresponds to imbalance of weighted loads for
>> + /// [`Scheduler::score_best_balancing_migration_candidates`]
>> + /// - corresponds to distance to ideal best alternatives for
>> + /// [`Scheduler::score_best_balancing_migration_candidates_topsis`]
>> + pub score: f64,
>
> As discussed off-list, I'm not sure whether we should expose the 'score'
> as it isn't relevant to the feedback loop now.
>
you're right, will be removed in v2
>> }
>>
>> impl Ord for ScoredMigration {
>> fn cmp(&self, other: &Self) -> Ordering {
>> - self.imbalance
>> - .total_cmp(&other.imbalance)
>> + self.score
>> + .total_cmp(&other.score)
>> + .then(self.imbalance.total_cmp(&other.imbalance))
>> .then(self.migration.cmp(&other.migration))
>
> AFAICT this could lead to unwanted load balancing decisions at least for
> the brute force case:
>
> If a user configures cpu_weight = 0, mem_weight = 1, i.e., do not
> include CPU usage in load balancing decisions, then if two migration
> candidates would have the equal change in memory distribution but
> different ones in CPU (i.e. equal imbalance values), it would then still
> include CPU usage in the load balancing decision with the unweighted
> 'imbalance' value.
>
> Of course, this wouldn't even be really noticable, so might not be an
> issue.
valid point, thx for the heads up!
afaict the described issue would rarely show up, but it's
nevertheless real and the behaviour of the ranking is wrong in
principle
v2 will remove `score`, so this should no longer cause issues in v2
>
> Though I wonder whether calculating both the unweighted and weighted
> node imbalance values for all migration candidates at least in the
> bruteforce case does increase the computational load for a relatively
> small special case (i.e. equal weighted imbalance values).
>
valid point too, thx!
as mentioned earlier, v2 will remove `score`, so this should no longer
cause issues in v2
[snip]
>> - /// Scores the given migration `candidates` by the best node imbalance improvement with
>> - /// exhaustive search.
>> + /// Uses brute-force search to score how much either of the given migration `candidates`
>> + /// improves node imbalance and ranks them accordingly.
>
> Good catch!
>
> As noted off-list, the "score" name is unfortunately a bit of a
> misnomer, because this doesn't actually score them (the imbalance part
> is only information for the feedback loop and log output outside), but
> ranks them, therefore making the limit parameter reasonable.
>
> However, as this is pre-existing and isn't relevant to this specific
> patch, this should be done in a separate patch to not clutter this one,
> which already changes quite a lot on its own.
>
> This makes understanding past changes much easier in the future and
> reviewing them too.
ack, will isolate all the changes refining docs in a separate patch
[snip]
>> pub fn score_best_balancing_migration_candidates_topsis(
>
> same for this method as above
>
>> &self,
>> candidates: &[MigrationCandidate],
>> limit: usize,
>> + cpu_weight: usize,
>> + mem_weight: usize,
>> ) -> Result<Vec<ScoredMigration>, Error> {
>> + let (w1, w2) = (cpu_weight as f64, mem_weight as f64);
>> +
>> + let average_cpu = topsis::Criterion::new("average_cpu".to_owned(), -w1);
>> + let highest_cpu = topsis::Criterion::new(
>> + "highest_cpu".to_owned(),
>> + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w1),
>> + );
>> + let average_mem = topsis::Criterion::new("average_mem".to_owned(), -w2);
>> + let highest_mem = topsis::Criterion::new(
>> + "highest_mem".to_owned(),
>> + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w2),
>> + );
>> +
>> let matrix = candidates
>> .iter()
>> .map(|candidate| {
>> self.topsis_alternative_with(|node| node_stats_after_migration(candidate, node))
>> - .into()
>> })
>> .collect::<Vec<_>>();
>>
>> - let best_alternatives =
>> - topsis::rank_alternatives(&topsis::Matrix::new(matrix)?, &PVE_HA_TOPSIS_CRITERIA)?;
>> + let mut scored_migrations = topsis::score_alternatives(
>> + &topsis::Matrix::new(matrix)?,
>> + &topsis::Criteria::new([average_cpu, highest_cpu, average_mem, highest_mem])?,
>> + )?
>> + .iter()
>> + .zip(candidates.iter())
>> + .map(|(topsis_score, candidate)| {
>> + let imbalance = self.node_imbalance_with_migration_candidate(candidate, w1, w2);
>
> So the meaning of the 'imbalance' field in ScoredMigration would also
> differ in meaning between brute force and TOPSIS?
in v1 yes, in v2 `imbalance` will simply be the weighted load imbalance with
the specified weights for both methods
>
>> +
>> + Reverse(ScoredMigration::new(
>> + candidate.clone(),
>> + imbalance,
>> + // invert score for consistent sorting
>> + 1.0 - topsis_score,
>> + ))
>> + })
>> + .collect::<BinaryHeap<_>>();
>>
>> - Ok(best_alternatives
>> - .into_iter()
>> - .take(limit)
>> - .map(|i| {
>> - let imbalance = self.node_imbalance_with_migration_candidate(&candidates[i]);
>> + let mut best_migrations = Vec::with_capacity(limit);
>>
>> - ScoredMigration::new(candidates[i].clone(), imbalance)
>> - })
>> - .collect())
>> + // BinaryHeap::into_iter_sorted() is still in nightly unfortunately
>> + while best_migrations.len() < limit {
>> + match scored_migrations.pop() {
>> + Some(Reverse(alternative)) => best_migrations.push(alternative),
>> + None => break,
>> + }
>> + }
>> +
>> + Ok(best_migrations)
>> }
>
> Hm, I think we should leave
> score_best_balancing_migration_candidates_topsis() to only rank by
> TOPSIS and not do the same procedure as is done with brute force.
>
ack
[snip]
>> pub fn score_nodes_to_start_resource<T: Into<ResourceStats>>(
>> &self,
>> resource_stats: T,
>> + cpu_weight: usize,
>> + mem_weight: usize,
>> ) -> Result<Vec<(String, f64)>, Error> {
>
> Unrelated to this patch itself, but the parameters are exposed as
> 'ha-auto-rebalance-{cpu,mem}-weight', but here they are also used for
> other kinds of load balancing, i.e., initial placement balancing.
>
technically these weights can be set in multiple places across this
series, but the series does not make them configurable in general [0]
the parameters were added in these places to make potential future
changes easier, in case the weights for initial placement balancing were
to become configurable at some point in the future
[0] https://lore.proxmox.com/all/DMBA12N76K0Z.31DGWX02RMAYD@proxmox.com/#:~:text=152%2C13%20%40%40%20sub%20score%5Fbest%5Fbalancing%5Fmigrations%5Ftopsis%20%7B%20%3E%20sub%20score%5Fnodes%5Fto%5Fstart%5Fservice%20%7B%20%3E%20my%20%28%24self%2C%20%24sid%29%20%3D%20%40%5F%3B%20%3E-,%3E%20%2B%20my%20%28%24cpu%5Fweight%2C%20%24mem%5Fweight%29%20%3D%20%281%2C%205%29%3B
> So maybe we should change the name of those?
this is a more general question, which (as you mentioned) is affecting
other patches rather than this one. nevertheless...
I was torn on this and decided to not make the weights for initial
placement balancing configurable atm, but making them configurable right
away is definitely possible
[snip]
next prev parent reply other threads:[~2026-10-23 10:00 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-21 8:59 [RFC PATCH-SERIES cluster/ha-manager/manager/proxmox{,-perl-rs} 00/15] fix #7993: implement weighted auto rebalancing Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox 01/15] resource-scheduling: scheduler: fix typos Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox 02/15] resource-scheduling: scheduler: abstract post-migration stats logic Dominik Rusovac
2026-10-21 12:49 ` Daniel Kral
2026-10-21 8:59 ` [PATCH proxmox 03/15] fix #7993: resource-scheduling: lift to weighted load balancing Dominik Rusovac
2026-10-22 9:15 ` Daniel Kral
2026-10-22 11:34 ` Daniel Kral
2026-10-23 10:00 ` Dominik Rusovac [this message]
2026-10-21 8:59 ` [PATCH proxmox 04/15] workspace,resource-scheduling: add proptest crate Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox 05/15] resource-scheduling: scheduler: proptest weights Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox-perl-rs 06/15] fix #7993: pve-rs: resource-scheduling: add weights to pve bindings Dominik Rusovac
2026-10-22 9:26 ` Daniel Kral
2026-10-21 8:59 ` [PATCH pve-manager 07/15] fix #7993: ui: form: crs options: add weights Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-manager 08/15] ui: form: crs options: make options advanced Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-ha-manager 09/15] manager: remove obsolete argument Dominik Rusovac
2026-10-22 9:29 ` Daniel Kral
2026-10-21 8:59 ` [PATCH pve-ha-manager 10/15] fix #7993: manager,usage: implement weighted auto rebalancing Dominik Rusovac
2026-10-22 9:58 ` Daniel Kral
2026-10-21 8:59 ` [PATCH pve-ha-manager 11/15] sim: hardware: enable datacenter config changes at runtime Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-ha-manager 12/15] manager: log changes to crs settings Dominik Rusovac
2026-10-22 12:05 ` Daniel Kral
2026-10-21 8:59 ` [PATCH pve-ha-manager 13/15] test: add auto rebalance with weights test cases Dominik Rusovac
2026-10-22 12:20 ` Daniel Kral
2026-10-21 8:59 ` [PATCH pve-ha-manager 14/15] manager: rename update_crs_scheduler_mode Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-cluster 15/15] fix #7993: datacenter config: add ha-auto-rebalance-{cpu,mem}-weight Dominik Rusovac
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=DMC4P5RBFXN5.O2N9237DAOM4@proxmox.com \
--to=d.rusovac@proxmox.com \
--cc=d.kral@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox