umar-sharif821 commited on
Commit
ddf831c
·
1 Parent(s): decc30b

feat: reproducible eval + shared policy module; fix smart_agent wasted-capacity bug

Browse files
.github/workflows/ci.yml CHANGED
@@ -48,3 +48,6 @@ jobs:
48
  break
49
  print(f"Smoke OK - 20 steps, reward sum={total:.3f}")
50
  PY
 
 
 
 
48
  break
49
  print(f"Smoke OK - 20 steps, reward sum={total:.3f}")
50
  PY
51
+
52
+ - name: Reproducible evaluator (quick)
53
+ run: python scripts/eval.py --quick
.gitignore CHANGED
@@ -28,6 +28,9 @@ runs/
28
  build/
29
  dist/
30
 
 
 
 
31
  # OS / editor
32
  .DS_Store
33
  Thumbs.db
 
28
  build/
29
  dist/
30
 
31
+ # Evaluator outputs (regenerable via `python scripts/eval.py`)
32
+ eval_results.json
33
+
34
  # OS / editor
35
  .DS_Store
36
  Thumbs.db
README.md CHANGED
@@ -37,15 +37,33 @@ pip install -r requirements.txt && python app.py # Gradio UI on :7860
37
 
38
  ## Results at a Glance
39
 
40
- Median over 5 seeds, `task_hard` (50MB cache, 35% viral files, 200 steps):
41
 
42
- | Policy | Hit Rate | Bandwidth Saved | Score |
43
- |---|---|---|---|
44
- | Random eviction | 0.23 | low | 0.41 |
45
- | LRU baseline | 0.45 | medium | 0.78 |
46
- | **Fine-tuned Agent (ours)** | **0.58** | **high** | **0.92** |
 
 
 
 
 
 
47
 
48
- `training_results.png` (produced by `colab_submission_script.py`) shows the 2×2 comparison chart judges can reference.
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
 
51
  **Hackathon writeup:** [Blog.MD](./Blog.MD)
 
37
 
38
  ## Results at a Glance
39
 
40
+ Mean over 5 seeds (0–4), generated by `python scripts/eval.py --seeds 0 1 2 3 4` from a clean checkout:
41
 
42
+ | Task | Policy | Hit Rate | Reward (mean +/- std) | Bandwidth MB |
43
+ |---|---|---|---|---|
44
+ | task_easy | random | 35.2% | 35.70 +/- 9.70 | 381.9 |
45
+ | task_easy | lru_baseline | 31.4% | 31.43 +/- 4.95 | 319.5 |
46
+ | **task_easy** | **smart_agent** | **41.2%** | **41.97 +/- 6.67** | **397.4** |
47
+ | task_medium | random | 18.5% | 27.75 +/- 7.30 | 270.8 |
48
+ | task_medium | lru_baseline | 18.7% | 29.59 +/- 8.78 | 253.2 |
49
+ | **task_medium** | **smart_agent** | **24.5%** | **43.34 +/- 11.70** | **274.0** |
50
+ | task_hard | random | 10.3% | 14.86 +/- 6.60 | 168.2 |
51
+ | task_hard | lru_baseline | 11.3% | 21.03 +/- 6.87 | 188.8 |
52
+ | **task_hard** | **smart_agent** | **12.2%** | **26.93 +/- 8.39** | **199.0** |
53
 
54
+ `smart_agent` beats the LRU baseline on hit rate, total reward, and bandwidth saved across all three tasks. The same code path is used by the Hugging Face Space UI, so what you measure locally is what the live demo shows. `training_results.png` (produced by `colab_submission_script.py`) shows the 2x2 RL training/comparison chart judges can reference.
55
+
56
+ ## Reproducible Evaluation
57
+
58
+ Judges can regenerate the numbers above in ~30 seconds with no GPU and no external services:
59
+
60
+ ```bash
61
+ pip install -r requirements.txt
62
+ python scripts/eval.py # full 3-task x 3-seed sweep
63
+ python scripts/eval.py --quick # 1 seed per task (fast smoke)
64
+ ```
65
+
66
+ The script runs three policies (`random`, `lru_baseline`, `smart_agent`) against all three OpenEnv tasks, prints a markdown table to stdout, and writes `eval_results.json` with per-episode rewards, hit rates, and bandwidth saved. The same `smart_agent` code path powers the Hugging Face Space, so what you see in the UI and what you get from the evaluator are the same policy.
67
 
68
 
69
  **Hackathon writeup:** [Blog.MD](./Blog.MD)
agents/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Reusable cache-eviction policies for the CDN Cache Optimizer."""
2
+
3
+ from .policies import lru_baseline, random_baseline, smart_agent
4
+
5
+ __all__ = ["lru_baseline", "random_baseline", "smart_agent"]
agents/policies.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cache-eviction policies used by both the HF Space UI and the evaluator.
2
+
3
+ Each policy has signature ``policy(obs: Observation) -> Action`` so it plugs
4
+ directly into ``CDNCacheEnv.step(...)``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import random
10
+ from typing import Tuple
11
+
12
+ from env.models import Action, Observation
13
+
14
+
15
+ def lru_baseline(obs: Observation) -> Action:
16
+ """Evict the least recently used file when a miss forces eviction."""
17
+ if obs.cache_hit or not obs.cached_files:
18
+ return Action(evict_file_id=None)
19
+ victim = min(obs.cached_files, key=lambda f: f.last_accessed)
20
+ return Action(evict_file_id=victim.file_id)
21
+
22
+
23
+ def random_baseline(obs: Observation, rng: random.Random | None = None) -> Action:
24
+ """Evict a uniformly random cached file. Sanity-check lower bound."""
25
+ if obs.cache_hit or not obs.cached_files:
26
+ return Action(evict_file_id=None)
27
+ picker = rng or random
28
+ victim = picker.choice(obs.cached_files)
29
+ return Action(evict_file_id=victim.file_id)
30
+
31
+
32
+ def smart_agent(obs: Observation) -> Action:
33
+ """Distilled RL policy with CDN guardrails.
34
+
35
+ On every cache miss the agent proposes a victim ranked by:
36
+ 1. Not in the short prefetch preview (queue look-ahead).
37
+ 2. Not currently viral.
38
+ 3. Low request frequency.
39
+ 4. Large size (free more room per eviction).
40
+
41
+ The env only consumes the ``evict_file_id`` when the incoming file cannot
42
+ fit, so nominating a victim on every miss is strictly >= returning ``None``
43
+ (which would incur a wasted-capacity penalty and skip admission).
44
+ """
45
+ if obs.cache_hit or not obs.cached_files:
46
+ return Action(evict_file_id=None)
47
+
48
+ preview = set(obs.queue_preview)
49
+
50
+ def score(file_entry) -> Tuple[int, int, float, float]:
51
+ preview_keep = 1 if file_entry.file_id in preview else 0
52
+ viral_keep = 1 if file_entry.is_viral else 0
53
+ return (
54
+ preview_keep,
55
+ viral_keep,
56
+ file_entry.request_frequency,
57
+ -file_entry.size_mb,
58
+ )
59
+
60
+ victim = min(obs.cached_files, key=score)
61
+ return Action(evict_file_id=victim.file_id)
app.py CHANGED
@@ -9,6 +9,7 @@ import gradio as gr
9
  import matplotlib.pyplot as plt
10
  import numpy as np
11
 
 
12
  from env.cache import CDNCacheEnv, TASK_CONFIGS
13
  from env.models import Action, Observation
14
 
@@ -22,35 +23,6 @@ class EpisodeMetrics:
22
  bandwidth_saved_mb: float
23
 
24
 
25
- def lru_baseline(obs: Observation) -> Action:
26
- if obs.cache_hit or not obs.cached_files:
27
- return Action(evict_file_id=None)
28
- victim = min(obs.cached_files, key=lambda f: f.last_accessed)
29
- return Action(evict_file_id=victim.file_id)
30
-
31
-
32
- def smart_agent(obs: Observation) -> Action:
33
- if obs.cache_hit or not obs.cached_files:
34
- return Action(evict_file_id=None)
35
- if obs.cache_fill_ratio < 0.92:
36
- return Action(evict_file_id=None)
37
-
38
- preview = set(obs.queue_preview)
39
-
40
- def score(file_entry) -> Tuple[int, float, int, float]:
41
- preview_keep = 1 if file_entry.file_id in preview else 0
42
- viral_keep = 1 if file_entry.is_viral else 0
43
- return (
44
- preview_keep,
45
- viral_keep,
46
- file_entry.request_frequency,
47
- -file_entry.size_mb,
48
- )
49
-
50
- victim = min(obs.cached_files, key=score)
51
- return Action(evict_file_id=victim.file_id)
52
-
53
-
54
  def run_episode(task_id: str, seed: int, policy: Callable[[Observation], Action]) -> EpisodeMetrics:
55
  env = CDNCacheEnv(task_id=task_id, seed=seed)
56
  obs = env.reset()
 
9
  import matplotlib.pyplot as plt
10
  import numpy as np
11
 
12
+ from agents.policies import lru_baseline, smart_agent
13
  from env.cache import CDNCacheEnv, TASK_CONFIGS
14
  from env.models import Action, Observation
15
 
 
23
  bandwidth_saved_mb: float
24
 
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def run_episode(task_id: str, seed: int, policy: Callable[[Observation], Action]) -> EpisodeMetrics:
27
  env = CDNCacheEnv(task_id=task_id, seed=seed)
28
  obs = env.reset()
scripts/__init__.py ADDED
File without changes
scripts/eval.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reproducible evaluator for judges.
2
+
3
+ Usage:
4
+ python scripts/eval.py # full 3-task x 3-seed sweep
5
+ python scripts/eval.py --quick # 1 seed per task (fast CI smoke)
6
+ python scripts/eval.py --out out.json
7
+
8
+ Outputs a markdown table to stdout and writes a JSON report.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import random
16
+ import statistics
17
+ import sys
18
+ import time
19
+ from pathlib import Path
20
+ from typing import Callable, Dict, List
21
+
22
+ REPO_ROOT = Path(__file__).resolve().parents[1]
23
+ if str(REPO_ROOT) not in sys.path:
24
+ sys.path.insert(0, str(REPO_ROOT))
25
+
26
+ from agents.policies import lru_baseline, random_baseline, smart_agent # noqa: E402
27
+ from env.cache import CDNCacheEnv, TASK_CONFIGS # noqa: E402
28
+ from env.models import Action, Observation # noqa: E402
29
+
30
+
31
+ POLICIES: Dict[str, Callable[[Observation], Action]] = {
32
+ "random": lambda obs: random_baseline(obs, rng=random.Random(0)),
33
+ "lru_baseline": lru_baseline,
34
+ "smart_agent": smart_agent,
35
+ }
36
+
37
+
38
+ def run_episode(task_id: str, seed: int, policy: Callable[[Observation], Action]) -> Dict:
39
+ env = CDNCacheEnv(task_id=task_id, seed=seed)
40
+ obs = env.reset()
41
+ rewards: List[float] = []
42
+ info: Dict = {}
43
+ done = False
44
+ while not done:
45
+ result = env.step(policy(obs))
46
+ obs = result.observation
47
+ info = result.info
48
+ rewards.append(float(result.reward.total))
49
+ done = result.done
50
+ return {
51
+ "task_id": task_id,
52
+ "seed": seed,
53
+ "total_reward": float(sum(rewards)),
54
+ "final_hit_rate": float(info.get("hit_rate", 0.0)),
55
+ "bandwidth_saved_mb": float(info.get("bandwidth_saved_mb", 0.0)),
56
+ }
57
+
58
+
59
+ def summarize(runs: List[Dict]) -> Dict:
60
+ reward = [r["total_reward"] for r in runs]
61
+ hit = [r["final_hit_rate"] for r in runs]
62
+ bw = [r["bandwidth_saved_mb"] for r in runs]
63
+ return {
64
+ "reward_mean": statistics.mean(reward),
65
+ "reward_std": statistics.pstdev(reward) if len(reward) > 1 else 0.0,
66
+ "hit_rate_mean": statistics.mean(hit),
67
+ "bandwidth_mean": statistics.mean(bw),
68
+ "n": len(runs),
69
+ }
70
+
71
+
72
+ def format_markdown(results: Dict) -> str:
73
+ header = "| Task | Policy | Hit Rate | Reward (mean +/- std) | Bandwidth MB |\n"
74
+ header += "|---|---|---|---|---|\n"
75
+ rows = []
76
+ for task_id in results["tasks"]:
77
+ for policy_name in POLICIES.keys():
78
+ s = results["summary"][task_id][policy_name]
79
+ rows.append(
80
+ f"| {task_id} | {policy_name} | {s['hit_rate_mean']:.1%} | "
81
+ f"{s['reward_mean']:.2f} +/- {s['reward_std']:.2f} | "
82
+ f"{s['bandwidth_mean']:.1f} |"
83
+ )
84
+ return header + "\n".join(rows)
85
+
86
+
87
+ def main() -> int:
88
+ parser = argparse.ArgumentParser(description="CDN Cache Optimizer evaluator")
89
+ parser.add_argument("--quick", action="store_true", help="1 seed per task (fast)")
90
+ parser.add_argument("--seeds", type=int, nargs="+", default=[0, 1, 2], help="seeds to run")
91
+ parser.add_argument("--tasks", nargs="+", default=list(TASK_CONFIGS.keys()))
92
+ parser.add_argument("--out", type=Path, default=REPO_ROOT / "eval_results.json")
93
+ args = parser.parse_args()
94
+
95
+ seeds = [0] if args.quick else args.seeds
96
+ t0 = time.time()
97
+
98
+ runs: List[Dict] = []
99
+ summary: Dict[str, Dict[str, Dict]] = {}
100
+ for task_id in args.tasks:
101
+ summary[task_id] = {}
102
+ for policy_name, policy_fn in POLICIES.items():
103
+ per_task_runs = []
104
+ for seed in seeds:
105
+ rec = run_episode(task_id, seed, policy_fn)
106
+ rec["policy"] = policy_name
107
+ runs.append(rec)
108
+ per_task_runs.append(rec)
109
+ summary[task_id][policy_name] = summarize(per_task_runs)
110
+
111
+ elapsed = time.time() - t0
112
+ results = {
113
+ "tasks": args.tasks,
114
+ "seeds": seeds,
115
+ "runs": runs,
116
+ "summary": summary,
117
+ "elapsed_sec": round(elapsed, 2),
118
+ }
119
+
120
+ args.out.write_text(json.dumps(results, indent=2))
121
+ print(format_markdown(results))
122
+ print(f"\nWrote {args.out} ({elapsed:.1f}s, {len(runs)} episodes)")
123
+ return 0
124
+
125
+
126
+ if __name__ == "__main__":
127
+ raise SystemExit(main())