mirror of
https://github.com/NousResearch/atropos.git
synced 2026-04-26 17:13:09 +00:00
67 lines
2.5 KiB
Python
67 lines
2.5 KiB
Python
import logging
|
|
import math
|
|
from typing import Optional, Dict, Any, List
|
|
from .metrics import WorkloadMetrics
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ScalingController:
|
|
"""
|
|
Decides the "Desired Actor Count" based on workload metrics.
|
|
Uses a dampened calculation with hysteresis to avoid flapping.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
min_actors: int = 1,
|
|
max_actors: int = 20,
|
|
target_pressure: float = 1.0,
|
|
scaling_threshold: float = 0.2, # ±20%
|
|
cooldown_seconds: int = 60,
|
|
max_step_change: int = 4
|
|
):
|
|
self.min_actors = min_actors
|
|
self.max_actors = max_actors
|
|
self.target_pressure = target_pressure
|
|
self.scaling_threshold = scaling_threshold
|
|
self.cooldown_seconds = cooldown_seconds
|
|
self.max_step_change = max_step_change
|
|
|
|
self.last_action_timestamp = 0
|
|
self.current_desired = min_actors
|
|
|
|
def calculate_desired(self, metrics: WorkloadMetrics, current_actors: int) -> int:
|
|
"""
|
|
Decides the next target for the number of environment actors.
|
|
"""
|
|
now = metrics.timestamp
|
|
pressure = metrics.rollout_pressure
|
|
|
|
# 1. Check cooldown
|
|
if now - self.last_action_timestamp < self.cooldown_seconds:
|
|
return self.current_desired
|
|
|
|
# 2. Sensitivity check (Hysteresis)
|
|
# If work is roughly satisfying target, don't change anything.
|
|
if abs(pressure - self.target_pressure) < self.scaling_threshold:
|
|
return self.current_desired
|
|
|
|
# 3. Calculate target
|
|
# Target = Current * (Current_Pressure / Ideal_Pressure)
|
|
# This is a dampened proportional controller.
|
|
raw_target = math.ceil(current_actors * (pressure / self.target_pressure))
|
|
|
|
# 4. Apply step constraints (Rate Limiting)
|
|
# Don't add/remove more than max_step_change in a single move.
|
|
diff = raw_target - current_actors
|
|
if abs(diff) > self.max_step_change:
|
|
raw_target = current_actors + (self.max_step_change if diff > 0 else -self.max_step_change)
|
|
|
|
# 5. Apply world bounds
|
|
final_target = max(self.min_actors, min(self.max_actors, raw_target))
|
|
|
|
if final_target != current_actors:
|
|
self.last_action_timestamp = now
|
|
self.current_desired = final_target
|
|
logger.info(f"Controller DECISION: Scale {current_actors} -> {final_target} (Pressure: {pressure:.2f})")
|
|
|
|
return final_target
|