sasukeUchiha123 commited on
Commit
6920f9b
·
verified ·
1 Parent(s): f1a4113

Upload agent/tools/benchmark.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. agent/tools/benchmark.py +222 -0
agent/tools/benchmark.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """benchmark tool — full benchmark (default 50 steps), version-tagged cached.
2
+
3
+ Delegates to `runner.protocol.LiveRunner` (which auto-falls-back to FakeRunner
4
+ when the host has no GPU). Adds a content-addressed cache keyed on:
5
+
6
+ sha256(canonical_config_json
7
+ || workload_script_sha
8
+ || rocm_image_tag
9
+ || runner_script_sha)
10
+
11
+ so re-running the same config is free, but a stale cache entry from before
12
+ a runner-script edit or a container bump is automatically invalidated.
13
+
14
+ Cache files live under `bench_cache/<hash>.json`. Pass `force_rerun=True` in
15
+ the tool input to bypass the cache for a single call (architecture.md §6:
16
+ the Day-3 dry-run that confirms cached results haven't gone stale).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import json
23
+ import logging
24
+ import os
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from agent.schemas import RunMetrics, ToolResult, WorkloadConfig
29
+ from agent.tools import Tool
30
+ from runner.protocol import _default_runner
31
+
32
+ _LOG = logging.getLogger(__name__)
33
+
34
+ _RUNNER = _default_runner()
35
+
36
+ _REPO_ROOT = Path(__file__).resolve().parent.parent.parent
37
+ _CACHE_DIR = _REPO_ROOT / "bench_cache"
38
+ _WORKLOAD_SCRIPT = _REPO_ROOT / "workloads" / "train_qwen_lora.py"
39
+ _RUNNER_SCRIPT = _REPO_ROOT / "runner" / "goblin_runner.sh"
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Cache-key helpers
44
+ # ---------------------------------------------------------------------------
45
+
46
+
47
+ def _canonical_config_json(config: dict) -> str:
48
+ """Stable JSON for hashing — deterministic key order, no whitespace drift."""
49
+ # Round-trip through WorkloadConfig so unknown keys don't leak into the
50
+ # hash and defaults are filled in consistently.
51
+ workload = WorkloadConfig.model_validate(config)
52
+ return json.dumps(workload.model_dump(), sort_keys=True, separators=(",", ":"))
53
+
54
+
55
+ def _file_sha(path: Path) -> str:
56
+ if not path.exists():
57
+ return "none"
58
+ return hashlib.sha256(path.read_bytes()).hexdigest()
59
+
60
+
61
+ def _container_tag() -> str:
62
+ """The ROCm container tag if running in CI/cloud, else `unknown`."""
63
+ return os.environ.get("ROCM_IMAGE_TAG", "unknown")
64
+
65
+
66
+ def _cache_key(config: dict, steps: int) -> str:
67
+ """Compose the cache key per architecture.md §6.
68
+
69
+ `steps` is included because a 10-step profile and a 500-step benchmark
70
+ are not the same artefact even with the same config. (Strictly speaking
71
+ architecture.md §6 only mentions config + workload + container + runner;
72
+ we add steps so profile_run-style short calls don't poison long-run
73
+ benchmark cache entries.)
74
+ """
75
+ payload = (
76
+ _canonical_config_json(config)
77
+ + "|"
78
+ + _file_sha(_WORKLOAD_SCRIPT)
79
+ + "|"
80
+ + _container_tag()
81
+ + "|"
82
+ + _file_sha(_RUNNER_SCRIPT)
83
+ + "|steps="
84
+ + str(steps)
85
+ )
86
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
87
+
88
+
89
+ def _cache_path(key: str) -> Path:
90
+ return _CACHE_DIR / f"{key}.json"
91
+
92
+
93
+ def _read_cache(key: str) -> dict[str, Any] | None:
94
+ path = _cache_path(key)
95
+ if not path.exists():
96
+ return None
97
+ try:
98
+ return json.loads(path.read_text())
99
+ except (OSError, json.JSONDecodeError) as exc:
100
+ _LOG.warning("benchmark: failed to read cache %s (%s); ignoring", path, exc)
101
+ return None
102
+
103
+
104
+ def _write_cache(key: str, metrics: RunMetrics, config: dict, steps: int) -> None:
105
+ try:
106
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
107
+ path = _cache_path(key)
108
+ # Persist the raw key tuple alongside the metrics so a human can
109
+ # debug "why did this cache hit?" without re-hashing.
110
+ payload = {
111
+ "metrics": metrics.model_dump(),
112
+ "key_components": {
113
+ "config": json.loads(_canonical_config_json(config)),
114
+ "steps": steps,
115
+ "workload_script_sha": _file_sha(_WORKLOAD_SCRIPT),
116
+ "rocm_image_tag": _container_tag(),
117
+ "runner_script_sha": _file_sha(_RUNNER_SCRIPT),
118
+ },
119
+ }
120
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True))
121
+ except OSError as exc:
122
+ _LOG.warning("benchmark: failed to write cache (%s); continuing", exc)
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Tool entry point
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ def _benchmark(
131
+ config: dict,
132
+ steps: int = 50,
133
+ cache: bool = True,
134
+ force_rerun: bool | None = None,
135
+ ) -> ToolResult:
136
+ """Run a benchmark. ``cache`` is the natural-language knob; ``force_rerun``
137
+ is kept as a backward-compat alias (cache=False ≡ force_rerun=True).
138
+ Live-AMD-GPU lesson: the LLM tends to pass ``cache: true`` when it means
139
+ "use the cache" — make that work directly.
140
+ """
141
+ # Defensive: Qwen2.5-7B occasionally nests ``steps`` / ``cache`` /
142
+ # ``force_rerun`` *inside* the config dict instead of at the top level
143
+ # alongside it. WorkloadConfig strict-validates extras, so the call
144
+ # would error out and waste a tool slot. Extract them back to the
145
+ # top-level args, with the caller's explicit values winning ties.
146
+ if isinstance(config, dict):
147
+ misnested_steps = config.pop("steps", None)
148
+ misnested_cache = config.pop("cache", None)
149
+ misnested_force = config.pop("force_rerun", None)
150
+ if misnested_steps is not None and steps == 50:
151
+ try:
152
+ steps = int(misnested_steps)
153
+ except (TypeError, ValueError):
154
+ pass
155
+ if misnested_cache is not None and cache is True:
156
+ cache = bool(misnested_cache)
157
+ if misnested_force is not None and force_rerun is None:
158
+ force_rerun = bool(misnested_force)
159
+
160
+ if force_rerun is not None:
161
+ # Explicit force_rerun overrides cache; legacy callers keep working.
162
+ use_cache = not force_rerun
163
+ else:
164
+ use_cache = bool(cache)
165
+
166
+ key = _cache_key(config, steps)
167
+
168
+ if use_cache:
169
+ cached = _read_cache(key)
170
+ if cached is not None:
171
+ metrics_dict = cached.get("metrics", cached)
172
+ metrics = RunMetrics.model_validate(metrics_dict)
173
+ metrics.warnings = [
174
+ "benchmark: cache hit (pass cache=False to bypass)",
175
+ *metrics.warnings,
176
+ ]
177
+ return ToolResult(ok=True, result=metrics.model_dump())
178
+
179
+ workload = WorkloadConfig.model_validate(config)
180
+ metrics = _RUNNER.run(workload, steps=steps)
181
+ _write_cache(key, metrics, config, steps)
182
+ return ToolResult(ok=True, result=metrics.model_dump())
183
+
184
+
185
+ BENCHMARK = Tool(
186
+ name="benchmark",
187
+ description=(
188
+ "Full benchmark (default 50 steps). Same metric shape as profile_run "
189
+ "but at production-scale step count. Result is cached by a version-"
190
+ "tagged hash so re-runs of the same config are free. Use this AFTER "
191
+ "propose_patch to validate the patched config — and call it once on "
192
+ "the original config for before/after comparison.\n"
193
+ "\n"
194
+ "Pass ``cache: false`` to bypass the cache and force a fresh measurement "
195
+ "(used by the Day-3 dry-run that confirms cached results haven't gone "
196
+ "stale). The default ``cache: true`` is what you want for the demo."
197
+ ),
198
+ input_schema={
199
+ "type": "object",
200
+ "properties": {
201
+ "config": {"type": "object", "description": "WorkloadConfig dict."},
202
+ "steps": {
203
+ "type": "integer",
204
+ "default": 50,
205
+ "minimum": 10,
206
+ "maximum": 500,
207
+ "description": "Number of measured steps (after a 2-step warmup).",
208
+ },
209
+ "cache": {
210
+ "type": "boolean",
211
+ "default": True,
212
+ "description": (
213
+ "If true (the default), reuse a previous benchmark for "
214
+ "the same config + workload + container. If false, force "
215
+ "a fresh measurement against the live runner."
216
+ ),
217
+ },
218
+ },
219
+ "required": ["config"],
220
+ },
221
+ fn=_benchmark,
222
+ )