Skip to content

Commit 445992d

Browse files
authored
Merge pull request #3 from varad-more/speculative-decoding
Added decode sweep benchmark runs and gemma4 model
2 parents 4fd6b35 + 1d10dbc commit 445992d

476 files changed

Lines changed: 1876153 additions & 354 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 256 additions & 15 deletions
Large diffs are not rendered by default.

analysis/decode_length_analysis.py

Lines changed: 424 additions & 0 deletions
Large diffs are not rendered by default.

analysis/final_report.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,127 @@ def aggregate_results(results_dir: Path, model: str | None = None) -> dict[str,
7070
"available_models": available_models,
7171
"selected_model": selected_model,
7272
"selection_mode": selection_mode,
73+
"_raw_records": records, # kept for saturation analysis; not serialised to JSON
7374
}
7475

7576

77+
def _compute_saturation(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
78+
"""
79+
For throughput_ramp and throughput_ramp_extended result files, partition requests
80+
by concurrency level (using scenario_config) and compute per-level throughput.
81+
82+
Throughput per level ≈ (total output tokens × concurrency) / sum(total_ms / 1000)
83+
This is an approximation for a semaphore-bounded concurrent workload.
84+
85+
Returns list of rows: {model, engine, scenario, concurrency, tokens_per_sec, requests}
86+
"""
87+
rows: list[dict[str, Any]] = []
88+
ramp_scenarios = {"throughput_ramp", "throughput_ramp_extended"}
89+
90+
for rec in records:
91+
if rec.get("scenario_name") not in ramp_scenarios:
92+
continue
93+
cfg = rec.get("scenario_config", {})
94+
concurrency_levels = cfg.get("concurrency_levels", [])
95+
rpl = cfg.get("requests_per_level", 100)
96+
if not concurrency_levels or not rpl:
97+
continue
98+
99+
model_raw = rec.get("run_metadata", {}).get("model", "")
100+
from analysis import get_engine_variant
101+
engine = get_engine_variant(rec)
102+
model = model_raw.split("/")[-1] if model_raw else "unknown"
103+
scenario = rec.get("scenario_name", "unknown")
104+
105+
sorted_requests = sorted(rec.get("requests", []), key=lambda r: r["request_id"])
106+
107+
for i, concurrency in enumerate(concurrency_levels):
108+
level_reqs = sorted_requests[i * rpl : (i + 1) * rpl]
109+
successful = [r for r in level_reqs if r.get("success")]
110+
if not successful:
111+
continue
112+
total_tokens = sum(r.get("output_tokens", 0) for r in successful)
113+
total_ms = sum(r.get("total_ms", 0.0) for r in successful)
114+
if total_ms <= 0:
115+
continue
116+
# Approximation: concurrent throughput = (tokens × concurrency) / total_latency
117+
approx_tps = total_tokens * concurrency / (total_ms / 1000.0)
118+
rows.append(
119+
{
120+
"model": model,
121+
"engine": engine,
122+
"scenario": scenario,
123+
"concurrency": concurrency,
124+
"tokens_per_sec": approx_tps,
125+
"n_requests": len(successful),
126+
}
127+
)
128+
return rows
129+
130+
131+
def _render_saturation(saturation_rows: list[dict[str, Any]]) -> list[str]:
132+
"""Render a 'Saturation Analysis' section from per-level throughput rows."""
133+
if not saturation_rows:
134+
return []
135+
136+
from collections import defaultdict
137+
138+
lines: list[str] = [
139+
"## Saturation Analysis",
140+
"",
141+
"Per-level throughput from `throughput_ramp` and `throughput_ramp_extended` runs. "
142+
"Throughput is approximated as `(output tokens × concurrency) / Σ(total_ms)`. "
143+
"The **saturation point** is the lowest concurrency level at which throughput is within 5% of the run maximum.",
144+
"",
145+
]
146+
147+
# Group by (model, engine, scenario)
148+
grouped: dict[tuple[str, str, str], list[dict]] = defaultdict(list)
149+
for row in saturation_rows:
150+
grouped[(row["model"], row["engine"], row["scenario"])].append(row)
151+
152+
# Collect all concurrency levels seen
153+
all_concurrencies: list[int] = sorted({r["concurrency"] for r in saturation_rows})
154+
conc_header = " | ".join(f"C={c}" for c in all_concurrencies)
155+
conc_sep = " | ".join("------" for _ in all_concurrencies)
156+
157+
lines.append(f"| Model | Engine | Scenario | {conc_header} | Saturation point |")
158+
lines.append(f"|-------|--------|----------|{conc_sep}|-----------------|")
159+
160+
# Aggregate multiple runs by mean
161+
for key in sorted(grouped.keys()):
162+
model, engine, scenario = key
163+
level_rows = grouped[key]
164+
165+
# Average across multiple runs at the same concurrency
166+
from statistics import mean as _mean
167+
by_conc: dict[int, list[float]] = defaultdict(list)
168+
for r in level_rows:
169+
by_conc[r["concurrency"]].append(r["tokens_per_sec"])
170+
avg_tps = {c: _mean(vs) for c, vs in by_conc.items()}
171+
172+
max_tps = max(avg_tps.values()) if avg_tps else 1.0
173+
sat_point = "—"
174+
for c in all_concurrencies:
175+
tps = avg_tps.get(c, 0.0)
176+
if tps >= max_tps * 0.95:
177+
sat_point = f"C={c}"
178+
break
179+
180+
cells = []
181+
for c in all_concurrencies:
182+
tps = avg_tps.get(c)
183+
if tps is None:
184+
cells.append("—")
185+
else:
186+
cells.append(f"{tps:.0f}")
187+
cells_str = " | ".join(cells)
188+
lines.append(f"| {model} | {engine} | {scenario} | {cells_str} | {sat_point} |")
189+
190+
lines.append("")
191+
return lines
192+
193+
76194
def render_markdown(summary: dict[str, Any]) -> str:
77195
rows = summary["rows"]
78196
models = sorted({row["model"] for row in rows})
@@ -105,6 +223,12 @@ def render_markdown(summary: dict[str, Any]) -> str:
105223
lines.append("No result files found.")
106224
return "\n".join(lines)
107225

226+
# Saturation analysis (requires access to raw records stored in summary)
227+
raw_records = summary.get("_raw_records", [])
228+
if raw_records:
229+
saturation_rows = _compute_saturation(raw_records)
230+
lines.extend(_render_saturation(saturation_rows))
231+
108232
for model in models:
109233
lines.extend(
110234
[

0 commit comments

Comments
 (0)