File size: 14,444 Bytes
0693474 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | """ACC-SPLIT tables over `sources.Obs`: acceptance by prompt, answer length, segment, batch and date, plus the metric
reconciliation (server counters vs SSE-derived) and the 32K / short shift decomposition.
Every value is MEASURED from the files `sources` reads, except where a row says ESTIMATED (SSE-derived segment
acceptance, the late-tokens estimate). Intervals are 95 % t-intervals over independent runs; they describe run-to-run
spread at T = 1, not a population claim about unseen prompts.
"""
from __future__ import annotations
import math
import statistics
from collections import defaultdict
from collections.abc import Callable, Iterable, Mapping, Sequence
from dataclasses import dataclass
from offload.drafter.acc_split.sources import Obs
from offload.prefill_maxlen.evidence import Json
# two-sided 95 % t quantiles by degrees of freedom (1..30); 1.96 beyond
_T95 = (12.706, 4.303, 3.182, 2.776, 2.571, 2.447, 2.365, 2.306, 2.262, 2.228, 2.201, 2.179, 2.160, 2.145, 2.131,
2.120, 2.110, 2.101, 2.093, 2.086, 2.080, 2.074, 2.069, 2.064, 2.060, 2.056, 2.052, 2.048, 2.045, 2.042)
POSITIONS = 5
# window start roles that run production as is (WIN2 CONTROL_ROLES); every other role is a candidate arm whose change may
# move acceptance (DEP1 fixed depth, PP4 sampling, native drafter) and is excluded from prompt/date/length tables.
CONTROL_ROLES = ('control', 'K', 'off', 'H')
def _jlist(values: Iterable[Json]) -> list[Json]:
return list(values)
def t95(df: int) -> float:
if df < 1:
raise ValueError('need at least two values for an interval')
return _T95[df - 1] if df <= len(_T95) else 1.96
@dataclass(frozen=True)
class Summary:
n: int
mean: float
sd: float
lo: float
hi: float
def json(self) -> dict[str, Json]:
return {'n': self.n, 'mean': round(self.mean, 4), 'sd': round(self.sd, 4), 'ci95': [round(self.lo, 4), round(self.hi, 4)]}
def summarize(values: Sequence[float]) -> Summary:
if not values:
raise ValueError('nothing to summarize')
mean = statistics.fmean(values)
if len(values) == 1:
return Summary(1, mean, 0.0, mean, mean)
sd = statistics.stdev(values)
half = t95(len(values) - 1) * sd / math.sqrt(len(values))
return Summary(len(values), mean, sd, mean - half, mean + half)
def prompt_names(obs: Iterable[Obs]) -> dict[str, str]:
"""A readable family name per prompt sha: bench2 '<workload>-<ctx>' or the window cells that sent it."""
names: dict[str, set[str]] = defaultdict(set)
for o in obs:
if o.batch == 1:
base = o.cell if o.source == 'bench2' else o.cell.removeprefix('ctl-').removeprefix('prof-')
names[o.prompt].add(('bench2:' if o.source == 'bench2' else 'corpus:') + base)
return {sha: '+'.join(sorted(v)) for sha, v in names.items()}
def _b1(obs: Iterable[Obs], keep: Callable[[Obs], bool] | None = None) -> list[Obs]:
"""Single-stream, reasoning-only-or-mixed runs; bench2 cold only (warm repeats and drills are other questions)."""
out = []
for o in obs:
if o.batch != 1 or o.rounds <= 0:
continue
if o.source == 'bench2' and (o.cache != 'cold' or 'drill' in o.label):
continue
if o.source == 'cell' and o.role not in CONTROL_ROLES:
continue
if keep is None or keep(o):
out.append(o)
return out
def position_rates(group: Sequence[Obs]) -> list[float]:
"""Pooled unconditional accepted-at-position-k per round (positions 0..4)."""
rounds = sum(o.rounds for o in group)
return [round(sum(o.per_position[k] if k < len(o.per_position) else 0.0 for o in group) / rounds, 4)
for k in range(POSITIONS)]
def by_prompt(obs: Iterable[Obs]) -> list[dict[str, Json]]:
"""Acceptance per (prompt, answer budget) at B1, with dates, depth, per-position rates and round time."""
rows = _b1(obs)
names = prompt_names(rows)
groups: dict[tuple[str, int], list[Obs]] = defaultdict(list)
for o in rows:
groups[(o.prompt, o.max_tokens)].append(o)
out: list[dict[str, Json]] = []
for (sha, budget), group in sorted(groups.items(), key=lambda kv: (names[kv[0][0]], kv[0][1])):
ms = [o.ms_per_round for o in group if o.ms_per_round is not None]
out.append({
'prompt': names[sha], 'prompt_sha256_12': sha[:12], 'prompt_tokens': group[0].prompt_tokens[0],
'max_tokens': budget, 'dates': _jlist(sorted({o.date for o in group})),
'configs': len({o.label for o in group}),
'answer_tokens_mean': round(statistics.fmean(o.answer_tokens for o in group), 1),
'accepted_per_round': summarize([o.accepted_per_round for o in group]).json(),
'pooled_accepted_per_round': round(sum(o.accepted for o in group) / sum(o.rounds for o in group), 4),
'depth': round(sum(o.draft_tokens for o in group) / sum(o.rounds for o in group), 3),
'per_position': _jlist(position_rates(group)),
'ms_per_round': round(statistics.fmean(ms), 2) if ms else None})
return out
def by_prompt_date(obs: Iterable[Obs]) -> list[dict[str, Json]]:
"""Per prompt and budget, the mean per date: the stability test for H1 (same prompt, different days and configs)."""
rows = _b1(obs)
names = prompt_names(rows)
groups: dict[tuple[str, int, str], list[float]] = defaultdict(list)
for o in rows:
groups[(o.prompt, o.max_tokens, o.date)].append(o.accepted_per_round)
return [{'prompt': names[sha], 'max_tokens': budget, 'date': date, **summarize(values).json()}
for (sha, budget, date), values in sorted(groups.items(), key=lambda kv: (names[kv[0][0]], kv[0][1], kv[0][2]))]
def by_window(obs: Iterable[Obs]) -> list[dict[str, Json]]:
"""Per window (or bench2 config label) and prompt/budget at B1: drift within and across days."""
rows = _b1(obs)
names = prompt_names(rows)
groups: dict[tuple[str, int, str, str], list[float]] = defaultdict(list)
for o in rows:
groups[(o.prompt, o.max_tokens, o.date, o.label)].append(o.accepted_per_round)
return [{'prompt': names[sha], 'max_tokens': budget, 'date': date, 'window': label, **summarize(v).json()}
for (sha, budget, date, label), v in sorted(groups.items(), key=lambda kv: (names[kv[0][0]], kv[0][1],
kv[0][2], kv[0][3]))]
def metric_reconciliation(obs: Iterable[Obs]) -> list[dict[str, Json]]:
"""H2: server accepted/round vs what a client derives from tok/s and the SSE step gap (bench2, B1)."""
rows = [o for o in _b1(obs) if o.source == 'bench2' and o.decode_tps and o.step_gap_p50_ms and o.step_gap_mean_ms
and o.step_events]
names = prompt_names(rows)
groups: dict[tuple[str, int], list[Obs]] = defaultdict(list)
for o in rows:
groups[(o.prompt, o.max_tokens)].append(o)
out: list[dict[str, Json]] = []
for (sha, budget), group in sorted(groups.items(), key=lambda kv: (names[kv[0][0]], kv[0][1])):
if len(group) < 2:
continue
def mean(f: Callable[[Obs], float]) -> float:
return round(statistics.fmean(f(o) for o in group), 4)
out.append({
'prompt': names[sha], 'max_tokens': budget, 'n': len(group),
'server': mean(lambda o: o.accepted_per_round),
'tps_x_p50_gap': mean(lambda o: (o.decode_tps or 0.0) * (o.step_gap_p50_ms or 0.0) / 1000 - 1),
'tps_x_mean_gap': mean(lambda o: (o.decode_tps or 0.0) * (o.step_gap_mean_ms or 0.0) / 1000 - 1),
'tokens_per_sse_event': mean(lambda o: (o.reasoning_tokens + o.answer_tokens) / (o.step_events or 1) - 1),
'rounds_per_sse_event': mean(lambda o: o.rounds / (o.step_events or 1))})
return out
def length_split(obs: Iterable[Obs]) -> list[dict[str, Json]]:
"""Same prompt at two answer budgets: acceptance of the first budget, and ESTIMATED acceptance of the tokens after
it ((accepted_L - accepted_S) / (rounds_L - rounds_S) over group means)."""
rows = _b1(obs)
names = prompt_names(rows)
groups: dict[str, dict[int, list[Obs]]] = defaultdict(lambda: defaultdict(list))
for o in rows:
groups[o.prompt][o.max_tokens].append(o)
out: list[dict[str, Json]] = []
for sha, budgets in sorted(groups.items(), key=lambda kv: names[kv[0]]):
keys = sorted(b for b, g in budgets.items() if len(g) >= 2)
for short, long in zip(keys, keys[1:]):
s, lg = budgets[short], budgets[long]
acc_s, r_s = statistics.fmean(o.accepted for o in s), statistics.fmean(o.rounds for o in s)
acc_l, r_l = statistics.fmean(o.accepted for o in lg), statistics.fmean(o.rounds for o in lg)
late = (acc_l - acc_s) / (r_l - r_s) if r_l > r_s else None
out.append({'prompt': names[sha], 'short_budget': short, 'long_budget': long,
'short': summarize([o.accepted_per_round for o in s]).json(),
'long': summarize([o.accepted_per_round for o in lg]).json(),
'late_tokens_estimated': round(late, 4) if late is not None else None,
'short_dates': _jlist(sorted({o.date for o in s})),
'long_dates': _jlist(sorted({o.date for o in lg}))})
return out
def segment_split(obs: Iterable[Obs]) -> dict[str, Json]:
"""ESTIMATED acceptance per output kind (reasoning vs answer), B1 bench2 runs.
Two estimators: (a) SSE events per kind, tokens per event - 1, validated on reasoning-only runs against the server
counter; (b) for runs without SSE step times, per-kind client decode tok/s with one assumption, equal round time C in
both segments: C = (r_tok / tps_r + a_tok / tps_a) / server_rounds, A_kind = tps_kind * C - 1. Both reproduce the
server's whole-run figure by construction ((b) exactly, (a) within its validated bias)."""
rows = [o for o in _b1(obs) if o.source == 'bench2']
only = [o for o in rows if set(o.segments) == {'reasoning'}]
validation = [o.segments['reasoning'][0] / o.segments['reasoning'][1] - 1 - o.accepted_per_round for o in only]
names = prompt_names(rows)
mixed: list[dict[str, Json]] = []
for o in rows:
if o.answer_tokens <= 0 or o.reasoning_tokens <= 0:
continue
row: dict[str, Json] = {'prompt': names[o.prompt], 'run': o.where[:12], 'label': o.label, 'date': o.date,
'max_tokens': o.max_tokens, 'reasoning_tokens': o.reasoning_tokens,
'answer_tokens': o.answer_tokens, 'server_all': round(o.accepted_per_round, 4)}
if {'reasoning', 'answer'} <= set(o.segments):
(rt, rev), (at, aev) = o.segments['reasoning'], o.segments['answer']
row.update(method='sse_events', reasoning_estimated=round(rt / rev - 1, 4),
answer_estimated=round(at / aev - 1, 4))
elif {'reasoning', 'answer'} <= set(o.segment_tps):
tr, ta = o.segment_tps['reasoning'], o.segment_tps['answer']
c = (o.reasoning_tokens / tr + o.answer_tokens / ta) / o.rounds
row.update(method='tps_equal_round_time', round_s_estimated=round(c, 5),
reasoning_estimated=round(tr * c - 1, 4), answer_estimated=round(ta * c - 1, 4))
else:
continue
mixed.append(row)
return {'validation_runs': len(only),
'validation_sse_minus_server': summarize(validation).json() if len(validation) >= 2 else None,
'mixed_runs': _jlist(mixed)}
def batch_split(obs: Iterable[Obs]) -> list[dict[str, Json]]:
"""Acceptance by batch for window cells at the same budget (multi-user strata)."""
groups: dict[tuple[str, int, int], list[Obs]] = defaultdict(list)
for o in obs:
if o.source == 'cell' and o.rounds > 0 and o.role in CONTROL_ROLES:
groups[(o.cell.removeprefix('ctl-'), o.max_tokens, o.batch)].append(o)
return [{'cell': cell, 'max_tokens': budget, 'batch': batch, 'dates': _jlist(sorted({o.date for o in g})),
'accepted_per_round': summarize([o.accepted_per_round for o in g]).json(),
'depth': round(sum(o.draft_tokens for o in g) / sum(o.rounds for o in g), 3),
'per_position': _jlist(position_rates(g))}
for (cell, budget, batch), g in sorted(groups.items())]
def decompose(obs: Iterable[Obs], old: str, new: str, old_budget: int, new_budget: int) -> dict[str, Json]:
"""Walk one shift from (old prompt, old budget) to (new prompt, new budget) through the shared budget: prompt step at
the old budget, then length step on the new prompt. `old`/`new` are prompt-name substrings."""
rows = _b1(obs)
names = prompt_names(rows)
def pick(name: str, budget: int) -> list[float]:
return [o.accepted_per_round for o in rows if name in names[o.prompt].split('+') and o.max_tokens == budget]
a, b, c = pick(old, old_budget), pick(new, old_budget), pick(new, new_budget)
if not a or not b or not c:
raise ValueError(f'decompose {old}->{new}: missing cell(s): n={len(a)},{len(b)},{len(c)}')
sa, sb, sc = summarize(a), summarize(b), summarize(c)
return {'from': {'prompt': old, 'max_tokens': old_budget, **sa.json()},
'via': {'prompt': new, 'max_tokens': old_budget, **sb.json()},
'to': {'prompt': new, 'max_tokens': new_budget, **sc.json()},
'prompt_step': round(sb.mean - sa.mean, 4), 'length_step': round(sc.mean - sb.mean, 4),
'total': round(sc.mean - sa.mean, 4)}
def report(obs: Sequence[Obs], shifts: Mapping[str, tuple[str, str, int, int]]) -> dict[str, Json]:
return {
'schema': 'dsv41-acc-split-v1',
'counts': {'bench2_runs': sum(o.source == 'bench2' for o in obs), 'window_cells': sum(o.source == 'cell' for o in obs),
'b1_used': len(_b1(obs))},
'by_prompt': _jlist(by_prompt(obs)), 'by_prompt_date': _jlist(by_prompt_date(obs)), 'by_window': _jlist(by_window(obs)),
'metric_reconciliation': _jlist(metric_reconciliation(obs)), 'length_split': _jlist(length_split(obs)),
'segment_split': segment_split(obs), 'batch_split': _jlist(batch_split(obs)),
'shifts': {name: decompose(obs, *spec) for name, spec in shifts.items()}}
|