narcolepticchicken commited on
Commit
47ac3cc
Β·
verified Β·
1 Parent(s): b352d2e

Upload ablation_study.py

Browse files
Files changed (1) hide show
  1. ablation_study.py +278 -0
ablation_study.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ACO Ablation Study + Cost-Quality Frontier Report.
3
+
4
+ 10 ablations from spec:
5
+ 1. no model router
6
+ 2. no context budgeter
7
+ 3. no cache-aware layout
8
+ 4. no tool-use cost gate
9
+ 5. no verifier budgeter
10
+ 6. no retry optimizer
11
+ 7. no meta-tools
12
+ 8. no early termination
13
+ 9. no specialist models (force all routing through frontier-tier)
14
+ 10. no telemetry feedback
15
+
16
+ Each ablation removes one module from the full ACO and re-runs the benchmark.
17
+ Reports which modules actually save money and which are noise.
18
+ """
19
+ import json, sys, random, os
20
+ from dataclasses import dataclass, field, asdict
21
+ from collections import defaultdict
22
+ from typing import List, Dict
23
+
24
+ # Import the benchmark suite
25
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)) or ".")
26
+ import importlib.util
27
+
28
+ # Try local import, fall back to Hub download
29
+ try:
30
+ from benchmark_suite import (
31
+ generate_tasks, simulate_task, Config, CONFIGS, MODELS,
32
+ FRONTIER, CHEAP, MEDIUM, TIER_CHEAPEST,
33
+ compute_metrics, run_benchmark
34
+ )
35
+ except ImportError:
36
+ import urllib.request
37
+ url = "https://huggingface.co/narcolepticchicken/agent-cost-optimizer/resolve/main/benchmark_suite.py"
38
+ path = "/tmp/benchmark_suite.py"
39
+ if not os.path.exists(path):
40
+ urllib.request.urlretrieve(url, path)
41
+ sys.path.insert(0, "/tmp")
42
+ from benchmark_suite import (
43
+ generate_tasks, simulate_task, Config, CONFIGS, MODELS,
44
+ FRONTIER, CHEAP, MEDIUM, TIER_CHEAPEST,
45
+ compute_metrics, run_benchmark
46
+ )
47
+
48
+ random.seed(42)
49
+
50
+ # ═══════════════════════════════════════════════════════════════════
51
+ # Ablation configs: full ACO minus one module
52
+ # ═══════════════════════════════════════════════════════════════════
53
+
54
+ def make_ablation_configs() -> List[Config]:
55
+ """Create 10 ablation configs, each removing one module from full ACO."""
56
+ base = dict(
57
+ use_model_routing=True, use_learned_router=True,
58
+ use_context_budget=True, use_cache_layout=True,
59
+ use_tool_gate=True, use_verifier_budget=True,
60
+ use_retry_optimizer=True, use_meta_tools=True,
61
+ use_early_termination=True, use_telemetry=True,
62
+ )
63
+ ablations = [
64
+ Config("abl1", "no model router", use_model_routing=False, use_learned_router=False,
65
+ use_context_budget=True, use_cache_layout=True, use_tool_gate=True,
66
+ use_verifier_budget=True, use_retry_optimizer=True, use_meta_tools=True,
67
+ use_early_termination=True, use_telemetry=True),
68
+ Config("abl2", "no context budgeter", **{**base, "use_context_budget": False}),
69
+ Config("abl3", "no cache layout", **{**base, "use_cache_layout": False}),
70
+ Config("abl4", "no tool gate", **{**base, "use_tool_gate": False}),
71
+ Config("abl5", "no verifier budgeter", **{**base, "use_verifier_budget": False}),
72
+ Config("abl6", "no retry optimizer", **{**base, "use_retry_optimizer": False}),
73
+ Config("abl7", "no meta-tools", **{**base, "use_meta_tools": False}),
74
+ Config("abl8", "no early termination", **{**base, "use_early_termination": False}),
75
+ Config("abl9", "no specialist models",
76
+ use_model_routing=True, use_learned_router=True,
77
+ use_context_budget=True, use_cache_layout=True, use_tool_gate=True,
78
+ use_verifier_budget=True, use_retry_optimizer=True, use_meta_tools=True,
79
+ use_early_termination=True, use_telemetry=True),
80
+ Config("abl10", "no telemetry feedback", **{**base, "use_telemetry": False}),
81
+ ]
82
+ return ablations
83
+
84
+
85
+ # ═══════════════════════════════════════════════════════════════════
86
+ # Override select_model for ablation 9 (no specialist models)
87
+ # ═══════════════════════════════════════════════════════════════════
88
+
89
+ def select_model_abl9(task) -> str:
90
+ """No specialist models: always use frontier-tier (tier 4) for routed tasks."""
91
+ if task.risk_level == "high" or task.difficulty > 0.5:
92
+ return "gpt-5.2" # tier 4
93
+ if task.difficulty > 0.2:
94
+ return "gpt-5.2"
95
+ return "gpt-5-mini" # tier 2 minimum, no tier-1 specialist
96
+
97
+
98
+ def run_ablation(n_per_domain: int = 20) -> Dict:
99
+ """Run all 10 ablations + full ACO baseline."""
100
+ tasks = generate_tasks(n_per_domain)
101
+ full_aco = Config("I", "full ACO", use_model_routing=True, use_learned_router=True,
102
+ use_context_budget=True, use_cache_layout=True, use_tool_gate=True,
103
+ use_verifier_budget=True, use_retry_optimizer=True, use_meta_tools=True,
104
+ use_early_termination=True, use_telemetry=True)
105
+ all_configs = [full_aco] + make_ablation_configs()
106
+
107
+ print(f"Generated {len(tasks)} tasks across 5 domains")
108
+ print(f"Running {len(all_configs)} configs (full ACO + 10 ablations) x {len(tasks)} tasks\n")
109
+
110
+ all_results = []
111
+ for config in all_configs:
112
+ print(f" {config.name}: {config.label}...", end=" ", flush=True)
113
+ for task in tasks:
114
+ # Special handling for ablation 9
115
+ if config.name == "abl9":
116
+ # Override model selection to avoid specialist (tier 1) models
117
+ result = simulate_task(config, task)
118
+ # Force model to non-specialist
119
+ if result["tier"] == 1:
120
+ result["model"] = "gpt-5-mini"
121
+ result["tier"] = 2
122
+ # Recalculate cost
123
+ mi = MODELS["gpt-5-mini"]
124
+ result["cost"] = round(
125
+ (result["input_tokens"] / 1_000_000) * mi["cost_in"] +
126
+ (result["output_tokens"] / 1_000_000) * mi["cost_out"], 6)
127
+ all_results.append(result)
128
+ else:
129
+ all_results.append(simulate_task(config, task))
130
+ cr = [r for r in all_results if r["config"] == config.name]
131
+ n = len(cr); s = sum(1 for r in cr if r["success"]); c = sum(r["cost"] for r in cr)
132
+ print(f"{s}/{n} success, ${c:.4f} total")
133
+
134
+ return {"tasks": [asdict(t) for t in tasks], "results": all_results}
135
+
136
+
137
+ def print_ablation_report(metrics: Dict, config_labels: Dict):
138
+ print(f"\n{'='*100}")
139
+ print(f" ACO ABLATION REPORT - Which Modules Actually Save Money?")
140
+ print(f"{'='*100}")
141
+
142
+ full = metrics["by_config"]["I"]
143
+ print(f"\n Full ACO baseline: {full['success_rate']*100:.1f}% success, ${full['total_cost']:.4f} cost")
144
+
145
+ print(f"\n{'Ablation':<40} {'Success':>8} {'Cost':>10} {'Cost Ξ”':>10} {'Quality Ξ”':>10} {'Verdict':>15}")
146
+ print("-" * 100)
147
+
148
+ verdicts = []
149
+ for abl_name in ["abl1", "abl2", "abl3", "abl4", "abl5", "abl6", "abl7", "abl8", "abl9", "abl10"]:
150
+ m = metrics["by_config"][abl_name]
151
+ label = config_labels.get(abl_name, abl_name)
152
+ cost_delta = m["total_cost"] - full["total_cost"]
153
+ cost_pct = (cost_delta / full["total_cost"]) * 100 if full["total_cost"] > 0 else 0
154
+ quality_delta = (m["success_rate"] - full["success_rate"]) * 100
155
+
156
+ # Verdict: does removing this module hurt?
157
+ if quality_delta < -3:
158
+ verdict = "CRITICAL"
159
+ elif cost_delta > 0 and quality_delta < -1:
160
+ verdict = "HURTS QUALITY"
161
+ elif cost_delta > 0.02:
162
+ verdict = "SAVES MONEY"
163
+ elif abs(cost_pct) < 2 and abs(quality_delta) < 1:
164
+ verdict = "NOISE"
165
+ elif cost_delta < 0 and quality_delta >= 0:
166
+ verdict = "COST INCREASE"
167
+ else:
168
+ verdict = "MARGINAL"
169
+
170
+ verdicts.append((abl_name, label, verdict, cost_pct, quality_delta))
171
+ print(f" {label:<38} {m['success_rate']*100:>6.1f}% ${m['total_cost']:>8.4f} "
172
+ f"{cost_pct:>+8.1f}% {quality_delta:>+8.1f}pp {verdict:>15}")
173
+
174
+ print(f"\n{'='*100}")
175
+ print(f" ABLATION SUMMARY")
176
+ print(f"{'='*100}")
177
+
178
+ critical = [v for v in verdicts if v[2] == "CRITICAL"]
179
+ saves = [v for v in verdicts if v[2] == "SAVES MONEY"]
180
+ noise = [v for v in verdicts if v[2] == "NOISE"]
181
+ hurts = [v for v in verdicts if v[2] == "HURTS QUALITY"]
182
+ marginal = [v for v in verdicts if v[2] == "MARGINAL"]
183
+
184
+ print(f"\n CRITICAL modules (removing causes >3pp quality loss):")
185
+ for _, label, _, _, qd in critical:
186
+ print(f" - {label}")
187
+ if not critical: print(f" (none)")
188
+
189
+ print(f"\n SAVES MONEY modules (removing increases cost):")
190
+ for _, label, _, cp, _ in saves:
191
+ print(f" - {label} (+{cp:.1f}% cost without it)")
192
+ if not saves: print(f" (none)")
193
+
194
+ print(f"\n NOISE modules (removing has <2% cost and <1pp quality impact):")
195
+ for _, label, _, _, _ in noise:
196
+ print(f" - {label}")
197
+ if not noise: print(f" (none)")
198
+
199
+ print(f"\n HURTS QUALITY modules (removing reduces quality but saves cost):")
200
+ for _, label, _, _, qd in hurts:
201
+ print(f" - {label} ({qd:+.1f}pp quality loss)")
202
+ if not hurts: print(f" (none)")
203
+
204
+
205
+ def print_frontier_report(metrics: Dict, config_labels: Dict):
206
+ """Cost-quality frontier: plot all configs on cost vs quality axis."""
207
+ print(f"\n{'='*100}")
208
+ print(f" COST-QUALITY FRONTIER")
209
+ print(f"{'='*100}")
210
+
211
+ configs = []
212
+ for cn in ["A", "B", "C", "D", "E", "F", "G", "H", "I"]:
213
+ m = metrics["by_config"][cn]
214
+ configs.append((cn, config_labels.get(cn, cn), m["success_rate"], m["total_cost"]))
215
+
216
+ # Sort by cost
217
+ configs.sort(key=lambda x: x[3])
218
+
219
+ print(f"\n {'Config':<40} {'Quality':>8} {'Cost':>10} {'Position':>30}")
220
+ print("-" * 90)
221
+
222
+ for cn, label, sr, cost in configs:
223
+ # Visual bar
224
+ bar_len = int(sr * 30)
225
+ cost_bar = int(cost / max(c[3] for c in configs) * 20)
226
+ bar = "β–ˆ" * bar_len + "β–‘" * (30 - bar_len)
227
+ print(f" {cn}. {label:<36} {sr*100:>6.1f}% ${cost:>8.4f} {bar}")
228
+
229
+ # Find Pareto-optimal configs
230
+ print(f"\n Pareto-optimal configs (no other config is both cheaper AND better):")
231
+ pareto = []
232
+ for i, (cn, label, sr, cost) in enumerate(configs):
233
+ dominated = False
234
+ for j, (cn2, label2, sr2, cost2) in enumerate(configs):
235
+ if i != j and sr2 >= sr and cost2 <= cost and (sr2 > sr or cost2 < cost):
236
+ dominated = True
237
+ break
238
+ if not dominated:
239
+ pareto.append((cn, label, sr, cost))
240
+
241
+ for cn, label, sr, cost in pareto:
242
+ print(f" {cn}. {label}: {sr*100:.1f}% quality at ${cost:.4f}")
243
+
244
+ # Iso-quality analysis
245
+ print(f"\n Iso-quality analysis (configs within Β±2pp of frontier quality):")
246
+ frontier_q = max(c[2] for c in configs)
247
+ for cn, label, sr, cost in configs:
248
+ if sr >= frontier_q - 0.02:
249
+ savings = (1 - cost / max(c[3] for c in configs)) * 100
250
+ print(f" {cn}. {label}: {sr*100:.1f}% at ${cost:.4f} ({savings:+.1f}% vs most expensive)")
251
+
252
+
253
+ def main():
254
+ n = int(sys.argv[1]) if len(sys.argv) > 1 else 20
255
+ data = run_ablation(n)
256
+ metrics = compute_metrics(data["results"])
257
+
258
+ config_labels = {c.name: c.label for c in CONFIGS}
259
+ # Add ablation labels
260
+ for c in make_ablation_configs():
261
+ config_labels[c.name] = c.label
262
+
263
+ print_ablation_report(metrics, config_labels)
264
+ print_frontier_report(metrics, config_labels)
265
+
266
+ output = {
267
+ "n_tasks_per_domain": n,
268
+ "metrics": metrics,
269
+ "config_labels": config_labels,
270
+ "raw_results": data["results"],
271
+ }
272
+ with open("/tmp/aco_ablation_results.json", "w") as f:
273
+ json.dump(output, f, indent=2)
274
+ print(f"\nResults saved to /tmp/aco_ablation_results.json")
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()