#[derive(Debug, Clone)]
struct SimConfig {
num_backends: usize,
slots_per_backend: usize,
total_loras: usize,
concurrent_loras: usize,
total_ticks: usize,
ramp_ticks: usize,
steady_ticks: usize,
ramp_down_ticks: usize,
max_load_per_lora: usize,
lifetime_mean: usize,
lifetime_stddev: f64,
seed: u64,
}
impl SimConfig {
fn effective_lifetime(&self) -> usize {
if self.lifetime_mean > 0 {
self.lifetime_mean
} else {
self.ramp_ticks + self.steady_ticks + self.ramp_down_ticks
}
}
}
impl Default for SimConfig {
fn default() -> Self {
Self {
num_backends: 8,
slots_per_backend: 4,
total_loras: 20,
concurrent_loras: 6,
total_ticks: 60,
ramp_ticks: 5,
steady_ticks: 10,
ramp_down_ticks: 5,
max_load_per_lora: 20,
lifetime_mean: 0,
lifetime_stddev: 0.0,
seed: 42,
}
}
}
#[derive(Debug, Clone)]
struct ChurnMetrics {
algorithm: String,
total_target_additions: usize,
total_target_removals: usize,
total_churn: usize,
peak_churn_per_tick: usize,
per_tick_churn: Vec<usize>,
ticks_with_churn: usize,
avg_churn_per_active_tick: f64,
per_tick_lora_additions: Vec<usize>,
per_tick_lora_removals: Vec<usize>,
total_lora_additions: usize,
total_lora_removals: usize,
per_tick_replica_dist: Vec<HashMap<usize, usize>>,
}
impl ChurnMetrics {
fn new(algorithm: &str) -> Self {
Self {
algorithm: algorithm.to_string(),
total_target_additions: 0,
total_target_removals: 0,
total_churn: 0,
peak_churn_per_tick: 0,
per_tick_replica_dist: Vec::new(),
per_tick_churn: Vec::new(),
ticks_with_churn: 0,
avg_churn_per_active_tick: 0.0,
per_tick_lora_additions: Vec::new(),
per_tick_lora_removals: Vec::new(),
total_lora_additions: 0,
total_lora_removals: 0,
}
}
fn finalize(&mut self) {
self.total_churn = self.total_target_additions + self.total_target_removals;
self.peak_churn_per_tick = self.per_tick_churn.iter().max().copied().unwrap_or(0);
self.ticks_with_churn = self.per_tick_churn.iter().filter(|&&c| c > 0).count();
self.avg_churn_per_active_tick = if self.ticks_with_churn > 0 {
self.total_churn as f64 / self.ticks_with_churn as f64
} else {
0.0
};
self.total_lora_additions = self.per_tick_lora_additions.iter().sum();
self.total_lora_removals = self.per_tick_lora_removals.iter().sum();
}
}
impl std::fmt::Display for ChurnMetrics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, " Algorithm: {}", self.algorithm)?;
writeln!(f, " Route Target Adds: {}", self.total_target_additions)?;
writeln!(f, " Route Target Removes: {}", self.total_target_removals)?;
writeln!(f, " Total Churn: {}", self.total_churn)?;
writeln!(f, " Peak Churn/Tick: {}", self.peak_churn_per_tick)?;
writeln!(f, " Ticks with Churn: {}", self.ticks_with_churn)?;
writeln!(
f,
" Avg Churn/Active Tick: {:.2}",
self.avg_churn_per_active_tick
)?;
writeln!(f, " LoRA Additions: {}", self.total_lora_additions)?;
writeln!(f, " LoRA Removals: {}", self.total_lora_removals)?;
Ok(())
}
}
#[derive(Debug, Clone)]
struct LoraLoadSchedule {
lora_name: String,
active_window: (usize, usize),
peak_load: usize,
ramp_up: usize,
steady: usize,
ramp_down: usize,
per_tick_loads: Option<Vec<usize>>,
}
impl LoraLoadSchedule {
fn load_at_tick(&self, tick: usize) -> usize {
if let Some(ref loads) = self.per_tick_loads {
return loads.get(tick).copied().unwrap_or(0);
}
if tick < self.active_window.0 || tick >= self.active_window.1 {
return 0;
}
let relative_tick = tick - self.active_window.0;
let total_active = self.ramp_up + self.steady + self.ramp_down;
if relative_tick >= total_active {
return 0;
}
if relative_tick < self.ramp_up {
let progress = (relative_tick + 1) as f64 / self.ramp_up as f64;
(progress * self.peak_load as f64).ceil() as usize
} else if relative_tick < self.ramp_up + self.steady {
self.peak_load
} else {
let ramp_down_tick = relative_tick - self.ramp_up - self.steady;
let progress = 1.0 - ((ramp_down_tick + 1) as f64 / self.ramp_down as f64);
(progress * self.peak_load as f64).ceil() as usize
}
}
}
fn sample_poisson(rng: &mut StdRng, lambda: f64) -> usize {
if lambda <= 0.0 {
return 0;
}
if lambda > 30.0 {
let uniform_1 = rng.random::<f64>().max(f64::MIN_POSITIVE);
let uniform_2 = rng.random::<f64>();
let standard_normal =
(-2.0 * uniform_1.ln()).sqrt() * (std::f64::consts::TAU * uniform_2).cos();
let normal = lambda + lambda.sqrt() * standard_normal;
return normal.round().max(0.0) as usize;
}
let l = (-lambda).exp();
let mut k: usize = 0;
let mut p: f64 = 1.0;
loop {
k += 1;
p *= rng.random::<f64>();
if p < l {
break;
}
}
k - 1
}
fn harmonic_number(n: usize, s: f64) -> f64 {
(1..=n).map(|k| 1.0 / (k as f64).powf(s)).sum()
}
fn sample_lifetime(config: &SimConfig, rng: &mut StdRng) -> (usize, usize, usize) {
let lifetime = if config.lifetime_mean > 0 {
if config.lifetime_stddev > 0.0 {
let half_range = config.lifetime_stddev * 3.0_f64.sqrt();
let lo = (config.lifetime_mean as f64 - half_range).max(3.0);
let hi = config.lifetime_mean as f64 + half_range;
rng.random_range(lo as usize..=hi as usize).max(3)
} else {
config.lifetime_mean
}
} else {
config.ramp_ticks + config.steady_ticks + config.ramp_down_ticks
};
if config.lifetime_mean > 0 {
let ramp_up = (lifetime as f64 * 0.20).round().max(1.0) as usize;
let ramp_down = (lifetime as f64 * 0.20).round().max(1.0) as usize;
let steady = lifetime.saturating_sub(ramp_up + ramp_down).max(1);
(ramp_up, steady, ramp_down)
} else {
(
config.ramp_ticks,
config.steady_ticks,
config.ramp_down_ticks,
)
}
}
fn generate_load_schedules(config: &SimConfig) -> Vec<LoraLoadSchedule> {
let mut rng = StdRng::seed_from_u64(config.seed);
let mut schedules = Vec::new();
let avg_lifetime = config.effective_lifetime() as f64;
let c = config.concurrent_loras.max(1) as f64;
let spacing = avg_lifetime / c;
for i in 0..config.total_loras {
let start_tick = (i as f64 * spacing) as usize;
if start_tick >= config.total_ticks {
break;
}
let (ramp_up, steady, ramp_down) = sample_lifetime(config, &mut rng);
let active_duration = ramp_up + steady + ramp_down;
let end_tick = (start_tick + active_duration).min(config.total_ticks);
schedules.push(LoraLoadSchedule {
lora_name: format!("lora-{:03}", i),
active_window: (start_tick, end_tick),
peak_load: config.max_load_per_lora,
ramp_up,
steady,
ramp_down,
per_tick_loads: None,
});
}
schedules
}
fn generate_zipf_poisson_schedules(
total_loras: usize,
total_ticks: usize,
zipf_s: f64,
avg_total_load: f64,
seed: u64,
) -> Vec<LoraLoadSchedule> {
let mut rng = StdRng::seed_from_u64(seed);
let h = harmonic_number(total_loras, zipf_s);
let lambdas: Vec<f64> = (1..=total_loras)
.map(|k| avg_total_load / ((k as f64).powf(zipf_s) * h))
.collect();
let mut schedules: Vec<LoraLoadSchedule> = Vec::with_capacity(total_loras);
for (idx, &lambda) in lambdas.iter().enumerate() {
let mut per_tick = Vec::with_capacity(total_ticks);
let mut peak = 0usize;
let mut first_active = total_ticks; let mut last_active = 0usize;
for _tick in 0..total_ticks {
let load = sample_poisson(&mut rng, lambda);
if load > 0 {
if _tick < first_active {
first_active = _tick;
}
last_active = _tick;
peak = peak.max(load);
}
per_tick.push(load);
}
if first_active < total_ticks {
schedules.push(LoraLoadSchedule {
lora_name: format!("lora-{:03}", idx),
active_window: (first_active, last_active + 1),
peak_load: peak,
ramp_up: 0,
steady: 0,
ramp_down: 0,
per_tick_loads: Some(per_tick),
});
}
}
schedules.sort_by_key(|s| s.lora_name.clone());
schedules
}
fn generate_diurnal_schedules(
total_loras: usize,
total_ticks: usize,
ticks_per_day: usize,
zipf_s: f64,
peak_total_load: f64,
trough_total_load: f64,
seed: u64,
) -> Vec<LoraLoadSchedule> {
let mut rng = StdRng::seed_from_u64(seed);
let h = harmonic_number(total_loras, zipf_s);
let weights: Vec<f64> = (1..=total_loras)
.map(|k| 1.0 / ((k as f64).powf(zipf_s) * h))
.collect();
let amplitude = (peak_total_load - trough_total_load) / 2.0;
let baseline = (peak_total_load + trough_total_load) / 2.0;
let mut schedules: Vec<LoraLoadSchedule> = Vec::with_capacity(total_loras);
for (idx, &w) in weights.iter().enumerate() {
let mut per_tick = Vec::with_capacity(total_ticks);
let mut peak = 0usize;
let mut first_active = total_ticks;
let mut last_active = 0usize;
for tick in 0..total_ticks {
let phase =
2.0 * std::f64::consts::PI * (tick % ticks_per_day) as f64 / ticks_per_day as f64;
let total_rate = baseline - amplitude * phase.cos(); let lambda = total_rate * w;
let load = sample_poisson(&mut rng, lambda);
if load > 0 {
if tick < first_active {
first_active = tick;
}
last_active = tick;
peak = peak.max(load);
}
per_tick.push(load);
}
if first_active < total_ticks {
schedules.push(LoraLoadSchedule {
lora_name: format!("lora-{:03}", idx),
active_window: (first_active, last_active + 1),
peak_load: peak,
ramp_up: 0,
steady: 0,
ramp_down: 0,
per_tick_loads: Some(per_tick),
});
}
}
schedules.sort_by_key(|s| s.lora_name.clone());
schedules
}
#[allow(clippy::too_many_arguments)]
fn generate_flash_crowd_schedules(
total_loras: usize,
total_ticks: usize,
zipf_s: f64,
base_total_load: f64,
spike_multiplier: f64,
decay_half_life: f64,
flash_ticks: &[usize],
seed: u64,
) -> Vec<LoraLoadSchedule> {
let mut rng = StdRng::seed_from_u64(seed);
let h = harmonic_number(total_loras, zipf_s);
let weights: Vec<f64> = (1..=total_loras)
.map(|k| 1.0 / ((k as f64).powf(zipf_s) * h))
.collect();
let decay_rate = (0.5_f64).ln() / decay_half_life; let mut multipliers = vec![1.0_f64; total_ticks];
for &ft in flash_ticks {
for (t, m) in multipliers.iter_mut().enumerate().skip(ft) {
let elapsed = (t - ft) as f64;
*m += (spike_multiplier - 1.0) * (decay_rate * elapsed).exp();
}
}
let mut schedules: Vec<LoraLoadSchedule> = Vec::with_capacity(total_loras);
for (idx, &w) in weights.iter().enumerate() {
let mut per_tick = Vec::with_capacity(total_ticks);
let mut peak = 0usize;
let mut first_active = total_ticks;
let mut last_active = 0usize;
for (tick, &mult) in multipliers.iter().enumerate() {
let lambda = base_total_load * mult * w;
let load = sample_poisson(&mut rng, lambda);
if load > 0 {
if tick < first_active {
first_active = tick;
}
last_active = tick;
peak = peak.max(load);
}
per_tick.push(load);
}
if first_active < total_ticks {
schedules.push(LoraLoadSchedule {
lora_name: format!("lora-{:03}", idx),
active_window: (first_active, last_active + 1),
peak_load: peak,
ramp_up: 0,
steady: 0,
ramp_down: 0,
per_tick_loads: Some(per_tick),
});
}
}
schedules.sort_by_key(|s| s.lora_name.clone());
schedules
}
fn generate_mmpp_schedules(
total_loras: usize,
total_ticks: usize,
zipf_s: f64,
state_rates: &[f64], transition_matrix: &[Vec<f64>], seed: u64,
) -> (Vec<LoraLoadSchedule>, Vec<usize>) {
let mut rng = StdRng::seed_from_u64(seed);
let h = harmonic_number(total_loras, zipf_s);
let weights: Vec<f64> = (1..=total_loras)
.map(|k| 1.0 / ((k as f64).powf(zipf_s) * h))
.collect();
let mut state_seq = Vec::with_capacity(total_ticks);
let mut current_state = 0usize; for _ in 0..total_ticks {
state_seq.push(current_state);
let r: f64 = rng.random();
let mut cumulative = 0.0;
for (next, &prob) in transition_matrix[current_state].iter().enumerate() {
cumulative += prob;
if r < cumulative {
current_state = next;
break;
}
}
}
let mut schedules: Vec<LoraLoadSchedule> = Vec::with_capacity(total_loras);
for (idx, &w) in weights.iter().enumerate() {
let mut per_tick = Vec::with_capacity(total_ticks);
let mut peak = 0usize;
let mut first_active = total_ticks;
let mut last_active = 0usize;
for (tick, &state) in state_seq.iter().enumerate() {
let lambda = state_rates[state] * w;
let load = sample_poisson(&mut rng, lambda);
if load > 0 {
if tick < first_active {
first_active = tick;
}
last_active = tick;
peak = peak.max(load);
}
per_tick.push(load);
}
if first_active < total_ticks {
schedules.push(LoraLoadSchedule {
lora_name: format!("lora-{:03}", idx),
active_window: (first_active, last_active + 1),
peak_load: peak,
ramp_up: 0,
steady: 0,
ramp_down: 0,
per_tick_loads: Some(per_tick),
});
}
}
schedules.sort_by_key(|s| s.lora_name.clone());
(schedules, state_seq)
}