mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
Refine load balance candidate ranking
This commit is contained in:
@@ -9,6 +9,7 @@ use async_trait::async_trait;
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
|
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
|
||||||
|
use crate::clock::current_unix_ms;
|
||||||
use crate::handlers::shared::provider_pool::admin_provider_pool_config_from_config_value;
|
use crate::handlers::shared::provider_pool::admin_provider_pool_config_from_config_value;
|
||||||
use crate::scheduler::config::{
|
use crate::scheduler::config::{
|
||||||
read_scheduler_ordering_config, SchedulerOrderingConfig, SchedulerSchedulingMode,
|
read_scheduler_ordering_config, SchedulerOrderingConfig, SchedulerSchedulingMode,
|
||||||
@@ -176,6 +177,7 @@ fn ai_ranking_context_config(ordering_config: SchedulerOrderingConfig) -> AiRank
|
|||||||
AiRankingContextConfig {
|
AiRankingContextConfig {
|
||||||
priority_mode: ordering_config.priority_mode,
|
priority_mode: ordering_config.priority_mode,
|
||||||
scheduling_mode: ai_ranking_scheduling_mode(ordering_config.scheduling_mode),
|
scheduling_mode: ai_ranking_scheduling_mode(ordering_config.scheduling_mode),
|
||||||
|
load_balance_seed: current_unix_ms(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -606,7 +606,7 @@ async fn cache_affinity_promotes_cached_scheduler_affinity_candidate_when_enable
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn load_balance_rotates_same_priority_group_and_ignores_cached_affinity() {
|
async fn load_balance_ignores_provider_priority_and_cached_affinity() {
|
||||||
let mut first = sample_row();
|
let mut first = sample_row();
|
||||||
first.provider_id = "provider-a".to_string();
|
first.provider_id = "provider-a".to_string();
|
||||||
first.provider_name = "provider-a".to_string();
|
first.provider_name = "provider-a".to_string();
|
||||||
@@ -623,9 +623,9 @@ async fn load_balance_rotates_same_priority_group_and_ignores_cached_affinity()
|
|||||||
second.endpoint_id = "endpoint-b".to_string();
|
second.endpoint_id = "endpoint-b".to_string();
|
||||||
second.key_id = "key-b".to_string();
|
second.key_id = "key-b".to_string();
|
||||||
second.key_name = "beta".to_string();
|
second.key_name = "beta".to_string();
|
||||||
second.provider_priority = 0;
|
second.provider_priority = 100;
|
||||||
second.key_internal_priority = 0;
|
second.key_internal_priority = 0;
|
||||||
second.key_global_priority_by_format = Some(json!({"openai:chat": 0}));
|
second.key_global_priority_by_format = Some(json!({"openai:chat": 100}));
|
||||||
|
|
||||||
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||||
first, second,
|
first, second,
|
||||||
@@ -664,6 +664,29 @@ async fn load_balance_rotates_same_priority_group_and_ignores_cached_affinity()
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("first pass should succeed");
|
.expect("first pass should succeed");
|
||||||
|
let mut provider_b_first_seed = None;
|
||||||
|
for seed in 101..600 {
|
||||||
|
let pass = collect_selectable_candidates(
|
||||||
|
state.data.as_ref(),
|
||||||
|
&state,
|
||||||
|
"openai:chat",
|
||||||
|
"gpt-4.1",
|
||||||
|
false,
|
||||||
|
Some(&auth_snapshot),
|
||||||
|
seed,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("seeded pass should succeed");
|
||||||
|
if pass
|
||||||
|
.first()
|
||||||
|
.is_some_and(|candidate| candidate.provider_id == "provider-b")
|
||||||
|
{
|
||||||
|
provider_b_first_seed = Some(seed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let provider_b_first_seed = provider_b_first_seed
|
||||||
|
.expect("test seed should allow provider-b to win despite lower priority");
|
||||||
let second_pass = collect_selectable_candidates(
|
let second_pass = collect_selectable_candidates(
|
||||||
state.data.as_ref(),
|
state.data.as_ref(),
|
||||||
&state,
|
&state,
|
||||||
@@ -671,27 +694,14 @@ async fn load_balance_rotates_same_priority_group_and_ignores_cached_affinity()
|
|||||||
"gpt-4.1",
|
"gpt-4.1",
|
||||||
false,
|
false,
|
||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
101,
|
provider_b_first_seed,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("second pass should succeed");
|
.expect("second pass should succeed");
|
||||||
|
|
||||||
assert_eq!(first_pass.len(), 2);
|
assert_eq!(first_pass.len(), 2);
|
||||||
assert_eq!(second_pass.len(), 2);
|
assert_eq!(second_pass.len(), 2);
|
||||||
assert_ne!(first_pass[0].provider_id, second_pass[0].provider_id);
|
assert_eq!(second_pass[0].provider_id, "provider-b");
|
||||||
assert!(
|
|
||||||
first_pass[0].provider_id != "provider-b" || second_pass[0].provider_id != "provider-b"
|
|
||||||
);
|
|
||||||
assert_ne!(
|
|
||||||
first_pass
|
|
||||||
.iter()
|
|
||||||
.map(|candidate| candidate.provider_id.as_str())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
second_pass
|
|
||||||
.iter()
|
|
||||||
.map(|candidate| candidate.provider_id.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub enum AiRankingSchedulingMode {
|
|||||||
pub struct AiRankingContextConfig {
|
pub struct AiRankingContextConfig {
|
||||||
pub priority_mode: SchedulerPriorityMode,
|
pub priority_mode: SchedulerPriorityMode,
|
||||||
pub scheduling_mode: AiRankingSchedulingMode,
|
pub scheduling_mode: AiRankingSchedulingMode,
|
||||||
|
pub load_balance_seed: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@@ -81,15 +82,7 @@ pub fn build_ai_rankable_candidate(
|
|||||||
)
|
)
|
||||||
.unwrap_or((u8::MAX, u8::MAX));
|
.unwrap_or((u8::MAX, u8::MAX));
|
||||||
|
|
||||||
let mut rankable =
|
SchedulerRankableCandidate::from_candidate(parts.candidate, parts.original_index)
|
||||||
SchedulerRankableCandidate::from_candidate(parts.candidate, parts.original_index);
|
|
||||||
// The scheduler order is the upstream tie-breaker; AI serving only adds transport facts.
|
|
||||||
rankable.provider_id.clear();
|
|
||||||
rankable.endpoint_id.clear();
|
|
||||||
rankable.key_id.clear();
|
|
||||||
rankable.selected_provider_model_name.clear();
|
|
||||||
|
|
||||||
rankable
|
|
||||||
.with_capability_priority(requested_capability_priority_for_candidate(
|
.with_capability_priority(requested_capability_priority_for_candidate(
|
||||||
parts.required_capabilities,
|
parts.required_capabilities,
|
||||||
parts.candidate,
|
parts.candidate,
|
||||||
@@ -107,7 +100,7 @@ pub fn ai_ranking_context(config: AiRankingContextConfig) -> SchedulerRankingCon
|
|||||||
priority_mode: config.priority_mode,
|
priority_mode: config.priority_mode,
|
||||||
ranking_mode: ai_ranking_mode(config.scheduling_mode),
|
ranking_mode: ai_ranking_mode(config.scheduling_mode),
|
||||||
include_health: false,
|
include_health: false,
|
||||||
load_balance_seed: 0,
|
load_balance_seed: config.load_balance_seed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ mod priority;
|
|||||||
mod reasons;
|
mod reasons;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
use modes::{apply_load_balance_rotation, compare_rankable_candidates};
|
use modes::compare_rankable_candidates;
|
||||||
use priority::candidate_priority_slot;
|
use priority::candidate_priority_slot;
|
||||||
use reasons::{demoted_by as ranking_demoted_by, promoted_by as ranking_promoted_by};
|
use reasons::{demoted_by as ranking_demoted_by, promoted_by as ranking_promoted_by};
|
||||||
pub use reasons::{
|
pub use reasons::{
|
||||||
@@ -37,7 +37,6 @@ fn scheduler_candidate_ranking_order(
|
|||||||
order.sort_by(|left, right| {
|
order.sort_by(|left, right| {
|
||||||
compare_rankable_candidates(&candidates[*left], &candidates[*right], context)
|
compare_rankable_candidates(&candidates[*left], &candidates[*right], context)
|
||||||
});
|
});
|
||||||
apply_load_balance_rotation(&mut order, candidates, context);
|
|
||||||
order
|
order
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +138,16 @@ mod tests {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ranked_keys(
|
||||||
|
candidates: &[SchedulerRankableCandidate],
|
||||||
|
context: SchedulerRankingContext,
|
||||||
|
) -> Vec<String> {
|
||||||
|
scheduler_candidate_ranking_order(candidates, context)
|
||||||
|
.into_iter()
|
||||||
|
.map(|index| candidates[index].key_id.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn provider_priority_mode_prefers_provider_priority_slot() {
|
fn provider_priority_mode_prefers_provider_priority_slot() {
|
||||||
let candidates = vec![
|
let candidates = vec![
|
||||||
@@ -335,6 +344,40 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_order_randomizes_equal_priority_ties_without_crossing_priority_slots() {
|
||||||
|
let first = candidate("first", 0, 0, Some(0));
|
||||||
|
let second = candidate("second", 0, 0, Some(0));
|
||||||
|
let lower_priority = candidate("lower", 10, 0, Some(10));
|
||||||
|
|
||||||
|
let first_seed_order = ranked_ids(
|
||||||
|
&[first.clone(), second.clone(), lower_priority.clone()],
|
||||||
|
SchedulerRankingContext {
|
||||||
|
priority_mode: SchedulerPriorityMode::Provider,
|
||||||
|
ranking_mode: SchedulerRankingMode::FixedOrder,
|
||||||
|
include_health: false,
|
||||||
|
load_balance_seed: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let alternate_order = (1..128)
|
||||||
|
.map(|seed| {
|
||||||
|
ranked_ids(
|
||||||
|
&[first.clone(), second.clone(), lower_priority.clone()],
|
||||||
|
SchedulerRankingContext {
|
||||||
|
priority_mode: SchedulerPriorityMode::Provider,
|
||||||
|
ranking_mode: SchedulerRankingMode::FixedOrder,
|
||||||
|
include_health: false,
|
||||||
|
load_balance_seed: seed,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.find(|order| order[0] != first_seed_order[0])
|
||||||
|
.expect("equal priority tie should vary by seed");
|
||||||
|
|
||||||
|
assert_eq!(first_seed_order[2], "provider-lower");
|
||||||
|
assert_eq!(alternate_order[2], "provider-lower");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_balance_does_not_rotate_across_cross_format_demotion_group() {
|
fn load_balance_does_not_rotate_across_cross_format_demotion_group() {
|
||||||
let same_format = candidate("same", 0, 0, Some(0));
|
let same_format = candidate("same", 0, 0, Some(0));
|
||||||
@@ -356,22 +399,89 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_balance_rotates_only_within_same_priority_group() {
|
fn load_balance_provider_mode_randomizes_providers_then_uses_internal_key_priority() {
|
||||||
let first = candidate("first", 0, 0, Some(0));
|
let mut provider_a_primary = candidate("a-primary", 0, 0, Some(0));
|
||||||
let second = candidate("second", 0, 0, Some(0));
|
provider_a_primary.provider_id = "provider-a".to_string();
|
||||||
let third = candidate("third", 10, 0, Some(10));
|
provider_a_primary.key_id = "key-a-primary".to_string();
|
||||||
|
let mut provider_a_secondary = candidate("a-secondary", 0, 10, Some(10));
|
||||||
|
provider_a_secondary.provider_id = "provider-a".to_string();
|
||||||
|
provider_a_secondary.key_id = "key-a-secondary".to_string();
|
||||||
|
let mut provider_b = candidate("b", 100, 0, Some(100));
|
||||||
|
provider_b.provider_id = "provider-b".to_string();
|
||||||
|
provider_b.key_id = "key-b".to_string();
|
||||||
|
let candidates = vec![provider_a_secondary, provider_b, provider_a_primary];
|
||||||
|
|
||||||
|
let seed = (0..512)
|
||||||
|
.find(|seed| {
|
||||||
|
ranked_keys(
|
||||||
|
&candidates,
|
||||||
|
SchedulerRankingContext {
|
||||||
|
priority_mode: SchedulerPriorityMode::Provider,
|
||||||
|
ranking_mode: SchedulerRankingMode::LoadBalance,
|
||||||
|
include_health: false,
|
||||||
|
load_balance_seed: *seed,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
.is_some_and(|key| key == "key-b")
|
||||||
|
})
|
||||||
|
.expect("test seed should put provider-b first");
|
||||||
|
|
||||||
|
let order = ranked_keys(
|
||||||
|
&candidates,
|
||||||
|
SchedulerRankingContext {
|
||||||
|
priority_mode: SchedulerPriorityMode::Provider,
|
||||||
|
ranking_mode: SchedulerRankingMode::LoadBalance,
|
||||||
|
include_health: false,
|
||||||
|
load_balance_seed: seed,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(order[0], "key-b");
|
||||||
|
let primary_index = order
|
||||||
|
.iter()
|
||||||
|
.position(|key| key == "key-a-primary")
|
||||||
|
.expect("primary key should be ranked");
|
||||||
|
let secondary_index = order
|
||||||
|
.iter()
|
||||||
|
.position(|key| key == "key-a-secondary")
|
||||||
|
.expect("secondary key should be ranked");
|
||||||
|
assert!(primary_index < secondary_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_balance_global_key_mode_randomizes_keys_ignoring_global_priority() {
|
||||||
|
let high_priority = candidate("high", 100, 0, Some(0));
|
||||||
|
let low_priority = candidate("low", 0, 0, Some(100));
|
||||||
|
let candidates = vec![high_priority, low_priority];
|
||||||
|
|
||||||
|
let seed = (0..512)
|
||||||
|
.find(|seed| {
|
||||||
|
ranked_ids(
|
||||||
|
&candidates,
|
||||||
|
SchedulerRankingContext {
|
||||||
|
priority_mode: SchedulerPriorityMode::GlobalKey,
|
||||||
|
ranking_mode: SchedulerRankingMode::LoadBalance,
|
||||||
|
include_health: false,
|
||||||
|
load_balance_seed: *seed,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
.is_some_and(|provider| provider == "provider-low")
|
||||||
|
})
|
||||||
|
.expect("test seed should put lower global-priority key first");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ranked_ids(
|
ranked_ids(
|
||||||
&[first, second, third],
|
&candidates,
|
||||||
SchedulerRankingContext {
|
SchedulerRankingContext {
|
||||||
priority_mode: SchedulerPriorityMode::Provider,
|
priority_mode: SchedulerPriorityMode::GlobalKey,
|
||||||
ranking_mode: SchedulerRankingMode::LoadBalance,
|
ranking_mode: SchedulerRankingMode::LoadBalance,
|
||||||
include_health: false,
|
include_health: false,
|
||||||
load_balance_seed: 1,
|
load_balance_seed: seed,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
vec!["provider-second", "provider-first", "provider-third"]
|
vec!["provider-low", "provider-high"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use super::compare_candidate_identity_for_ranking;
|
use super::compare_candidate_identity_for_ranking;
|
||||||
use super::format::{
|
use super::format::{
|
||||||
compare_cross_format_demotion, compare_demoted_format_preference, compare_format_preference,
|
compare_cross_format_demotion, compare_demoted_format_preference, compare_format_preference,
|
||||||
};
|
};
|
||||||
use super::priority::{candidates_share_priority_group, compare_candidate_priority_slot};
|
use super::priority::compare_candidate_priority_slot;
|
||||||
use super::types::{SchedulerRankableCandidate, SchedulerRankingContext, SchedulerRankingMode};
|
use super::types::{SchedulerRankableCandidate, SchedulerRankingContext, SchedulerRankingMode};
|
||||||
|
|
||||||
pub(super) fn compare_rankable_candidates(
|
pub(super) fn compare_rankable_candidates(
|
||||||
@@ -30,6 +32,7 @@ fn compare_fixed_order(
|
|||||||
.then_with(|| compare_demoted_format_preference(left, right))
|
.then_with(|| compare_demoted_format_preference(left, right))
|
||||||
.then_with(|| compare_candidate_priority_slot(left, right, context.priority_mode))
|
.then_with(|| compare_candidate_priority_slot(left, right, context.priority_mode))
|
||||||
.then_with(|| compare_format_preference(left, right))
|
.then_with(|| compare_format_preference(left, right))
|
||||||
|
.then_with(|| compare_seeded_candidate_hash(left, right, context.load_balance_seed, "tie"))
|
||||||
.then_with(|| compare_candidate_identity_for_ranking(left, right))
|
.then_with(|| compare_candidate_identity_for_ranking(left, right))
|
||||||
.then(left.original_index.cmp(&right.original_index))
|
.then(left.original_index.cmp(&right.original_index))
|
||||||
}
|
}
|
||||||
@@ -48,7 +51,7 @@ fn compare_cache_affinity(
|
|||||||
.then(left.tunnel_bucket.cmp(&right.tunnel_bucket))
|
.then(left.tunnel_bucket.cmp(&right.tunnel_bucket))
|
||||||
.then_with(|| compare_format_preference(left, right))
|
.then_with(|| compare_format_preference(left, right))
|
||||||
.then_with(|| compare_health(left, right, context.include_health))
|
.then_with(|| compare_health(left, right, context.include_health))
|
||||||
.then(left.affinity_hash.cmp(&right.affinity_hash))
|
.then_with(|| compare_affinity_or_seeded_hash(left, right, context.load_balance_seed))
|
||||||
.then_with(|| compare_candidate_identity_for_ranking(left, right))
|
.then_with(|| compare_candidate_identity_for_ranking(left, right))
|
||||||
.then(left.original_index.cmp(&right.original_index))
|
.then(left.original_index.cmp(&right.original_index))
|
||||||
}
|
}
|
||||||
@@ -62,14 +65,37 @@ fn compare_load_balance_base(
|
|||||||
.cmp(&right.capability_priority)
|
.cmp(&right.capability_priority)
|
||||||
.then_with(|| compare_cross_format_demotion(left, right))
|
.then_with(|| compare_cross_format_demotion(left, right))
|
||||||
.then_with(|| compare_demoted_format_preference(left, right))
|
.then_with(|| compare_demoted_format_preference(left, right))
|
||||||
.then_with(|| compare_candidate_priority_slot(left, right, context.priority_mode))
|
|
||||||
.then_with(|| compare_format_preference(left, right))
|
.then_with(|| compare_format_preference(left, right))
|
||||||
.then_with(|| compare_health(left, right, context.include_health))
|
.then_with(|| compare_load_balance_distribution(left, right, context))
|
||||||
.then(left.affinity_hash.cmp(&right.affinity_hash))
|
|
||||||
.then_with(|| compare_candidate_identity_for_ranking(left, right))
|
.then_with(|| compare_candidate_identity_for_ranking(left, right))
|
||||||
.then(left.original_index.cmp(&right.original_index))
|
.then(left.original_index.cmp(&right.original_index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compare_load_balance_distribution(
|
||||||
|
left: &SchedulerRankableCandidate,
|
||||||
|
right: &SchedulerRankableCandidate,
|
||||||
|
context: SchedulerRankingContext,
|
||||||
|
) -> Ordering {
|
||||||
|
match context.priority_mode {
|
||||||
|
crate::SchedulerPriorityMode::Provider => {
|
||||||
|
compare_seeded_provider_hash(left, right, context.load_balance_seed)
|
||||||
|
.then_with(|| {
|
||||||
|
if left.provider_id == right.provider_id {
|
||||||
|
left.key_internal_priority.cmp(&right.key_internal_priority)
|
||||||
|
} else {
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then_with(|| {
|
||||||
|
compare_seeded_candidate_hash(left, right, context.load_balance_seed, "key")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
crate::SchedulerPriorityMode::GlobalKey => {
|
||||||
|
compare_seeded_candidate_hash(left, right, context.load_balance_seed, "global-key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn compare_health(
|
fn compare_health(
|
||||||
left: &SchedulerRankableCandidate,
|
left: &SchedulerRankableCandidate,
|
||||||
right: &SchedulerRankableCandidate,
|
right: &SchedulerRankableCandidate,
|
||||||
@@ -84,44 +110,78 @@ fn compare_health(
|
|||||||
.then_with(|| right.health_score.total_cmp(&left.health_score))
|
.then_with(|| right.health_score.total_cmp(&left.health_score))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn apply_load_balance_rotation(
|
fn compare_affinity_or_seeded_hash(
|
||||||
sorted_indices: &mut [usize],
|
|
||||||
candidates: &[SchedulerRankableCandidate],
|
|
||||||
context: SchedulerRankingContext,
|
|
||||||
) {
|
|
||||||
if context.ranking_mode != SchedulerRankingMode::LoadBalance || sorted_indices.len() < 2 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut start = 0usize;
|
|
||||||
while start < sorted_indices.len() {
|
|
||||||
let mut end = start + 1;
|
|
||||||
while end < sorted_indices.len()
|
|
||||||
&& candidates_share_load_balance_rotation_group(
|
|
||||||
&candidates[sorted_indices[start]],
|
|
||||||
&candidates[sorted_indices[end]],
|
|
||||||
context.priority_mode,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
end += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let group_len = end - start;
|
|
||||||
if group_len > 1 {
|
|
||||||
let offset = usize::try_from(context.load_balance_seed).unwrap_or(0) % group_len;
|
|
||||||
sorted_indices[start..end].rotate_left(offset);
|
|
||||||
}
|
|
||||||
start = end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn candidates_share_load_balance_rotation_group(
|
|
||||||
left: &SchedulerRankableCandidate,
|
left: &SchedulerRankableCandidate,
|
||||||
right: &SchedulerRankableCandidate,
|
right: &SchedulerRankableCandidate,
|
||||||
priority_mode: crate::SchedulerPriorityMode,
|
seed: u64,
|
||||||
) -> bool {
|
) -> Ordering {
|
||||||
candidates_share_priority_group(left, right, priority_mode)
|
match (left.affinity_hash, right.affinity_hash) {
|
||||||
&& left.capability_priority == right.capability_priority
|
(Some(left_hash), Some(right_hash)) => left_hash.cmp(&right_hash),
|
||||||
&& left.demote_cross_format == right.demote_cross_format
|
_ => compare_seeded_candidate_hash(left, right, seed, "tie"),
|
||||||
&& left.format_preference == right.format_preference
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_seeded_provider_hash(
|
||||||
|
left: &SchedulerRankableCandidate,
|
||||||
|
right: &SchedulerRankableCandidate,
|
||||||
|
seed: u64,
|
||||||
|
) -> Ordering {
|
||||||
|
seeded_rank_hash(seed, "provider", [left.provider_id.as_str()], 0).cmp(&seeded_rank_hash(
|
||||||
|
seed,
|
||||||
|
"provider",
|
||||||
|
[right.provider_id.as_str()],
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_seeded_candidate_hash(
|
||||||
|
left: &SchedulerRankableCandidate,
|
||||||
|
right: &SchedulerRankableCandidate,
|
||||||
|
seed: u64,
|
||||||
|
salt: &str,
|
||||||
|
) -> Ordering {
|
||||||
|
seeded_rank_hash(
|
||||||
|
seed,
|
||||||
|
salt,
|
||||||
|
[
|
||||||
|
left.provider_id.as_str(),
|
||||||
|
left.endpoint_id.as_str(),
|
||||||
|
left.key_id.as_str(),
|
||||||
|
left.selected_provider_model_name.as_str(),
|
||||||
|
],
|
||||||
|
left.original_index,
|
||||||
|
)
|
||||||
|
.cmp(&seeded_rank_hash(
|
||||||
|
seed,
|
||||||
|
salt,
|
||||||
|
[
|
||||||
|
right.provider_id.as_str(),
|
||||||
|
right.endpoint_id.as_str(),
|
||||||
|
right.key_id.as_str(),
|
||||||
|
right.selected_provider_model_name.as_str(),
|
||||||
|
],
|
||||||
|
right.original_index,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seeded_rank_hash<'a>(
|
||||||
|
seed: u64,
|
||||||
|
salt: &str,
|
||||||
|
parts: impl IntoIterator<Item = &'a str>,
|
||||||
|
original_index: usize,
|
||||||
|
) -> u64 {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(seed.to_be_bytes());
|
||||||
|
hasher.update(b":");
|
||||||
|
hasher.update(salt.as_bytes());
|
||||||
|
for part in parts {
|
||||||
|
hasher.update(b":");
|
||||||
|
hasher.update(part.as_bytes());
|
||||||
|
}
|
||||||
|
hasher.update(b":");
|
||||||
|
hasher.update(original_index.to_be_bytes());
|
||||||
|
let digest = hasher.finalize();
|
||||||
|
u64::from_be_bytes([
|
||||||
|
digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7],
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,19 +34,3 @@ pub(super) fn compare_candidate_priority_slot(
|
|||||||
.then(left.key_internal_priority.cmp(&right.key_internal_priority)),
|
.then(left.key_internal_priority.cmp(&right.key_internal_priority)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn candidates_share_priority_group(
|
|
||||||
left: &SchedulerRankableCandidate,
|
|
||||||
right: &SchedulerRankableCandidate,
|
|
||||||
priority_mode: SchedulerPriorityMode,
|
|
||||||
) -> bool {
|
|
||||||
match priority_mode {
|
|
||||||
SchedulerPriorityMode::Provider => {
|
|
||||||
left.provider_priority == right.provider_priority
|
|
||||||
&& left.key_internal_priority == right.key_internal_priority
|
|
||||||
}
|
|
||||||
SchedulerPriorityMode::GlobalKey => {
|
|
||||||
left.key_global_priority_for_format == right.key_global_priority_for_format
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user