[router] bucket policy (#11719)

This commit is contained in:
syy-hw
2025-11-10 02:02:53 -08:00
committed by GitHub
parent 9ea2c686c7
commit 611a4fd08b
12 changed files with 1435 additions and 9 deletions
@@ -19,6 +19,7 @@ def policy_from_str(policy_str: Optional[str]) -> PolicyType:
"round_robin": PolicyType.RoundRobin, "round_robin": PolicyType.RoundRobin,
"cache_aware": PolicyType.CacheAware, "cache_aware": PolicyType.CacheAware,
"power_of_two": PolicyType.PowerOfTwo, "power_of_two": PolicyType.PowerOfTwo,
"bucket": PolicyType.Bucket,
} }
return policy_map[policy_str] return policy_map[policy_str]
@@ -34,6 +34,7 @@ class RouterArgs:
eviction_interval_secs: int = 120 eviction_interval_secs: int = 120
max_tree_size: int = 2**26 max_tree_size: int = 2**26
max_payload_size: int = 512 * 1024 * 1024 # 512MB default for large batches max_payload_size: int = 512 * 1024 * 1024 # 512MB default for large batches
bucket_adjust_interval_secs: int = 5
dp_aware: bool = False dp_aware: bool = False
enable_igw: bool = False # Enable IGW (Inter-Gateway) mode for multi-model support enable_igw: bool = False # Enable IGW (Inter-Gateway) mode for multi-model support
api_key: Optional[str] = None api_key: Optional[str] = None
@@ -167,7 +168,7 @@ class RouterArgs:
f"--{prefix}prefill-policy", f"--{prefix}prefill-policy",
type=str, type=str,
default=None, default=None,
choices=["random", "round_robin", "cache_aware", "power_of_two"], choices=["random", "round_robin", "cache_aware", "power_of_two", "bucket"],
help="Specific policy for prefill nodes in PD mode. If not specified, uses the main policy", help="Specific policy for prefill nodes in PD mode. If not specified, uses the main policy",
) )
parser.add_argument( parser.add_argument(
@@ -234,6 +235,12 @@ class RouterArgs:
default=RouterArgs.balance_rel_threshold, default=RouterArgs.balance_rel_threshold,
help="Load balancing is triggered when (max_load - min_load) > abs_threshold AND max_load > min_load * rel_threshold. Otherwise, use cache aware", help="Load balancing is triggered when (max_load - min_load) > abs_threshold AND max_load > min_load * rel_threshold. Otherwise, use cache aware",
) )
parser.add_argument(
f"--{prefix}bucket-adjust-interval-secs",
type=int,
default=RouterArgs.bucket_adjust_interval_secs,
help="Interval in seconds between bucket boundary adjustment operations",
)
parser.add_argument( parser.add_argument(
f"--{prefix}eviction-interval-secs", f"--{prefix}eviction-interval-secs",
type=int, type=int,
+33
View File
@@ -263,6 +263,16 @@ pub enum PolicyConfig {
#[serde(rename = "power_of_two")] #[serde(rename = "power_of_two")]
PowerOfTwo { load_check_interval_secs: u64 }, PowerOfTwo { load_check_interval_secs: u64 },
#[serde(rename = "bucket")]
Bucket {
/// Absolute load difference threshold for load balancing
balance_abs_threshold: usize,
/// Relative load ratio threshold for load balancing
balance_rel_threshold: f32,
/// Interval between bucket boundary adjustment cycles (seconds)
bucket_adjust_interval_secs: usize,
},
} }
impl PolicyConfig { impl PolicyConfig {
@@ -272,6 +282,7 @@ impl PolicyConfig {
PolicyConfig::RoundRobin => "round_robin", PolicyConfig::RoundRobin => "round_robin",
PolicyConfig::CacheAware { .. } => "cache_aware", PolicyConfig::CacheAware { .. } => "cache_aware",
PolicyConfig::PowerOfTwo { .. } => "power_of_two", PolicyConfig::PowerOfTwo { .. } => "power_of_two",
PolicyConfig::Bucket { .. } => "bucket",
} }
} }
} }
@@ -728,6 +739,28 @@ mod tests {
} }
} }
#[test]
fn test_bucket_parameters() {
let bucket = PolicyConfig::Bucket {
balance_abs_threshold: 20,
balance_rel_threshold: 2.0,
bucket_adjust_interval_secs: 5,
};
match bucket {
PolicyConfig::Bucket {
balance_abs_threshold,
balance_rel_threshold,
bucket_adjust_interval_secs,
} => {
assert_eq!(balance_abs_threshold, 20);
assert!((balance_rel_threshold - 2.0).abs() < 0.0001);
assert_eq!(bucket_adjust_interval_secs, 5);
}
_ => panic!("Expected Bucket"),
}
}
#[test] #[test]
fn test_discovery_config_default() { fn test_discovery_config_default() {
let config = DiscoveryConfig::default(); let config = DiscoveryConfig::default();
+96
View File
@@ -209,6 +209,34 @@ impl ConfigValidator {
}); });
} }
} }
PolicyConfig::Bucket {
balance_abs_threshold: _,
balance_rel_threshold,
bucket_adjust_interval_secs,
} => {
if *balance_rel_threshold < 1.0 {
return Err(ConfigError::InvalidValue {
field: "balance_rel_threshold".to_string(),
value: balance_rel_threshold.to_string(),
reason: "Must be >= 1.0".to_string(),
});
}
if *bucket_adjust_interval_secs < 1 {
return Err(ConfigError::InvalidValue {
field: "bucket_adjust_interval_secs".to_string(),
value: bucket_adjust_interval_secs.to_string(),
reason: "Must be >= 1s".to_string(),
});
}
if *bucket_adjust_interval_secs >= 4294967296 {
return Err(ConfigError::InvalidValue {
field: "bucket_adjust_interval_secs".to_string(),
value: bucket_adjust_interval_secs.to_string(),
reason: "Must be < 4294967296s".to_string(),
});
}
}
} }
Ok(()) Ok(())
} }
@@ -505,6 +533,13 @@ impl ConfigValidator {
}); });
} }
} }
// Check bucket for decode
if let Some(PolicyConfig::Bucket { .. }) = decode_policy {
return Err(ConfigError::IncompatibleConfig {
reason: "Decode policy should not be allowed to be bucket".to_string(),
});
}
} }
} }
@@ -792,6 +827,67 @@ mod tests {
} }
} }
#[test]
fn test_validate_pd_mode_bucket_policy_restrictions() {
let config = RouterConfig::new(
RoutingMode::PrefillDecode {
prefill_urls: vec![
("http://prefill1:8000".to_string(), None),
("http://prefill2:8000".to_string(), None),
],
decode_urls: vec![
"http://decode1:8000".to_string(),
"http://decode2:8000".to_string(),
],
prefill_policy: Some(PolicyConfig::Bucket {
balance_abs_threshold: 32,
balance_rel_threshold: 1.1,
bucket_adjust_interval_secs: 5,
}),
decode_policy: Some(PolicyConfig::PowerOfTwo {
load_check_interval_secs: 60,
}),
},
PolicyConfig::Random, // Main policy as fallback
);
let result = ConfigValidator::validate(&config);
assert!(
result.is_ok(),
"Prefill policy should be allowed to be bucket"
);
let config = RouterConfig::new(
RoutingMode::PrefillDecode {
prefill_urls: vec![
("http://prefill1:8000".to_string(), None),
("http://prefill2:8000".to_string(), None),
],
decode_urls: vec![
"http://decode1:8000".to_string(),
"http://decode2:8000".to_string(),
],
prefill_policy: Some(PolicyConfig::Bucket {
balance_abs_threshold: 32,
balance_rel_threshold: 1.1,
bucket_adjust_interval_secs: 5,
}),
decode_policy: Some(PolicyConfig::Bucket {
balance_abs_threshold: 32,
balance_rel_threshold: 1.1,
bucket_adjust_interval_secs: 5,
}),
},
PolicyConfig::Random, // Main policy as fallback
);
let result = ConfigValidator::validate(&config);
assert!(
result.is_err(),
"Decode policy should not be allowed to be bucket"
);
}
#[test] #[test]
fn test_validate_grpc_requires_tokenizer() { fn test_validate_grpc_requires_tokenizer() {
let mut config = RouterConfig::new( let mut config = RouterConfig::new(
@@ -756,6 +756,13 @@ impl StepExecutor for UpdatePoliciesStep {
.init_cache_aware_policy(&model_id, &all_workers); .init_cache_aware_policy(&model_id, &all_workers);
} }
} }
let prefill_workers = app_context.worker_registry.get_prefill_workers();
let policy = app_context.policy_registry.get_prefill_policy();
if policy.name() == "bucket" {
app_context
.policy_registry
.init_pd_bucket_policies(&prefill_workers);
}
debug!( debug!(
"Updated policies for worker {} (model: {})", "Updated policies for worker {} (model: {})",
+11
View File
@@ -29,6 +29,7 @@ pub enum PolicyType {
RoundRobin, RoundRobin,
CacheAware, CacheAware,
PowerOfTwo, PowerOfTwo,
Bucket,
} }
#[pyclass(eq)] #[pyclass(eq)]
@@ -169,6 +170,8 @@ struct Router {
request_timeout_secs: u64, request_timeout_secs: u64,
request_id_headers: Option<Vec<String>>, request_id_headers: Option<Vec<String>>,
pd_disaggregation: bool, pd_disaggregation: bool,
// Takes effect in PD mode and when policy = bucket
bucket_adjust_interval_secs: usize,
prefill_urls: Option<Vec<(String, Option<u16>)>>, prefill_urls: Option<Vec<(String, Option<u16>)>>,
decode_urls: Option<Vec<String>>, decode_urls: Option<Vec<String>>,
prefill_policy: Option<PolicyType>, prefill_policy: Option<PolicyType>,
@@ -244,6 +247,11 @@ impl Router {
PolicyType::PowerOfTwo => ConfigPolicyConfig::PowerOfTwo { PolicyType::PowerOfTwo => ConfigPolicyConfig::PowerOfTwo {
load_check_interval_secs: 5, load_check_interval_secs: 5,
}, },
PolicyType::Bucket => ConfigPolicyConfig::Bucket {
balance_abs_threshold: self.balance_abs_threshold,
balance_rel_threshold: self.balance_rel_threshold,
bucket_adjust_interval_secs: self.bucket_adjust_interval_secs,
},
} }
}; };
@@ -407,6 +415,7 @@ impl Router {
request_timeout_secs = 1800, request_timeout_secs = 1800,
request_id_headers = None, request_id_headers = None,
pd_disaggregation = false, pd_disaggregation = false,
bucket_adjust_interval_secs = 5,
prefill_urls = None, prefill_urls = None,
decode_urls = None, decode_urls = None,
prefill_policy = None, prefill_policy = None,
@@ -480,6 +489,7 @@ impl Router {
request_timeout_secs: u64, request_timeout_secs: u64,
request_id_headers: Option<Vec<String>>, request_id_headers: Option<Vec<String>>,
pd_disaggregation: bool, pd_disaggregation: bool,
bucket_adjust_interval_secs: usize,
prefill_urls: Option<Vec<(String, Option<u16>)>>, prefill_urls: Option<Vec<(String, Option<u16>)>>,
decode_urls: Option<Vec<String>>, decode_urls: Option<Vec<String>>,
prefill_policy: Option<PolicyType>, prefill_policy: Option<PolicyType>,
@@ -566,6 +576,7 @@ impl Router {
request_timeout_secs, request_timeout_secs,
request_id_headers, request_id_headers,
pd_disaggregation, pd_disaggregation,
bucket_adjust_interval_secs,
prefill_urls, prefill_urls,
decode_urls, decode_urls,
prefill_policy, prefill_policy,
File diff suppressed because it is too large Load Diff
+28 -6
View File
@@ -3,8 +3,8 @@
use std::sync::Arc; use std::sync::Arc;
use super::{ use super::{
CacheAwareConfig, CacheAwarePolicy, LoadBalancingPolicy, PowerOfTwoPolicy, RandomPolicy, BucketConfig, BucketPolicy, CacheAwareConfig, CacheAwarePolicy, LoadBalancingPolicy,
RoundRobinPolicy, PowerOfTwoPolicy, RandomPolicy, RoundRobinPolicy,
}; };
use crate::config::PolicyConfig; use crate::config::PolicyConfig;
@@ -34,6 +34,18 @@ impl PolicyFactory {
}; };
Arc::new(CacheAwarePolicy::with_config(config)) Arc::new(CacheAwarePolicy::with_config(config))
} }
PolicyConfig::Bucket {
balance_abs_threshold,
balance_rel_threshold,
bucket_adjust_interval_secs,
} => {
let config = BucketConfig {
balance_abs_threshold: *balance_abs_threshold,
balance_rel_threshold: *balance_rel_threshold,
bucket_adjust_interval_secs: *bucket_adjust_interval_secs,
};
Arc::new(BucketPolicy::with_config(config))
}
} }
} }
@@ -44,6 +56,7 @@ impl PolicyFactory {
"round_robin" | "roundrobin" => Some(Arc::new(RoundRobinPolicy::new())), "round_robin" | "roundrobin" => Some(Arc::new(RoundRobinPolicy::new())),
"power_of_two" | "poweroftwo" => Some(Arc::new(PowerOfTwoPolicy::new())), "power_of_two" | "poweroftwo" => Some(Arc::new(PowerOfTwoPolicy::new())),
"cache_aware" | "cacheaware" => Some(Arc::new(CacheAwarePolicy::new())), "cache_aware" | "cacheaware" => Some(Arc::new(CacheAwarePolicy::new())),
"bucket" => Some(Arc::new(BucketPolicy::new())),
_ => None, _ => None,
} }
} }
@@ -53,8 +66,8 @@ impl PolicyFactory {
mod tests { mod tests {
use super::*; use super::*;
#[test] #[tokio::test]
fn test_create_from_config() { async fn test_create_from_config() {
let policy = PolicyFactory::create_from_config(&PolicyConfig::Random); let policy = PolicyFactory::create_from_config(&PolicyConfig::Random);
assert_eq!(policy.name(), "random"); assert_eq!(policy.name(), "random");
@@ -74,10 +87,17 @@ mod tests {
max_tree_size: 1000, max_tree_size: 1000,
}); });
assert_eq!(policy.name(), "cache_aware"); assert_eq!(policy.name(), "cache_aware");
let policy = PolicyFactory::create_from_config(&PolicyConfig::Bucket {
balance_abs_threshold: 10,
balance_rel_threshold: 1.5,
bucket_adjust_interval_secs: 5,
});
assert_eq!(policy.name(), "bucket");
} }
#[test] #[tokio::test]
fn test_create_by_name() { async fn test_create_by_name() {
assert!(PolicyFactory::create_by_name("random").is_some()); assert!(PolicyFactory::create_by_name("random").is_some());
assert!(PolicyFactory::create_by_name("RANDOM").is_some()); assert!(PolicyFactory::create_by_name("RANDOM").is_some());
assert!(PolicyFactory::create_by_name("round_robin").is_some()); assert!(PolicyFactory::create_by_name("round_robin").is_some());
@@ -86,6 +106,8 @@ mod tests {
assert!(PolicyFactory::create_by_name("PowerOfTwo").is_some()); assert!(PolicyFactory::create_by_name("PowerOfTwo").is_some());
assert!(PolicyFactory::create_by_name("cache_aware").is_some()); assert!(PolicyFactory::create_by_name("cache_aware").is_some());
assert!(PolicyFactory::create_by_name("CacheAware").is_some()); assert!(PolicyFactory::create_by_name("CacheAware").is_some());
assert!(PolicyFactory::create_by_name("bucket").is_some());
assert!(PolicyFactory::create_by_name("Bucket").is_some());
assert!(PolicyFactory::create_by_name("unknown").is_none()); assert!(PolicyFactory::create_by_name("unknown").is_none());
} }
} }
+19
View File
@@ -7,6 +7,7 @@ use std::{fmt::Debug, sync::Arc};
use crate::core::Worker; use crate::core::Worker;
mod bucket;
mod cache_aware; mod cache_aware;
mod factory; mod factory;
mod power_of_two; mod power_of_two;
@@ -14,6 +15,7 @@ mod random;
mod registry; mod registry;
mod round_robin; mod round_robin;
pub use bucket::BucketPolicy;
pub use cache_aware::CacheAwarePolicy; pub use cache_aware::CacheAwarePolicy;
pub use factory::PolicyFactory; pub use factory::PolicyFactory;
pub use power_of_two::PowerOfTwoPolicy; pub use power_of_two::PowerOfTwoPolicy;
@@ -108,6 +110,23 @@ impl Default for CacheAwareConfig {
} }
} }
#[derive(Debug, Clone)]
pub struct BucketConfig {
pub balance_abs_threshold: usize,
pub balance_rel_threshold: f32,
pub bucket_adjust_interval_secs: usize,
}
impl Default for BucketConfig {
fn default() -> Self {
Self {
balance_abs_threshold: 32,
balance_rel_threshold: 1.0001,
bucket_adjust_interval_secs: 5,
}
}
}
/// Helper function to filter healthy workers and return their indices /// Helper function to filter healthy workers and return their indices
pub(crate) fn get_healthy_worker_indices(workers: &[Arc<dyn Worker>]) -> Vec<usize> { pub(crate) fn get_healthy_worker_indices(workers: &[Arc<dyn Worker>]) -> Vec<usize> {
workers workers
+32 -2
View File
@@ -12,8 +12,8 @@ use tracing::{debug, info, warn};
/// All subsequent workers of the same model use the established policy. /// All subsequent workers of the same model use the established policy.
/// When the last worker of a model is removed, the policy mapping is cleaned up. /// When the last worker of a model is removed, the policy mapping is cleaned up.
use super::{ use super::{
CacheAwareConfig, CacheAwarePolicy, LoadBalancingPolicy, PowerOfTwoPolicy, RandomPolicy, BucketConfig, BucketPolicy, CacheAwareConfig, CacheAwarePolicy, LoadBalancingPolicy,
RoundRobinPolicy, PowerOfTwoPolicy, RandomPolicy, RoundRobinPolicy,
}; };
use crate::{config::types::PolicyConfig, core::Worker}; use crate::{config::types::PolicyConfig, core::Worker};
@@ -176,6 +176,7 @@ impl PolicyRegistry {
"random" => Arc::new(RandomPolicy::new()), "random" => Arc::new(RandomPolicy::new()),
"cache_aware" => Arc::new(CacheAwarePolicy::new()), "cache_aware" => Arc::new(CacheAwarePolicy::new()),
"power_of_two" => Arc::new(PowerOfTwoPolicy::new()), "power_of_two" => Arc::new(PowerOfTwoPolicy::new()),
"bucket" => Arc::new(BucketPolicy::new()),
_ => { _ => {
warn!("Unknown policy type '{}', using default", policy_type); warn!("Unknown policy type '{}', using default", policy_type);
Arc::clone(&self.default_policy) Arc::clone(&self.default_policy)
@@ -205,6 +206,18 @@ impl PolicyRegistry {
Arc::new(CacheAwarePolicy::with_config(cache_config)) Arc::new(CacheAwarePolicy::with_config(cache_config))
} }
PolicyConfig::PowerOfTwo { .. } => Arc::new(PowerOfTwoPolicy::new()), PolicyConfig::PowerOfTwo { .. } => Arc::new(PowerOfTwoPolicy::new()),
PolicyConfig::Bucket {
balance_abs_threshold,
balance_rel_threshold,
bucket_adjust_interval_secs,
} => {
let config = BucketConfig {
balance_abs_threshold: *balance_abs_threshold,
balance_rel_threshold: *balance_rel_threshold,
bucket_adjust_interval_secs: *bucket_adjust_interval_secs,
};
Arc::new(BucketPolicy::with_config(config))
}
} }
} }
@@ -375,6 +388,23 @@ impl PolicyRegistry {
} }
} }
} }
pub fn init_pd_bucket_policies(&self, prefill_workers: &[Arc<dyn Worker>]) {
// Initialize prefill policy if it's bucket
if let Some(prefill_policy) = self.prefill_policy.read().unwrap().as_ref() {
if prefill_policy.name() == "bucket" {
if let Some(bucket) = prefill_policy.as_any().downcast_ref::<BucketPolicy>() {
if !prefill_workers.is_empty() {
debug!(
"Initializing prefill bucket policy with {} workers",
prefill_workers.len()
);
bucket.init_prefill_worker_urls(prefill_workers);
}
}
}
}
}
} }
impl std::fmt::Debug for PolicyRegistry { impl std::fmt::Debug for PolicyRegistry {
+5
View File
@@ -70,4 +70,9 @@ pub enum PDSelectionPolicy {
balance_abs_threshold: usize, balance_abs_threshold: usize,
balance_rel_threshold: f32, balance_rel_threshold: f32,
}, },
Bucket {
balance_abs_threshold: usize,
balance_rel_threshold: f32,
bucket_adjust_interval_secs: usize,
},
} }
+28
View File
@@ -92,6 +92,11 @@ mod test_pd_routing {
balance_abs_threshold: 32, balance_abs_threshold: 32,
balance_rel_threshold: 1.1, balance_rel_threshold: 1.1,
}, },
PDSelectionPolicy::Bucket {
balance_abs_threshold: 32,
balance_rel_threshold: 1.1,
bucket_adjust_interval_secs: 5,
},
]; ];
for policy in policies { for policy in policies {
@@ -107,6 +112,12 @@ mod test_pd_routing {
} => { } => {
assert!(*cache_threshold >= 0.0 && *cache_threshold <= 1.0); assert!(*cache_threshold >= 0.0 && *cache_threshold <= 1.0);
} }
PDSelectionPolicy::Bucket {
balance_rel_threshold,
..
} => {
assert!(*balance_rel_threshold >= 1.0);
}
} }
} }
} }
@@ -160,6 +171,23 @@ mod test_pd_routing {
max_tree_size: 1000000, max_tree_size: 1000000,
}, },
), ),
(
RoutingMode::PrefillDecode {
prefill_urls: vec![
("http://p1:8080".to_string(), Some(9000)),
("http://p2:8080".to_string(), Some(9001)),
("http://p3:8080".to_string(), Some(9002)),
],
decode_urls: vec!["http://d1:8080".to_string(), "http://d2:8080".to_string()],
prefill_policy: None,
decode_policy: None,
},
PolicyConfig::Bucket {
balance_abs_threshold: 20,
balance_rel_threshold: 1.2,
bucket_adjust_interval_secs: 5,
},
),
]; ];
for (mode, policy) in test_cases { for (mode, policy) in test_cases {