Spaces:
Running
Running
| from __future__ import annotations | |
| import heapq | |
| import math | |
| import random | |
| from dataclasses import asdict, dataclass | |
| from statistics import mean, median | |
| from typing import Any | |
| from .latency import AnalyticalLatencyModel | |
| from .metrics import percentile | |
| from .prediction import OnlineToolGapPredictor, PREDICTOR_SCOPES | |
| from .profiles import get_accelerator, get_model | |
| ROUTING_POLICIES = {"least_load", "session_affinity", "bounded_affinity"} | |
| RETENTION_POLICIES = {"evict", "retain", "ttl", "offload", "gap_aware", "adaptive"} | |
| TOOL_GAP_MULTIPLIERS = { | |
| "filesystem": 0.45, | |
| "search": 0.80, | |
| "database": 1.25, | |
| "remote_api": 2.20, | |
| } | |
| TOOL_WEIGHTS = (0.28, 0.32, 0.22, 0.18) | |
| class AgentSessionConfig: | |
| model: str = "Qwen2.5-3B" | |
| accelerator: str = "L4" | |
| quantization: str = "int8" | |
| seed: int = 7 | |
| duration_s: float = 30.0 | |
| session_rate_rps: float = 0.45 | |
| replicas: int = 2 | |
| turns_mean: float = 4.0 | |
| turns_cv: float = 0.25 | |
| initial_prompt_tokens_mean: int = 640 | |
| append_tokens_mean: int = 180 | |
| token_cv: float = 0.35 | |
| output_tokens_mean: int = 72 | |
| output_tokens_cv: float = 0.45 | |
| tool_gap_mean_s: float = 1.5 | |
| tool_gap_cv: float = 0.75 | |
| retention_policy: str = "ttl" | |
| kv_ttl_s: float = 3.0 | |
| routing_policy: str = "session_affinity" | |
| kv_memory_fraction: float = 0.85 | |
| kv_capacity_override_gb: float = 0.0 | |
| host_memory_gb: float = 32.0 | |
| host_bandwidth_gbps: float = 32.0 | |
| host_transfer_base_ms: float = 0.15 | |
| affinity_slack_ms: float = 150.0 | |
| gap_aware_threshold_s: float = 1.5 | |
| adaptive_predictor_scope: str = "per_tool_ema" | |
| adaptive_alpha: float = 0.30 | |
| adaptive_min_observations: int = 2 | |
| tool_regime_shift_fraction: float = 0.0 | |
| tool_regime_shift_multiplier: float = 1.0 | |
| slo_turn_ttft_ms: float = 500.0 | |
| slo_session_e2e_ms: float = 30000.0 | |
| timeline_points: int = 240 | |
| def from_dict(cls, data: dict[str, Any]) -> "AgentSessionConfig": | |
| allowed = cls.__dataclass_fields__.keys() | |
| cfg = cls(**{k: data[k] for k in allowed if k in data}) | |
| if cfg.routing_policy not in ROUTING_POLICIES: | |
| raise ValueError(f"Unsupported agent routing policy: {cfg.routing_policy}") | |
| if cfg.retention_policy not in RETENTION_POLICIES: | |
| raise ValueError(f"Unsupported KV retention policy: {cfg.retention_policy}") | |
| cfg.replicas = max(1, min(int(cfg.replicas), 8)) | |
| cfg.session_rate_rps = max(float(cfg.session_rate_rps), 0.01) | |
| cfg.duration_s = max(float(cfg.duration_s), 1.0) | |
| cfg.kv_ttl_s = max(float(cfg.kv_ttl_s), 0.0) | |
| cfg.kv_memory_fraction = min(max(float(cfg.kv_memory_fraction), 0.01), 0.98) | |
| cfg.kv_capacity_override_gb = max(float(cfg.kv_capacity_override_gb), 0.0) | |
| cfg.host_memory_gb = max(float(cfg.host_memory_gb), 0.0) | |
| cfg.host_bandwidth_gbps = max(float(cfg.host_bandwidth_gbps), 0.1) | |
| cfg.host_transfer_base_ms = max(float(cfg.host_transfer_base_ms), 0.0) | |
| cfg.affinity_slack_ms = max(float(cfg.affinity_slack_ms), 0.0) | |
| cfg.gap_aware_threshold_s = max(float(cfg.gap_aware_threshold_s), 0.0) | |
| if cfg.adaptive_predictor_scope not in PREDICTOR_SCOPES: | |
| raise ValueError(f"Unsupported adaptive predictor scope: {cfg.adaptive_predictor_scope}") | |
| cfg.adaptive_alpha = min(max(float(cfg.adaptive_alpha), 0.01), 1.0) | |
| cfg.adaptive_min_observations = max(1, int(cfg.adaptive_min_observations)) | |
| cfg.tool_regime_shift_fraction = min(max(float(cfg.tool_regime_shift_fraction), 0.0), 0.95) | |
| cfg.tool_regime_shift_multiplier = max(float(cfg.tool_regime_shift_multiplier), 0.1) | |
| return cfg | |
| def to_dict(self) -> dict[str, Any]: | |
| return asdict(self) | |
| class TurnSpec: | |
| session_id: int | |
| turn_index: int | |
| append_tokens: int | |
| output_tokens: int | |
| tool_gap_after_s: float | |
| tool_kind: str = "generic" | |
| shifted_regime: bool = False | |
| class SessionSpec: | |
| session_id: int | |
| arrival_time: float | |
| initial_prompt_tokens: int | |
| turns: list[TurnSpec] | |
| class CacheEntry: | |
| session_id: int | |
| tokens: int | |
| size_gb: float | |
| last_access: float | |
| expiry_time: float | None | |
| generation: int | |
| source_replica: int | None = None | |
| available_time: float = 0.0 | |
| class ReplicaState: | |
| replica_id: int | |
| busy: bool = False | |
| busy_until: float = 0.0 | |
| queue: list[tuple[int, TurnSpec, float]] | None = None | |
| cache: dict[int, CacheEntry] | None = None | |
| current_session: int | None = None | |
| def __post_init__(self) -> None: | |
| if self.queue is None: | |
| self.queue = [] | |
| if self.cache is None: | |
| self.cache = {} | |
| class SessionRuntime: | |
| spec: SessionSpec | |
| context_tokens: int | |
| completed_turns: int = 0 | |
| completion_time: float | None = None | |
| last_replica: int | None = None | |
| def _sample_positive_lognormal(rng: random.Random, mean_value: float, cv: float, minimum: float) -> float: | |
| mean_value = max(float(mean_value), minimum) | |
| cv = max(float(cv), 0.0) | |
| if mean_value <= 0.0: | |
| return minimum | |
| if cv <= 1e-12: | |
| return mean_value | |
| sigma2 = math.log1p(cv * cv) | |
| sigma = math.sqrt(sigma2) | |
| mu = math.log(mean_value) - sigma2 / 2.0 | |
| return max(minimum, rng.lognormvariate(mu, sigma)) | |
| def _sample_int(rng: random.Random, mean_value: float, cv: float, minimum: int = 1) -> int: | |
| return max(minimum, int(round(_sample_positive_lognormal(rng, mean_value, cv, minimum)))) | |
| def generate_agent_sessions(cfg: AgentSessionConfig) -> list[SessionSpec]: | |
| """Generate a deterministic multi-turn program trace. | |
| All policies consume this same trace when the seed/config are unchanged. Tool | |
| gaps are sampled up front so policy comparisons use common random numbers. | |
| """ | |
| rng = random.Random(cfg.seed ^ 0xA63E17) | |
| sessions: list[SessionSpec] = [] | |
| now = 0.0 | |
| sid = 0 | |
| while True: | |
| now += rng.expovariate(cfg.session_rate_rps) | |
| if now > cfg.duration_s: | |
| break | |
| turns = max(2, _sample_int(rng, cfg.turns_mean, cfg.turns_cv, 2)) | |
| initial = _sample_int(rng, cfg.initial_prompt_tokens_mean, cfg.token_cv, 32) | |
| specs = [] | |
| tool_names = tuple(TOOL_GAP_MULTIPLIERS) | |
| shifted = cfg.tool_regime_shift_fraction > 0.0 and now >= cfg.duration_s * cfg.tool_regime_shift_fraction | |
| for turn_idx in range(turns): | |
| append = 0 if turn_idx == 0 else _sample_int(rng, cfg.append_tokens_mean, cfg.token_cv, 8) | |
| output = _sample_int(rng, cfg.output_tokens_mean, cfg.output_tokens_cv, 1) | |
| tool_kind = rng.choices(tool_names, weights=TOOL_WEIGHTS, k=1)[0] | |
| if turn_idx == turns - 1: | |
| gap = 0.0 | |
| else: | |
| mean_gap = cfg.tool_gap_mean_s * TOOL_GAP_MULTIPLIERS[tool_kind] | |
| # The optional shift deliberately changes only the slower external | |
| # tools. This creates a non-stationary workload where a global | |
| # duration average is less informative than tool-aware history. | |
| if shifted and tool_kind in {"database", "remote_api"}: | |
| mean_gap *= cfg.tool_regime_shift_multiplier | |
| gap = _sample_positive_lognormal(rng, mean_gap, cfg.tool_gap_cv, 0.0) | |
| specs.append(TurnSpec(sid, turn_idx, append, output, gap, tool_kind, shifted)) | |
| sessions.append(SessionSpec(sid, now, initial, specs)) | |
| sid += 1 | |
| return sessions | |
| class AgentSessionSimulator: | |
| """Discrete-event simulator for stateful multi-turn serving. | |
| The module deliberately isolates session locality / KV-retention effects from | |
| dynamic batching. Each replica is a serial analytical service station; the | |
| existing Serving Lab remains the place to study continuous batching. This | |
| separation keeps the agent experiment interpretable while still modelling | |
| session dependencies, tool gaps, routing, memory pressure, and cross-turn KV. | |
| """ | |
| def __init__(self, cfg: AgentSessionConfig, sessions: list[SessionSpec] | None = None): | |
| self.cfg = cfg | |
| self.model = get_model(cfg.model) | |
| self.accelerator = get_accelerator(cfg.accelerator) | |
| self.latency = AnalyticalLatencyModel(self.model, self.accelerator, cfg.quantization) | |
| remaining = max(0.0, self.accelerator.vram_gb - self.latency.model_weight_gb - 1.2) | |
| automatic_capacity = remaining * cfg.kv_memory_fraction | |
| self.kv_capacity_gb = ( | |
| min(automatic_capacity, cfg.kv_capacity_override_gb) | |
| if cfg.kv_capacity_override_gb > 0.0 | |
| else automatic_capacity | |
| ) | |
| self.replicas = [ReplicaState(i) for i in range(cfg.replicas)] | |
| self.host_cache: dict[int, CacheEntry] = {} | |
| specs = sessions if sessions is not None else generate_agent_sessions(cfg) | |
| self.sessions = {s.session_id: SessionRuntime(s, s.initial_prompt_tokens) for s in specs} | |
| self.events: list[tuple[float, int, str, tuple[Any, ...]]] = [] | |
| self.event_seq = 0 | |
| self.now = 0.0 | |
| self.turn_rows: list[dict[str, Any]] = [] | |
| self.timeline: list[dict[str, Any]] = [] | |
| self.peak_kv_gb = 0.0 | |
| self.peak_replica_kv_gb = 0.0 | |
| self.hbm_gb_seconds = 0.0 | |
| self.host_gb_seconds = 0.0 | |
| self.peak_host_gb = 0.0 | |
| self.last_memory_time = 0.0 | |
| self.pressure_evictions = 0 | |
| self.ttl_evictions = 0 | |
| self.host_pressure_evictions = 0 | |
| self.host_cache_hits = 0 | |
| self.offload_bytes = 0.0 | |
| self.restore_bytes = 0.0 | |
| self.host_transfer_latencies_ms: list[float] = [] | |
| self.recompute_tokens = 0 | |
| self.cache_hits = 0 | |
| self.cache_eligible_turns = 0 | |
| self.affinity_routes = 0 | |
| self.route_opportunities = 0 | |
| self.failed_turns = 0 | |
| self.tool_gap_total_s = 0.0 | |
| self.tool_gap_predictor = OnlineToolGapPredictor( | |
| initial_mean_s=cfg.tool_gap_mean_s, | |
| alpha=cfg.adaptive_alpha, | |
| min_observations=cfg.adaptive_min_observations, | |
| scope=cfg.adaptive_predictor_scope, | |
| ) | |
| self.prediction_rows: list[dict[str, Any]] = [] | |
| for session in specs: | |
| if session.turns: | |
| self._push(session.arrival_time, "turn_ready", session.session_id, 0) | |
| def _push(self, when: float, kind: str, *payload: Any) -> None: | |
| self.event_seq += 1 | |
| heapq.heappush(self.events, (float(when), self.event_seq, kind, tuple(payload))) | |
| def _used_gb(self) -> float: | |
| return sum(entry.size_gb for replica in self.replicas for entry in replica.cache.values()) | |
| def _used_host_gb(self) -> float: | |
| return sum(entry.size_gb for entry in self.host_cache.values()) | |
| def _integrate_memory(self, new_time: float) -> None: | |
| new_time = max(float(new_time), self.last_memory_time) | |
| elapsed = new_time - self.last_memory_time | |
| used = self._used_gb() | |
| host_used = self._used_host_gb() | |
| self.hbm_gb_seconds += used * elapsed | |
| self.host_gb_seconds += host_used * elapsed | |
| self.last_memory_time = new_time | |
| self.peak_kv_gb = max(self.peak_kv_gb, used) | |
| self.peak_replica_kv_gb = max( | |
| self.peak_replica_kv_gb, | |
| max((sum(entry.size_gb for entry in replica.cache.values()) for replica in self.replicas), default=0.0), | |
| ) | |
| self.peak_host_gb = max(self.peak_host_gb, host_used) | |
| def _record_timeline(self) -> None: | |
| max_points = max(int(self.cfg.timeline_points), 40) | |
| if self.timeline and self.now - self.timeline[-1]["time_s"] < max(self.cfg.duration_s / max_points, 0.05): | |
| return | |
| self.timeline.append( | |
| { | |
| "time_s": self.now, | |
| "queued_turns": sum(len(r.queue) for r in self.replicas), | |
| "busy_replicas": sum(1 for r in self.replicas if r.busy), | |
| "kv_used_gb": self._used_gb(), | |
| "host_kv_gb": self._used_host_gb(), | |
| "resident_sessions": sum(len(r.cache) for r in self.replicas), | |
| "host_sessions": len(self.host_cache), | |
| } | |
| ) | |
| if len(self.timeline) > max_points * 2: | |
| self.timeline = self.timeline[::2] | |
| def _entry(self, replica: ReplicaState, session_id: int) -> CacheEntry | None: | |
| return replica.cache.get(session_id) | |
| def _cache_replica(self, session_id: int) -> ReplicaState | None: | |
| for replica in self.replicas: | |
| if session_id in replica.cache: | |
| return replica | |
| return None | |
| def _replica_load_key(self, replica: ReplicaState) -> tuple[int, float, int]: | |
| return (len(replica.queue), max(replica.busy_until, self.now), replica.replica_id) | |
| def _route(self, session_id: int) -> ReplicaState: | |
| cached = self._cache_replica(session_id) | |
| if cached is not None: | |
| self.route_opportunities += 1 | |
| least_loaded = min(self.replicas, key=self._replica_load_key) | |
| if self.cfg.routing_policy == "session_affinity" and cached is not None: | |
| self.affinity_routes += 1 | |
| return cached | |
| if self.cfg.routing_policy == "bounded_affinity" and cached is not None: | |
| cached_ready = max(cached.busy_until, self.now) | |
| best_ready = max(least_loaded.busy_until, self.now) | |
| queue_gap = max(0, len(cached.queue) - len(least_loaded.queue)) | |
| # Queue length is converted to a modest reference penalty because | |
| # exact queued service time is intentionally not known at routing time. | |
| estimated_extra_ms = max(0.0, cached_ready - best_ready) * 1000.0 + queue_gap * 75.0 | |
| if estimated_extra_ms <= self.cfg.affinity_slack_ms: | |
| self.affinity_routes += 1 | |
| return cached | |
| return least_loaded | |
| def _remove_cache(self, replica: ReplicaState, session_id: int, reason: str) -> None: | |
| if session_id not in replica.cache: | |
| return | |
| self._integrate_memory(self.now) | |
| del replica.cache[session_id] | |
| if reason == "pressure": | |
| self.pressure_evictions += 1 | |
| elif reason == "ttl": | |
| self.ttl_evictions += 1 | |
| self._record_timeline() | |
| def _host_transfer_seconds(self, size_gb: float) -> float: | |
| return self.cfg.host_transfer_base_ms / 1000.0 + size_gb / self.cfg.host_bandwidth_gbps | |
| def _remove_host_cache(self, session_id: int) -> None: | |
| if session_id not in self.host_cache: | |
| return | |
| self._integrate_memory(self.now) | |
| del self.host_cache[session_id] | |
| self._record_timeline() | |
| def _ensure_host_capacity(self, session_id: int, target_gb: float) -> bool: | |
| if target_gb > self.cfg.host_memory_gb + 1e-12: | |
| return False | |
| while self._used_host_gb() + target_gb > self.cfg.host_memory_gb + 1e-12: | |
| victims = [entry for sid, entry in self.host_cache.items() if sid != session_id] | |
| if not victims: | |
| return False | |
| victim = min(victims, key=lambda e: (e.last_access, e.session_id)) | |
| self._integrate_memory(self.now) | |
| del self.host_cache[victim.session_id] | |
| self.host_pressure_evictions += 1 | |
| return True | |
| def _offload_cache(self, replica: ReplicaState, session_id: int, tokens: int) -> bool: | |
| size_gb = tokens * self.latency.kv_bytes_per_token() / 1e9 | |
| if self.cfg.host_memory_gb <= 0.0 or not self._ensure_host_capacity(session_id, size_gb): | |
| self._remove_cache(replica, session_id, "complete") | |
| return False | |
| transfer_s = self._host_transfer_seconds(size_gb) | |
| self._integrate_memory(self.now) | |
| replica.cache.pop(session_id, None) | |
| self.host_cache[session_id] = CacheEntry( | |
| session_id=session_id, | |
| tokens=tokens, | |
| size_gb=size_gb, | |
| last_access=self.now, | |
| expiry_time=None, | |
| generation=1, | |
| source_replica=replica.replica_id, | |
| available_time=self.now + transfer_s, | |
| ) | |
| self.offload_bytes += size_gb * 1e9 | |
| self.host_transfer_latencies_ms.append(transfer_s * 1000.0) | |
| self.peak_host_gb = max(self.peak_host_gb, self._used_host_gb()) | |
| self._record_timeline() | |
| return True | |
| def _restore_host_entry(self, replica: ReplicaState, session_id: int) -> tuple[bool, float]: | |
| entry = self.host_cache.get(session_id) | |
| if entry is None: | |
| return False, 0.0 | |
| if not self._ensure_capacity(replica, session_id, entry.size_gb): | |
| return False, 0.0 | |
| wait_s = max(0.0, entry.available_time - self.now) | |
| copy_s = self._host_transfer_seconds(entry.size_gb) | |
| # The host entry remains resident while an unfinished offload or restore | |
| # is exposed. Account for that residency explicitly because the event | |
| # clock advances only when the turn completes. | |
| self.host_gb_seconds += entry.size_gb * (wait_s + copy_s) | |
| self._integrate_memory(self.now) | |
| del self.host_cache[session_id] | |
| self.restore_bytes += entry.size_gb * 1e9 | |
| self.host_transfer_latencies_ms.append((wait_s + copy_s) * 1000.0) | |
| self.host_cache_hits += 1 | |
| self._record_timeline() | |
| return True, wait_s + copy_s | |
| def _ensure_capacity(self, replica: ReplicaState, session_id: int, target_gb: float) -> bool: | |
| current = replica.cache.get(session_id) | |
| current_gb = current.size_gb if current else 0.0 | |
| additional = max(0.0, target_gb - current_gb) | |
| if additional <= 1e-12: | |
| return True | |
| # Capacity is per replica; evict least-recently-used inactive session KV. | |
| while sum(e.size_gb for e in replica.cache.values()) + additional > self.kv_capacity_gb + 1e-12: | |
| victims = [e for sid, e in replica.cache.items() if sid != session_id] | |
| if not victims: | |
| return False | |
| victim = min(victims, key=lambda e: (e.last_access, e.session_id)) | |
| self._remove_cache(replica, victim.session_id, "pressure") | |
| return True | |
| def _put_cache(self, replica: ReplicaState, session_id: int, tokens: int, keep: bool) -> bool: | |
| size_gb = tokens * self.latency.kv_bytes_per_token() / 1e9 | |
| if size_gb > self.kv_capacity_gb + 1e-12: | |
| return False | |
| if not self._ensure_capacity(replica, session_id, size_gb): | |
| return False | |
| self._integrate_memory(self.now) | |
| previous = replica.cache.get(session_id) | |
| generation = (previous.generation + 1) if previous else 1 | |
| expiry: float | None = None | |
| if keep and self.cfg.retention_policy in {"ttl", "adaptive"}: | |
| expiry = self.now + self.cfg.kv_ttl_s | |
| replica.cache[session_id] = CacheEntry(session_id, tokens, size_gb, self.now, expiry, generation) | |
| self.peak_kv_gb = max(self.peak_kv_gb, self._used_gb()) | |
| self.peak_replica_kv_gb = max( | |
| self.peak_replica_kv_gb, | |
| sum(entry.size_gb for entry in replica.cache.values()), | |
| ) | |
| if expiry is not None: | |
| self._push(expiry, "cache_expire", replica.replica_id, session_id, generation) | |
| self._record_timeline() | |
| return True | |
| def _start_next(self, replica: ReplicaState) -> None: | |
| if replica.busy or not replica.queue: | |
| return | |
| session_id, turn, ready_time = replica.queue.pop(0) | |
| runtime = self.sessions[session_id] | |
| cache = self._entry(replica, session_id) | |
| hbm_hit = turn.turn_index > 0 and cache is not None | |
| host_hit = False | |
| restore_s = 0.0 | |
| if turn.turn_index > 0: | |
| self.cache_eligible_turns += 1 | |
| if hbm_hit: | |
| self.cache_hits += 1 | |
| prefill_tokens = max(turn.append_tokens, 1) | |
| cache_source = "hbm" | |
| elif turn.turn_index > 0 and session_id in self.host_cache: | |
| host_hit, restore_s = self._restore_host_entry(replica, session_id) | |
| if host_hit: | |
| prefill_tokens = max(turn.append_tokens, 1) | |
| cache_source = "host" | |
| else: | |
| prefill_tokens = max(runtime.context_tokens + turn.append_tokens, 1) | |
| self.recompute_tokens += runtime.context_tokens | |
| cache_source = "miss" | |
| else: | |
| prefill_tokens = max(runtime.context_tokens + turn.append_tokens, 1) | |
| if turn.turn_index > 0: | |
| self.recompute_tokens += runtime.context_tokens | |
| cache_source = "miss" | |
| context_before_decode = runtime.context_tokens + turn.append_tokens | |
| projected_tokens = context_before_decode + turn.output_tokens | |
| projected_gb = projected_tokens * self.latency.kv_bytes_per_token() / 1e9 | |
| if not self._ensure_capacity(replica, session_id, projected_gb): | |
| self.failed_turns += 1 | |
| self._push(self.now, "turn_failed", session_id, turn.turn_index, replica.replica_id, ready_time) | |
| self._start_next(replica) | |
| return | |
| # Model the current turn's KV as resident while it executes, even when | |
| # the selected policy will evict it immediately after the turn. | |
| if not self._put_cache(replica, session_id, projected_tokens, keep=False): | |
| self.failed_turns += 1 | |
| self._push(self.now, "turn_failed", session_id, turn.turn_index, replica.replica_id, ready_time) | |
| self._start_next(replica) | |
| return | |
| prefill_s = self.latency.prefill_seconds([prefill_tokens]) | |
| first_decode_s = self.latency.decode_step_seconds([context_before_decode]) | |
| midpoint = context_before_decode + max(turn.output_tokens // 2, 1) | |
| decode_step_s = self.latency.decode_step_seconds([midpoint]) | |
| service_s = restore_s + prefill_s + turn.output_tokens * decode_step_s | |
| ttft_s = (self.now - ready_time) + restore_s + prefill_s + first_decode_s | |
| replica.busy = True | |
| replica.current_session = session_id | |
| replica.busy_until = self.now + service_s | |
| self._push( | |
| replica.busy_until, | |
| "turn_complete", | |
| session_id, | |
| turn.turn_index, | |
| replica.replica_id, | |
| ready_time, | |
| hbm_hit or host_hit, | |
| cache_source, | |
| restore_s, | |
| prefill_tokens, | |
| ttft_s, | |
| service_s, | |
| projected_tokens, | |
| ) | |
| def _finish_turn( | |
| self, | |
| session_id: int, | |
| turn_index: int, | |
| replica_id: int, | |
| ready_time: float, | |
| cache_hit: bool, | |
| cache_source: str, | |
| restore_s: float, | |
| prefill_tokens: int, | |
| ttft_s: float, | |
| service_s: float, | |
| projected_tokens: int, | |
| ) -> None: | |
| replica = self.replicas[replica_id] | |
| runtime = self.sessions[session_id] | |
| turn = runtime.spec.turns[turn_index] | |
| runtime.context_tokens = projected_tokens | |
| runtime.completed_turns += 1 | |
| runtime.last_replica = replica_id | |
| replica.busy = False | |
| replica.current_session = None | |
| replica.busy_until = self.now | |
| is_final = turn_index == len(runtime.spec.turns) - 1 | |
| state_action = "evict" | |
| if is_final: | |
| self._remove_cache(replica, session_id, "complete") | |
| self._remove_host_cache(session_id) | |
| elif self.cfg.retention_policy == "evict": | |
| self._remove_cache(replica, session_id, "complete") | |
| self._remove_host_cache(session_id) | |
| elif self.cfg.retention_policy in {"retain", "ttl"}: | |
| self._put_cache(replica, session_id, projected_tokens, keep=True) | |
| state_action = "retain_hbm" | |
| elif self.cfg.retention_policy == "offload": | |
| if self._offload_cache(replica, session_id, projected_tokens): | |
| state_action = "offload_host" | |
| else: | |
| state_action = "evict_host_full" | |
| elif self.cfg.retention_policy == "gap_aware": | |
| # This is intentionally an oracle upper-bound policy: the simulated | |
| # tool gap is already known from the generated program trace. | |
| if turn.tool_gap_after_s <= self.cfg.gap_aware_threshold_s: | |
| self._put_cache(replica, session_id, projected_tokens, keep=True) | |
| state_action = "retain_hbm_short_gap" | |
| elif self._offload_cache(replica, session_id, projected_tokens): | |
| state_action = "offload_host_long_gap" | |
| else: | |
| state_action = "evict_host_full" | |
| elif self.cfg.retention_policy == "adaptive": | |
| predicted_gap_s, prediction_source, history_count = self.tool_gap_predictor.predict(turn.tool_kind) | |
| predicted_retain = predicted_gap_s <= self.cfg.gap_aware_threshold_s | |
| oracle_retain = turn.tool_gap_after_s <= self.cfg.gap_aware_threshold_s | |
| if predicted_retain: | |
| self._put_cache(replica, session_id, projected_tokens, keep=True) | |
| state_action = "adaptive_retain_hbm" | |
| elif self._offload_cache(replica, session_id, projected_tokens): | |
| state_action = "adaptive_offload_host" | |
| else: | |
| state_action = "adaptive_evict_host_full" | |
| self.prediction_rows.append({ | |
| "session_id": session_id, | |
| "turn_index": turn_index + 1, | |
| "tool_kind": turn.tool_kind, | |
| "shifted_regime": turn.shifted_regime, | |
| "predicted_gap_s": predicted_gap_s, | |
| "actual_gap_s": turn.tool_gap_after_s, | |
| "absolute_error_s": abs(predicted_gap_s - turn.tool_gap_after_s), | |
| "prediction_source": prediction_source, | |
| "history_count": history_count, | |
| "predicted_action": "retain" if predicted_retain else "offload", | |
| "oracle_action": "retain" if oracle_retain else "offload", | |
| "action_match": predicted_retain == oracle_retain, | |
| }) | |
| e2e_ms = (self.now - ready_time) * 1000.0 | |
| self.turn_rows.append( | |
| { | |
| "session_id": session_id, | |
| "turn_index": turn_index + 1, | |
| "replica": replica_id, | |
| "ready_time": ready_time, | |
| "completion_time": self.now, | |
| "cache_hit": cache_hit, | |
| "cache_source": cache_source, | |
| "restore_ms": restore_s * 1000.0, | |
| "state_action": state_action, | |
| "tool_kind": turn.tool_kind, | |
| "tool_gap_after_s": turn.tool_gap_after_s, | |
| "shifted_regime": turn.shifted_regime, | |
| "prefill_tokens": prefill_tokens, | |
| "context_tokens_after": projected_tokens, | |
| "output_tokens": turn.output_tokens, | |
| "ttft_ms": ttft_s * 1000.0, | |
| "e2e_ms": e2e_ms, | |
| "queue_ms": max(0.0, e2e_ms - service_s * 1000.0), | |
| } | |
| ) | |
| if is_final: | |
| runtime.completion_time = self.now | |
| else: | |
| self.tool_gap_total_s += turn.tool_gap_after_s | |
| observed_at = self.now + turn.tool_gap_after_s | |
| self._push(observed_at, "tool_observed", turn.tool_kind, turn.tool_gap_after_s) | |
| self._push(observed_at, "turn_ready", session_id, turn_index + 1) | |
| self._start_next(replica) | |
| def run(self) -> dict[str, Any]: | |
| self._record_timeline() | |
| while self.events: | |
| event_time, _, kind, payload = heapq.heappop(self.events) | |
| self._integrate_memory(event_time) | |
| self.now = event_time | |
| if kind == "tool_observed": | |
| self.tool_gap_predictor.observe(str(payload[0]), float(payload[1])) | |
| elif kind == "turn_ready": | |
| session_id, turn_index = int(payload[0]), int(payload[1]) | |
| turn = self.sessions[session_id].spec.turns[turn_index] | |
| replica = self._route(session_id) | |
| replica.queue.append((session_id, turn, self.now)) | |
| self._start_next(replica) | |
| elif kind == "turn_complete": | |
| self._finish_turn( | |
| int(payload[0]), int(payload[1]), int(payload[2]), float(payload[3]), bool(payload[4]), | |
| str(payload[5]), float(payload[6]), int(payload[7]), float(payload[8]), float(payload[9]), int(payload[10]), | |
| ) | |
| elif kind == "cache_expire": | |
| replica_id, session_id, generation = map(int, payload) | |
| replica = self.replicas[replica_id] | |
| entry = replica.cache.get(session_id) | |
| if entry is not None and entry.generation == generation and entry.expiry_time is not None and entry.expiry_time <= self.now + 1e-12: | |
| self._remove_cache(replica, session_id, "ttl") | |
| elif kind == "turn_failed": | |
| session_id, turn_index, replica_id, ready_time = int(payload[0]), int(payload[1]), int(payload[2]), float(payload[3]) | |
| self.turn_rows.append({ | |
| "session_id": session_id, "turn_index": turn_index + 1, "replica": replica_id, | |
| "ready_time": ready_time, "completion_time": self.now, "cache_hit": False, | |
| "prefill_tokens": 0, "context_tokens_after": self.sessions[session_id].context_tokens, | |
| "output_tokens": 0, "ttft_ms": 0.0, "e2e_ms": 0.0, "queue_ms": 0.0, "failed": True, | |
| }) | |
| self._record_timeline() | |
| self._integrate_memory(self.now) | |
| self._record_timeline() | |
| completed_sessions = [s for s in self.sessions.values() if s.completion_time is not None] | |
| successful_turns = [row for row in self.turn_rows if not row.get("failed")] | |
| ttfts = [float(row["ttft_ms"]) for row in successful_turns] | |
| turn_e2e = [float(row["e2e_ms"]) for row in successful_turns] | |
| session_e2e = [ | |
| (s.completion_time - s.spec.arrival_time) * 1000.0 | |
| for s in completed_sessions | |
| if s.completion_time is not None | |
| ] | |
| session_slo = sum(1 for v in session_e2e if v <= self.cfg.slo_session_e2e_ms) | |
| turn_slo = sum(1 for v in ttfts if v <= self.cfg.slo_turn_ttft_ms) | |
| turns_generated = sum(len(s.spec.turns) for s in self.sessions.values()) | |
| completion_horizon = max([s.completion_time or 0.0 for s in self.sessions.values()] + [self.cfg.duration_s, 1e-9]) | |
| mean_kv_gb = self.hbm_gb_seconds / completion_horizon | |
| summary = { | |
| "sessions_generated": len(self.sessions), | |
| "sessions_completed": len(completed_sessions), | |
| "turns_generated": turns_generated, | |
| "turns_completed": len(successful_turns), | |
| "turns_failed": self.failed_turns, | |
| "session_completion_rate": len(completed_sessions) / len(self.sessions) if self.sessions else 0.0, | |
| "turn_completion_rate": len(successful_turns) / turns_generated if turns_generated else 0.0, | |
| "session_throughput_rps": len(completed_sessions) / completion_horizon, | |
| "turn_throughput_rps": len(successful_turns) / completion_horizon, | |
| "turn_ttft_slo_attainment": turn_slo / turns_generated if turns_generated else 0.0, | |
| "session_slo_attainment": session_slo / len(self.sessions) if self.sessions else 0.0, | |
| "simulated_makespan_s": completion_horizon, | |
| } | |
| latency = { | |
| "turn_ttft_ms": {"p50": percentile(ttfts, 0.50), "p95": percentile(ttfts, 0.95), "p99": percentile(ttfts, 0.99)}, | |
| "turn_e2e_ms": {"p50": percentile(turn_e2e, 0.50), "p95": percentile(turn_e2e, 0.95), "p99": percentile(turn_e2e, 0.99)}, | |
| "session_e2e_ms": {"p50": percentile(session_e2e, 0.50), "p95": percentile(session_e2e, 0.95), "p99": percentile(session_e2e, 0.99)}, | |
| } | |
| host_restore_p95_ms = percentile(self.host_transfer_latencies_ms, 0.95) | |
| total_reuse_hits = self.cache_hits + self.host_cache_hits | |
| prediction_errors = [float(row["absolute_error_s"]) for row in self.prediction_rows] | |
| prediction_matches = [bool(row["action_match"]) for row in self.prediction_rows] | |
| resource = { | |
| "replicas": self.cfg.replicas, | |
| "kv_capacity_gb_per_replica": self.kv_capacity_gb, | |
| "peak_kv_gb": self.peak_kv_gb, | |
| "peak_replica_kv_gb": self.peak_replica_kv_gb, | |
| "mean_kv_gb": mean_kv_gb, | |
| "hbm_gb_seconds": self.hbm_gb_seconds, | |
| "peak_host_kv_gb": self.peak_host_gb, | |
| "mean_host_kv_gb": self.host_gb_seconds / completion_horizon, | |
| "host_gb_seconds": self.host_gb_seconds, | |
| "cross_turn_cache_hits": total_reuse_hits, | |
| "hbm_cache_hits": self.cache_hits, | |
| "host_cache_hits": self.host_cache_hits, | |
| "cross_turn_cache_eligible": self.cache_eligible_turns, | |
| "cross_turn_cache_hit_rate": total_reuse_hits / self.cache_eligible_turns if self.cache_eligible_turns else 0.0, | |
| "hbm_cache_hit_rate": self.cache_hits / self.cache_eligible_turns if self.cache_eligible_turns else 0.0, | |
| "host_cache_hit_rate": self.host_cache_hits / self.cache_eligible_turns if self.cache_eligible_turns else 0.0, | |
| "routing_locality_rate": self.affinity_routes / self.route_opportunities if self.route_opportunities else 0.0, | |
| "recomputed_history_tokens": self.recompute_tokens, | |
| "pressure_evictions": self.pressure_evictions, | |
| "ttl_evictions": self.ttl_evictions, | |
| "host_pressure_evictions": self.host_pressure_evictions, | |
| "offloaded_gb": self.offload_bytes / 1e9, | |
| "restored_gb": self.restore_bytes / 1e9, | |
| "p95_host_transfer_ms": host_restore_p95_ms, | |
| "tool_gap_total_s": self.tool_gap_total_s, | |
| "adaptive_prediction_count": len(self.prediction_rows), | |
| "adaptive_prediction_mae_s": mean(prediction_errors) if prediction_errors else 0.0, | |
| "adaptive_prediction_p95_abs_error_s": percentile(prediction_errors, 0.95), | |
| "adaptive_oracle_action_agreement": ( | |
| sum(1 for match in prediction_matches if match) / len(prediction_matches) | |
| if prediction_matches else 0.0 | |
| ), | |
| } | |
| return { | |
| "config": self.cfg.to_dict(), | |
| "provenance": { | |
| "simulator": "InferScale-Sim", | |
| "mode": "stateful-agent-session-simulation", | |
| "latency_profile_type": "analytical-reference", | |
| "agent_service_model": "serial-per-replica-reference", | |
| "host_tier_model": "serialized-reference-transfer", | |
| "gap_aware_policy": "oracle-upper-bound" if self.cfg.retention_policy == "gap_aware" else "not-active", | |
| "adaptive_policy": ( | |
| "online-tool-gap-ewma-no-lookahead" if self.cfg.retention_policy == "adaptive" else "not-active" | |
| ), | |
| "warning": "Agent-session mode isolates routing/KV-retention effects and does not model dynamic batching within each replica.", | |
| }, | |
| "summary": summary, | |
| "latency": latency, | |
| "resource": resource, | |
| "turns": successful_turns[:3000], | |
| "prediction": { | |
| "rows": self.prediction_rows[:3000], | |
| "predictor": self.tool_gap_predictor.snapshot(), | |
| }, | |
| "sessions": [ | |
| { | |
| "session_id": s.spec.session_id, | |
| "arrival_time": s.spec.arrival_time, | |
| "turns": len(s.spec.turns), | |
| "completion_time": s.completion_time, | |
| "e2e_ms": (s.completion_time - s.spec.arrival_time) * 1000.0 if s.completion_time is not None else None, | |
| } | |
| for s in list(self.sessions.values())[:1000] | |
| ], | |
| "timeline": self.timeline, | |
| } | |
| def run_agent_session_simulation(config: dict[str, Any], sessions: list[SessionSpec] | None = None) -> dict[str, Any]: | |
| cfg = AgentSessionConfig.from_dict(config) | |
| return AgentSessionSimulator(cfg, sessions=sessions).run() | |
| def _same_trace(cfg: AgentSessionConfig) -> list[SessionSpec]: | |
| return generate_agent_sessions(cfg) | |
| def compare_agent_policies(config: dict[str, Any]) -> dict[str, Any]: | |
| base = AgentSessionConfig.from_dict(config) | |
| trace = _same_trace(base) | |
| policies = [ | |
| ("Stateless / least-load", "evict", "least_load", 0.0), | |
| ("Retain / least-load", "retain", "least_load", base.kv_ttl_s), | |
| ("TTL / affinity", "ttl", "session_affinity", base.kv_ttl_s), | |
| ("Retain / affinity", "retain", "session_affinity", base.kv_ttl_s), | |
| ] | |
| rows = [] | |
| results = [] | |
| for label, retention, routing, ttl in policies: | |
| cfg = AgentSessionConfig.from_dict(base.to_dict()) | |
| cfg.retention_policy = retention | |
| cfg.routing_policy = routing | |
| cfg.kv_ttl_s = ttl | |
| result = run_agent_session_simulation(cfg.to_dict(), trace) | |
| results.append(result) | |
| rows.append( | |
| { | |
| "label": label, | |
| "retention": retention, | |
| "routing": routing, | |
| "p95_turn_ttft_ms": result["latency"]["turn_ttft_ms"]["p95"], | |
| "p95_session_e2e_ms": result["latency"]["session_e2e_ms"]["p95"], | |
| "cache_hit_rate": result["resource"]["cross_turn_cache_hit_rate"], | |
| "routing_locality_rate": result["resource"]["routing_locality_rate"], | |
| "recomputed_history_tokens": result["resource"]["recomputed_history_tokens"], | |
| "peak_kv_gb": result["resource"]["peak_kv_gb"], | |
| "mean_kv_gb": result["resource"]["mean_kv_gb"], | |
| "hbm_gb_seconds": result["resource"]["hbm_gb_seconds"], | |
| "pressure_evictions": result["resource"]["pressure_evictions"], | |
| "ttl_evictions": result["resource"]["ttl_evictions"], | |
| "sessions_completed": result["summary"]["sessions_completed"], | |
| } | |
| ) | |
| return {"protocol": "common-agent-program-trace", "candidate_count": len(rows), "rows": rows, "results": results} | |
| def _pareto(rows: list[dict[str, Any]], x: str, y: str) -> set[int]: | |
| # Both x and y are minimized. | |
| front: set[int] = set() | |
| for idx, row in enumerate(rows): | |
| dominated = False | |
| for jdx, other in enumerate(rows): | |
| if idx == jdx: | |
| continue | |
| if other[x] <= row[x] and other[y] <= row[y] and (other[x] < row[x] or other[y] < row[y]): | |
| dominated = True | |
| break | |
| if not dominated: | |
| front.add(idx) | |
| return front | |
| def ttl_retention_sweep(config: dict[str, Any], ttl_values: list[float] | None = None) -> dict[str, Any]: | |
| base = AgentSessionConfig.from_dict(config) | |
| base.retention_policy = "ttl" | |
| base.routing_policy = "session_affinity" | |
| trace = _same_trace(base) | |
| values = ttl_values or [0.0, 0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0] | |
| cleaned = sorted({max(0.0, min(float(v), 120.0)) for v in values}) | |
| rows = [] | |
| for ttl in cleaned: | |
| cfg = AgentSessionConfig.from_dict(base.to_dict()) | |
| cfg.kv_ttl_s = ttl | |
| result = run_agent_session_simulation(cfg.to_dict(), trace) | |
| rows.append( | |
| { | |
| "ttl_s": ttl, | |
| "p95_turn_ttft_ms": result["latency"]["turn_ttft_ms"]["p95"], | |
| "p95_session_e2e_ms": result["latency"]["session_e2e_ms"]["p95"], | |
| "cache_hit_rate": result["resource"]["cross_turn_cache_hit_rate"], | |
| "recomputed_history_tokens": result["resource"]["recomputed_history_tokens"], | |
| "mean_kv_gb": result["resource"]["mean_kv_gb"], | |
| "hbm_gb_seconds": result["resource"]["hbm_gb_seconds"], | |
| "pressure_evictions": result["resource"]["pressure_evictions"], | |
| "ttl_evictions": result["resource"]["ttl_evictions"], | |
| } | |
| ) | |
| front = _pareto(rows, "p95_turn_ttft_ms", "mean_kv_gb") | |
| # Collapse numerically equivalent frontier points to the shortest TTL. A | |
| # longer retention horizon with indistinguishable latency/residency is not a | |
| # distinct engineering trade-off. | |
| unique_front: set[int] = set() | |
| seen: set[tuple[float, float]] = set() | |
| for idx in sorted(front, key=lambda i: rows[i]["ttl_s"]): | |
| key = (round(rows[idx]["p95_turn_ttft_ms"], 6), round(rows[idx]["mean_kv_gb"], 6)) | |
| if key not in seen: | |
| unique_front.add(idx) | |
| seen.add(key) | |
| for idx, row in enumerate(rows): | |
| row["pareto"] = idx in unique_front | |
| return { | |
| "protocol": "common-agent-program-trace", | |
| "objective": "minimize-p95-turn-ttft-and-mean-kv-residency", | |
| "rows": rows, | |
| "pareto_count": len(unique_front), | |
| } | |
| def compare_agent_memory_policies(config: dict[str, Any]) -> dict[str, Any]: | |
| """Compare cache-residency and routing strategies on one common agent trace.""" | |
| base = AgentSessionConfig.from_dict(config) | |
| trace = _same_trace(base) | |
| policies = [ | |
| ("Stateless / least-load", "evict", "least_load"), | |
| ("TTL / strict affinity", "ttl", "session_affinity"), | |
| ("TTL / bounded affinity", "ttl", "bounded_affinity"), | |
| ("Host offload / bounded affinity", "offload", "bounded_affinity"), | |
| ("Gap-aware tiering / bounded affinity", "gap_aware", "bounded_affinity"), | |
| ] | |
| rows: list[dict[str, Any]] = [] | |
| results: list[dict[str, Any]] = [] | |
| for label, retention, routing in policies: | |
| cfg = AgentSessionConfig.from_dict(base.to_dict()) | |
| cfg.retention_policy = retention | |
| cfg.routing_policy = routing | |
| result = run_agent_session_simulation(cfg.to_dict(), trace) | |
| results.append(result) | |
| rows.append( | |
| { | |
| "label": label, | |
| "retention": retention, | |
| "routing": routing, | |
| "p95_turn_ttft_ms": result["latency"]["turn_ttft_ms"]["p95"], | |
| "p95_session_e2e_ms": result["latency"]["session_e2e_ms"]["p95"], | |
| "turn_slo_attainment": result["summary"]["turn_ttft_slo_attainment"], | |
| "session_slo_attainment": result["summary"]["session_slo_attainment"], | |
| "cache_hit_rate": result["resource"]["cross_turn_cache_hit_rate"], | |
| "hbm_hit_rate": result["resource"]["hbm_cache_hit_rate"], | |
| "host_hit_rate": result["resource"]["host_cache_hit_rate"], | |
| "routing_locality_rate": result["resource"]["routing_locality_rate"], | |
| "recomputed_history_tokens": result["resource"]["recomputed_history_tokens"], | |
| "mean_hbm_gb": result["resource"]["mean_kv_gb"], | |
| "mean_host_gb": result["resource"]["mean_host_kv_gb"], | |
| "hbm_gb_seconds": result["resource"]["hbm_gb_seconds"], | |
| "host_gb_seconds": result["resource"]["host_gb_seconds"], | |
| "p95_host_transfer_ms": result["resource"]["p95_host_transfer_ms"], | |
| "offloaded_gb": result["resource"]["offloaded_gb"], | |
| "pressure_evictions": result["resource"]["pressure_evictions"], | |
| "host_pressure_evictions": result["resource"]["host_pressure_evictions"], | |
| "turns_failed": result["summary"]["turns_failed"], | |
| } | |
| ) | |
| return { | |
| "protocol": "common-agent-program-trace", | |
| "study": "agent-memory-tiering", | |
| "candidate_count": len(rows), | |
| "rows": rows, | |
| "results": results, | |
| "note": "Gap-aware tiering uses realized simulated tool gaps and is an oracle upper bound, not a deployable predictor.", | |
| } | |
| def agent_memory_budget_sweep( | |
| config: dict[str, Any], budget_multipliers: list[float] | None = None | |
| ) -> dict[str, Any]: | |
| """Stress state policies under finite per-replica HBM KV budgets. | |
| Budgets are derived from the unconstrained peak working set of the exact same | |
| program trace, avoiding arbitrary fractions of total GPU VRAM that would be | |
| too loose for small models. | |
| """ | |
| base = AgentSessionConfig.from_dict(config) | |
| trace = _same_trace(base) | |
| reference_cfg = AgentSessionConfig.from_dict(base.to_dict()) | |
| reference_cfg.retention_policy = "retain" | |
| reference_cfg.routing_policy = "session_affinity" | |
| reference_cfg.kv_capacity_override_gb = 0.0 | |
| reference = run_agent_session_simulation(reference_cfg.to_dict(), trace) | |
| reference_peak = max(float(reference["resource"]["peak_replica_kv_gb"]), 0.002) | |
| multipliers = budget_multipliers or [0.35, 0.50, 0.75, 1.00, 1.50] | |
| cleaned = sorted({max(0.10, min(float(v), 3.0)) for v in multipliers}) | |
| policies = [ | |
| ("TTL / bounded affinity", "ttl", "bounded_affinity"), | |
| ("Host offload / bounded affinity", "offload", "bounded_affinity"), | |
| ("Gap-aware tiering / bounded affinity", "gap_aware", "bounded_affinity"), | |
| ] | |
| rows: list[dict[str, Any]] = [] | |
| for multiplier in cleaned: | |
| budget = reference_peak * multiplier | |
| for label, retention, routing in policies: | |
| cfg = AgentSessionConfig.from_dict(base.to_dict()) | |
| cfg.retention_policy = retention | |
| cfg.routing_policy = routing | |
| cfg.kv_capacity_override_gb = budget | |
| result = run_agent_session_simulation(cfg.to_dict(), trace) | |
| rows.append( | |
| { | |
| "policy": label, | |
| "budget_multiplier": multiplier, | |
| "budget_gb_per_replica": budget, | |
| "p95_turn_ttft_ms": result["latency"]["turn_ttft_ms"]["p95"], | |
| "p95_session_e2e_ms": result["latency"]["session_e2e_ms"]["p95"], | |
| "turn_slo_attainment": result["summary"]["turn_ttft_slo_attainment"], | |
| "session_slo_attainment": result["summary"]["session_slo_attainment"], | |
| "cache_hit_rate": result["resource"]["cross_turn_cache_hit_rate"], | |
| "recomputed_history_tokens": result["resource"]["recomputed_history_tokens"], | |
| "mean_hbm_gb": result["resource"]["mean_kv_gb"], | |
| "mean_host_gb": result["resource"]["mean_host_kv_gb"], | |
| "p95_host_transfer_ms": result["resource"]["p95_host_transfer_ms"], | |
| "pressure_evictions": result["resource"]["pressure_evictions"], | |
| "host_pressure_evictions": result["resource"]["host_pressure_evictions"], | |
| "turns_failed": result["summary"]["turns_failed"], | |
| } | |
| ) | |
| return { | |
| "protocol": "common-agent-program-trace", | |
| "study": "finite-hbm-budget-stress", | |
| "reference_peak_replica_kv_gb": reference_peak, | |
| "rows": rows, | |
| "policy_count": len(policies), | |
| "budget_count": len(cleaned), | |
| } | |
| def agent_affinity_sweep(config: dict[str, Any], slack_values_ms: list[float] | None = None) -> dict[str, Any]: | |
| """Sweep how much queue imbalance the router tolerates for KV locality.""" | |
| base = AgentSessionConfig.from_dict(config) | |
| base.retention_policy = "ttl" | |
| base.routing_policy = "bounded_affinity" | |
| trace = _same_trace(base) | |
| values = slack_values_ms or [0.0, 25.0, 75.0, 150.0, 300.0, 600.0, 1200.0] | |
| cleaned = sorted({max(0.0, min(float(v), 5000.0)) for v in values}) | |
| rows: list[dict[str, Any]] = [] | |
| for slack in cleaned: | |
| cfg = AgentSessionConfig.from_dict(base.to_dict()) | |
| cfg.affinity_slack_ms = slack | |
| result = run_agent_session_simulation(cfg.to_dict(), trace) | |
| rows.append( | |
| { | |
| "affinity_slack_ms": slack, | |
| "p95_turn_ttft_ms": result["latency"]["turn_ttft_ms"]["p95"], | |
| "p95_session_e2e_ms": result["latency"]["session_e2e_ms"]["p95"], | |
| "cache_hit_rate": result["resource"]["cross_turn_cache_hit_rate"], | |
| "routing_locality_rate": result["resource"]["routing_locality_rate"], | |
| "recomputed_history_tokens": result["resource"]["recomputed_history_tokens"], | |
| "turn_slo_attainment": result["summary"]["turn_ttft_slo_attainment"], | |
| "session_slo_attainment": result["summary"]["session_slo_attainment"], | |
| "pressure_evictions": result["resource"]["pressure_evictions"], | |
| } | |
| ) | |
| return { | |
| "protocol": "common-agent-program-trace", | |
| "study": "bounded-affinity-routing-frontier", | |
| "rows": rows, | |
| } | |
| def _prediction_phase_metrics(result: dict[str, Any]) -> dict[str, float]: | |
| rows = result.get("prediction", {}).get("rows", []) | |
| before = [row for row in rows if not row.get("shifted_regime")] | |
| after = [row for row in rows if row.get("shifted_regime")] | |
| def phase(items: list[dict[str, Any]]) -> tuple[float, float]: | |
| if not items: | |
| return 0.0, 0.0 | |
| mae = mean(float(row["absolute_error_s"]) for row in items) | |
| agreement = sum(1 for row in items if row.get("action_match")) / len(items) | |
| return mae, agreement | |
| pre_mae, pre_agreement = phase(before) | |
| post_mae, post_agreement = phase(after) | |
| return { | |
| "pre_shift_mae_s": pre_mae, | |
| "post_shift_mae_s": post_mae, | |
| "pre_shift_action_agreement": pre_agreement, | |
| "post_shift_action_agreement": post_agreement, | |
| } | |
| def _rolling_prediction_curve(result: dict[str, Any], window: int = 16) -> list[dict[str, Any]]: | |
| rows = result.get("prediction", {}).get("rows", []) | |
| if not rows: | |
| return [] | |
| points: list[dict[str, Any]] = [] | |
| width = max(4, min(int(window), 64)) | |
| for idx in range(len(rows)): | |
| start = max(0, idx - width + 1) | |
| chunk = rows[start : idx + 1] | |
| points.append( | |
| { | |
| "observation": idx + 1, | |
| "rolling_mae_s": mean(float(row["absolute_error_s"]) for row in chunk), | |
| "rolling_action_agreement": sum(1 for row in chunk if row.get("action_match")) / len(chunk), | |
| "tool_kind": rows[idx].get("tool_kind", "generic"), | |
| "shifted_regime": bool(rows[idx].get("shifted_regime")), | |
| } | |
| ) | |
| return points | |
| def adaptive_tiering_study( | |
| config: dict[str, Any], | |
| *, | |
| horizon_s: float = 120.0, | |
| shift_fraction: float = 0.55, | |
| shift_multiplier: float = 2.5, | |
| alpha: float = 0.30, | |
| ) -> dict[str, Any]: | |
| """Compare fixed, adaptive, and oracle KV tiering on one non-stationary trace. | |
| The adaptive candidates never inspect the realized future tool duration at | |
| decision time. They learn an EWMA online from completed tool calls. The | |
| oracle candidate intentionally sees the realized gap and serves only as an | |
| upper bound. | |
| """ | |
| base = AgentSessionConfig.from_dict(config) | |
| base.duration_s = max(float(horizon_s), 30.0) | |
| base.tool_regime_shift_fraction = min(max(float(shift_fraction), 0.05), 0.95) | |
| base.tool_regime_shift_multiplier = max(float(shift_multiplier), 1.0) | |
| base.adaptive_alpha = min(max(float(alpha), 0.01), 1.0) | |
| base.routing_policy = "bounded_affinity" | |
| trace = generate_agent_sessions(base) | |
| candidates = [ | |
| ("Fixed TTL", "ttl", "per_tool_ema"), | |
| ("Always host offload", "offload", "per_tool_ema"), | |
| ("Adaptive global EWMA", "adaptive", "global_ema"), | |
| ("Adaptive per-tool EWMA", "adaptive", "per_tool_ema"), | |
| ("Oracle gap-aware", "gap_aware", "per_tool_ema"), | |
| ] | |
| rows: list[dict[str, Any]] = [] | |
| results: dict[str, dict[str, Any]] = {} | |
| for label, retention, scope in candidates: | |
| cfg = AgentSessionConfig.from_dict(base.to_dict()) | |
| cfg.retention_policy = retention | |
| cfg.adaptive_predictor_scope = scope | |
| result = run_agent_session_simulation(cfg.to_dict(), trace) | |
| results[label] = result | |
| phase = _prediction_phase_metrics(result) | |
| rows.append( | |
| { | |
| "label": label, | |
| "retention": retention, | |
| "predictor_scope": scope if retention == "adaptive" else "not-active", | |
| "p95_turn_ttft_ms": result["latency"]["turn_ttft_ms"]["p95"], | |
| "p95_session_e2e_ms": result["latency"]["session_e2e_ms"]["p95"], | |
| "session_slo_attainment": result["summary"]["session_slo_attainment"], | |
| "cache_hit_rate": result["resource"]["cross_turn_cache_hit_rate"], | |
| "mean_hbm_gb": result["resource"]["mean_kv_gb"], | |
| "mean_host_gb": result["resource"]["mean_host_kv_gb"], | |
| "recomputed_history_tokens": result["resource"]["recomputed_history_tokens"], | |
| "prediction_count": result["resource"].get("adaptive_prediction_count", 0), | |
| "prediction_mae_s": result["resource"].get("adaptive_prediction_mae_s", 0.0), | |
| "oracle_action_agreement": result["resource"].get("adaptive_oracle_action_agreement", 0.0), | |
| **phase, | |
| } | |
| ) | |
| global_result = results["Adaptive global EWMA"] | |
| tool_result = results["Adaptive per-tool EWMA"] | |
| shift_observation = next( | |
| ( | |
| idx + 1 | |
| for idx, row in enumerate(tool_result.get("prediction", {}).get("rows", [])) | |
| if row.get("shifted_regime") | |
| ), | |
| 0, | |
| ) | |
| return { | |
| "protocol": "common-nonstationary-agent-program-trace", | |
| "study": "online-predictive-kv-tiering", | |
| "horizon_s": base.duration_s, | |
| "shift_fraction": base.tool_regime_shift_fraction, | |
| "shift_multiplier": base.tool_regime_shift_multiplier, | |
| "shift_time_s": base.duration_s * base.tool_regime_shift_fraction, | |
| "shift_observation": shift_observation, | |
| "threshold_s": base.gap_aware_threshold_s, | |
| "alpha": base.adaptive_alpha, | |
| "rows": rows, | |
| "learning_curves": { | |
| "global": _rolling_prediction_curve(global_result), | |
| "per_tool": _rolling_prediction_curve(tool_result), | |
| }, | |
| "tool_profiles": TOOL_GAP_MULTIPLIERS, | |
| "note": ( | |
| "The oracle gap-aware candidate sees realized future tool gaps and is an upper bound. " | |
| "Adaptive candidates learn only from tool calls that have already completed." | |
| ), | |
| } | |
| def adaptive_alpha_sweep( | |
| config: dict[str, Any], | |
| alpha_values: list[float] | None = None, | |
| *, | |
| horizon_s: float = 120.0, | |
| shift_fraction: float = 0.55, | |
| shift_multiplier: float = 2.5, | |
| ) -> dict[str, Any]: | |
| """Sweep EWMA adaptation speed on one shifted trace. | |
| Lower alpha values are stable but adapt slowly; high values react quickly but | |
| are noisier. The study reports both pre- and post-shift prediction error and | |
| the resulting serving metrics. | |
| """ | |
| base = AgentSessionConfig.from_dict(config) | |
| base.duration_s = max(float(horizon_s), 30.0) | |
| base.tool_regime_shift_fraction = min(max(float(shift_fraction), 0.05), 0.95) | |
| base.tool_regime_shift_multiplier = max(float(shift_multiplier), 1.0) | |
| base.retention_policy = "adaptive" | |
| base.routing_policy = "bounded_affinity" | |
| base.adaptive_predictor_scope = "per_tool_ema" | |
| trace = generate_agent_sessions(base) | |
| values = alpha_values or [0.05, 0.10, 0.20, 0.30, 0.50, 0.75, 1.00] | |
| cleaned = sorted({min(max(float(value), 0.01), 1.0) for value in values}) | |
| rows: list[dict[str, Any]] = [] | |
| for alpha in cleaned: | |
| cfg = AgentSessionConfig.from_dict(base.to_dict()) | |
| cfg.adaptive_alpha = alpha | |
| result = run_agent_session_simulation(cfg.to_dict(), trace) | |
| phase = _prediction_phase_metrics(result) | |
| rows.append( | |
| { | |
| "alpha": alpha, | |
| "prediction_mae_s": result["resource"]["adaptive_prediction_mae_s"], | |
| "oracle_action_agreement": result["resource"]["adaptive_oracle_action_agreement"], | |
| "p95_turn_ttft_ms": result["latency"]["turn_ttft_ms"]["p95"], | |
| "p95_session_e2e_ms": result["latency"]["session_e2e_ms"]["p95"], | |
| "session_slo_attainment": result["summary"]["session_slo_attainment"], | |
| "mean_hbm_gb": result["resource"]["mean_kv_gb"], | |
| "mean_host_gb": result["resource"]["mean_host_kv_gb"], | |
| **phase, | |
| } | |
| ) | |
| best_post = min(rows, key=lambda row: (row["post_shift_mae_s"], row["p95_turn_ttft_ms"])) if rows else None | |
| return { | |
| "protocol": "common-nonstationary-agent-program-trace", | |
| "study": "adaptive-ewma-rate-sweep", | |
| "shift_fraction": base.tool_regime_shift_fraction, | |
| "shift_multiplier": base.tool_regime_shift_multiplier, | |
| "rows": rows, | |
| "best_post_shift_alpha": best_post["alpha"] if best_post else None, | |
| "note": "Alpha controls adaptation speed; this is an online heuristic study, not a learned model benchmark.", | |
| } | |