mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Restrict scheduler affinity to cache affinity mode
This commit is contained in:
@@ -31,6 +31,7 @@ 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::orchestration::{local_attempt_slot_count, ExecutionAttemptIdentity};
|
||||
use crate::scheduler::config::{read_scheduler_ordering_config, SchedulerSchedulingMode};
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
const POOL_KEY_RETRY_INDEX_STRIDE: u32 = 100;
|
||||
@@ -182,6 +183,7 @@ struct GatewayLocalCandidateMaterializationPort<'a, F, G> {
|
||||
request_auth_channel: Option<&'a str>,
|
||||
persistence_policy: LocalCandidatePersistencePolicy<'a>,
|
||||
resolution_mode: LocalCandidateResolutionMode,
|
||||
scheduler_cache_affinity_enabled: bool,
|
||||
build_available_extra_data: F,
|
||||
decorate_skipped_candidate: G,
|
||||
}
|
||||
@@ -244,6 +246,9 @@ where
|
||||
}
|
||||
|
||||
fn remember_first_candidate_affinity(&self, candidates: &[Self::Eligible]) {
|
||||
if !self.scheduler_cache_affinity_enabled {
|
||||
return;
|
||||
}
|
||||
remember_first_local_candidate_affinity(
|
||||
self.state,
|
||||
self.auth_snapshot,
|
||||
@@ -430,6 +435,7 @@ where
|
||||
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value> + Send + Sync,
|
||||
G: Fn(SkippedLocalExecutionCandidate) -> SkippedLocalExecutionCandidate + Send + Sync,
|
||||
{
|
||||
let scheduler_cache_affinity_enabled = scheduler_cache_affinity_enabled(state).await;
|
||||
let port = GatewayLocalCandidateMaterializationPort {
|
||||
state,
|
||||
trace_id,
|
||||
@@ -442,6 +448,7 @@ where
|
||||
request_auth_channel,
|
||||
persistence_policy,
|
||||
resolution_mode,
|
||||
scheduler_cache_affinity_enabled,
|
||||
build_available_extra_data,
|
||||
decorate_skipped_candidate,
|
||||
};
|
||||
@@ -474,6 +481,7 @@ where
|
||||
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value> + Send + Sync,
|
||||
G: Fn(SkippedLocalExecutionCandidate) -> SkippedLocalExecutionCandidate + Send + Sync,
|
||||
{
|
||||
let scheduler_cache_affinity_enabled = scheduler_cache_affinity_enabled(state).await;
|
||||
let _ = build_available_extra_data;
|
||||
let (candidates, resolved_skipped) = resolve_and_rank_logical_local_execution_candidates(
|
||||
state,
|
||||
@@ -505,14 +513,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
remember_first_local_candidate_affinity(
|
||||
state,
|
||||
auth_snapshot,
|
||||
client_session_affinity,
|
||||
client_api_format,
|
||||
requested_model,
|
||||
&candidates,
|
||||
);
|
||||
if scheduler_cache_affinity_enabled {
|
||||
remember_first_local_candidate_affinity(
|
||||
state,
|
||||
auth_snapshot,
|
||||
client_session_affinity,
|
||||
client_api_format,
|
||||
requested_model,
|
||||
&candidates,
|
||||
);
|
||||
}
|
||||
|
||||
let (items, _) = build_logical_candidate_items(
|
||||
state,
|
||||
@@ -597,6 +607,7 @@ where
|
||||
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value> + Send + Sync + 'a,
|
||||
G: Fn(SkippedLocalExecutionCandidate) -> SkippedLocalExecutionCandidate + Send + Sync + 'a,
|
||||
{
|
||||
let scheduler_cache_affinity_enabled = scheduler_cache_affinity_enabled(state).await;
|
||||
let _ = build_available_extra_data;
|
||||
let decorate_skipped_candidate = Arc::new(decorate_skipped_candidate);
|
||||
let record_runtime_miss_diagnostic = persistence_policy.skipped.record_runtime_miss_diagnostic;
|
||||
@@ -630,6 +641,7 @@ where
|
||||
candidate_count: 0,
|
||||
next_candidate_index: 0,
|
||||
remembered_affinity: false,
|
||||
scheduler_cache_affinity_enabled,
|
||||
};
|
||||
cursor.load_next_page().await;
|
||||
let candidate_count = cursor.candidate_count;
|
||||
@@ -665,6 +677,7 @@ struct RequestedModelAttemptPageCursor<'a> {
|
||||
candidate_count: usize,
|
||||
next_candidate_index: u32,
|
||||
remembered_affinity: bool,
|
||||
scheduler_cache_affinity_enabled: bool,
|
||||
}
|
||||
|
||||
impl<'a> RequestedModelAttemptPageCursor<'a> {
|
||||
@@ -726,7 +739,10 @@ impl<'a> RequestedModelAttemptPageCursor<'a> {
|
||||
);
|
||||
}
|
||||
}
|
||||
if !self.remembered_affinity && !candidates.is_empty() {
|
||||
if self.scheduler_cache_affinity_enabled
|
||||
&& !self.remembered_affinity
|
||||
&& !candidates.is_empty()
|
||||
{
|
||||
remember_first_local_candidate_affinity(
|
||||
self.state,
|
||||
Some(&self.auth_snapshot),
|
||||
@@ -795,6 +811,21 @@ async fn pop_attempt_from_items(
|
||||
}
|
||||
}
|
||||
|
||||
async fn scheduler_cache_affinity_enabled(state: PlannerAppState<'_>) -> bool {
|
||||
match read_scheduler_ordering_config(state.app()).await {
|
||||
Ok(config) => config.scheduling_mode == SchedulerSchedulingMode::CacheAffinity,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
event_name = "planner_scheduler_affinity_config_load_failed",
|
||||
log_type = "event",
|
||||
error = ?error,
|
||||
"failed to load scheduler config while checking cache affinity mode"
|
||||
);
|
||||
SchedulerSchedulingMode::default() == SchedulerSchedulingMode::CacheAffinity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remember_first_local_candidate_affinity(
|
||||
state: PlannerAppState<'_>,
|
||||
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
||||
@@ -1211,6 +1242,7 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use aether_data::repository::auth::InMemoryAuthApiKeySnapshotRepository;
|
||||
use aether_data::repository::auth::StoredAuthApiKeySnapshot;
|
||||
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
@@ -1219,6 +1251,7 @@ mod tests {
|
||||
GatewayProviderTransportProvider,
|
||||
};
|
||||
use aether_scheduler_core::{
|
||||
build_scheduler_affinity_cache_key_for_api_key_id,
|
||||
SchedulerMinimalCandidateSelectionCandidate, SchedulerPriorityMode, SchedulerRankingMode,
|
||||
SchedulerRankingOutcome,
|
||||
};
|
||||
@@ -1227,6 +1260,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::data::GatewayDataState;
|
||||
use crate::orchestration::LocalExecutionCandidateMetadata;
|
||||
use crate::scheduler::affinity::SCHEDULER_AFFINITY_TTL;
|
||||
|
||||
fn sample_candidate(key_id: &str) -> SchedulerMinimalCandidateSelectionCandidate {
|
||||
SchedulerMinimalCandidateSelectionCandidate {
|
||||
@@ -1331,6 +1365,46 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_auth_snapshot() -> GatewayAuthApiKeySnapshot {
|
||||
GatewayAuthApiKeySnapshot::from_stored(
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
"user-1".to_string(),
|
||||
"alice".to_string(),
|
||||
Some("alice@example.com".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"api-key-1".to_string(),
|
||||
Some("default".to_string()),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some(60),
|
||||
Some(5),
|
||||
Some(4_102_444_800),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("stored auth snapshot should build"),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
fn no_extra_data(_: &EligibleLocalExecutionCandidate) -> Option<Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn identity_skipped_candidate(
|
||||
candidate: SkippedLocalExecutionCandidate,
|
||||
) -> SkippedLocalExecutionCandidate {
|
||||
candidate
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pool_group_keys_are_not_persisted_as_available_before_attempt() {
|
||||
let repository = Arc::new(InMemoryRequestCandidateRepository::default());
|
||||
@@ -1368,6 +1442,55 @@ mod tests {
|
||||
assert_eq!(stored[0].candidate_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialization_port_ignores_scheduler_affinity_when_cache_affinity_disabled() {
|
||||
let app = AppState::new().expect("state should build");
|
||||
let auth_snapshot = sample_auth_snapshot();
|
||||
let port = GatewayLocalCandidateMaterializationPort {
|
||||
state: PlannerAppState::new(&app),
|
||||
trace_id: "trace-affinity-disabled",
|
||||
client_api_format: "openai:chat",
|
||||
requested_model: Some("gpt-5"),
|
||||
auth_snapshot: Some(&auth_snapshot),
|
||||
client_session_affinity: None,
|
||||
required_capabilities: None,
|
||||
sticky_session_token: None,
|
||||
request_auth_channel: None,
|
||||
persistence_policy: LocalCandidatePersistencePolicy {
|
||||
available: LocalAvailableCandidatePersistenceContext {
|
||||
user_id: "user-1",
|
||||
api_key_id: "api-key-1",
|
||||
required_capabilities: None,
|
||||
error_context: "test available",
|
||||
},
|
||||
skipped: LocalSkippedCandidatePersistenceContext {
|
||||
user_id: "user-1",
|
||||
api_key_id: "api-key-1",
|
||||
required_capabilities: None,
|
||||
error_context: "test skipped",
|
||||
record_runtime_miss_diagnostic: false,
|
||||
},
|
||||
},
|
||||
resolution_mode: LocalCandidateResolutionMode::Standard,
|
||||
scheduler_cache_affinity_enabled: false,
|
||||
build_available_extra_data: no_extra_data,
|
||||
decorate_skipped_candidate: identity_skipped_candidate,
|
||||
};
|
||||
let candidate = sample_eligible("key-a", None);
|
||||
let cache_key =
|
||||
build_scheduler_affinity_cache_key_for_api_key_id("api-key-1", "openai:chat", "gpt-5")
|
||||
.expect("scheduler affinity cache key should build");
|
||||
|
||||
aether_ai_serving::AiCandidateMaterializationPort::remember_first_candidate_affinity(
|
||||
&port,
|
||||
&[candidate],
|
||||
);
|
||||
|
||||
assert!(app
|
||||
.read_scheduler_affinity_target(cache_key.as_str(), SCHEDULER_AFFINITY_TTL)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logical_materialization_does_not_persist_pool_group_representative() {
|
||||
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
|
||||
|
||||
@@ -23,8 +23,10 @@ use crate::ai_serving::{
|
||||
candidate_common_transport_skip_reason, CandidateTransportPolicyFacts, 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::read_admin_provider_pool_runtime_state;
|
||||
use crate::handlers::shared::provider_pool::{
|
||||
admin_provider_pool_cache_affinity_enabled, admin_provider_pool_config_from_config_value,
|
||||
};
|
||||
use crate::handlers::shared::provider_pool::{
|
||||
try_claim_admin_provider_pool_key, AdminProviderPoolConfig, AdminProviderPoolRuntimeState,
|
||||
};
|
||||
@@ -306,6 +308,9 @@ impl<'a> PoolKeyCursor<'a> {
|
||||
|
||||
async fn sticky_candidate(&mut self) -> Option<EligibleLocalExecutionCandidate> {
|
||||
let pool_config = pool_config_for_candidate(&self.group)?;
|
||||
if !admin_provider_pool_cache_affinity_enabled(&pool_config) {
|
||||
return None;
|
||||
}
|
||||
let runtime = read_admin_provider_pool_runtime_state(
|
||||
self.state.app().runtime_state.as_ref(),
|
||||
self.group.candidate.provider_id.as_str(),
|
||||
@@ -1206,7 +1211,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_scheduler_promotes_sticky_hit_regardless_distribution_mode() {
|
||||
fn pool_scheduler_ignores_sticky_hit_without_cache_affinity() {
|
||||
let key_a = sample_eligible_candidate(
|
||||
"provider-pool",
|
||||
"endpoint-1",
|
||||
@@ -1250,7 +1255,7 @@ mod tests {
|
||||
.iter()
|
||||
.map(|item| item.candidate.key_id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["key-a", "key-b"]
|
||||
vec!["key-b", "key-a"]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user