File size: 10,035 Bytes
1e251f3 | 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 | """WIN2-R3 smoke start (F-METHOD §3.2 rule 1): prove a candidate arm starts and serves before it costs a window.
One smoke = start the arm with start_exp.sh, prove it healthy (/health + /v1/models), one greedy prompt, one 64-token
seeded request whose server counters must move (speculative rounds > 0), then ALWAYS stop the arm. MEASURED arm start
6.8 min + ESTIMATED ~1.5 min -> ~8.5 min; the bound is SMOKE_BUDGET_S.
Two modes:
- chain-head: in a chain (production already down) the coordinator smokes every candidate of the chain back to back at
the chain head; a failing candidate is dropped before its measurement window (`chain_head`).
- x-first: outside a chain the smoke is the window's FIRST start (candidate first); a failure ends the window before any
control start (`x_first`; the runner turns FAIL into its stop).
Smoke windows never count as measurement windows (window_cap) and never write to the control archive.
The host side is the runner's own Live backend (it already owns start_exp.sh, scopes, fences and the HOLD): it implements
`Backend`. This module holds the shared sequence, timing and verdict only; it contacts nothing itself.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib
import json
import math
import os
import sys
import time
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import Protocol, cast
from offload.prefill_maxlen.evidence import Json, number, record
SMOKE_SCHEMA = 'dsv41-smoke-v1'
SMOKE_SEED = 990_001
SMOKE_TOKENS = 64
SMOKE_BUDGET_S = 15 * 60.0 # 6.8 min MEASURED start + ~1.5 min checks, with margin
COORDINATOR = 'Deepseek/coordinator'
STAGES = ('start', 'healthy', 'greedy', 'seeded', 'stop')
class Backend(Protocol):
"""The runner's Live adapter for one arm (start_exp.sh inside the runner's scopes and HOLD)."""
def start(self, arm: str) -> None: ...
def healthy(self, arm: str) -> dict[str, Json]: ... # /health + /v1/models proof of the arm
def greedy(self, arm: str) -> dict[str, Json]: ... # one registered greedy prompt: digest + tokens
def seeded(self, arm: str, body: dict[str, Json]) -> dict[str, Json]: ... # one chat completion, usage + digest
def metrics(self, arm: str) -> str: ...
def stop(self, arm: str) -> None: ...
def seeded_body(model: str) -> dict[str, Json]:
return {'model': model, 'messages': [{'role': 'user', 'content': 'Summarize the duties of a guardian ad litem.'}],
'max_tokens': SMOKE_TOKENS, 'temperature': 1.0, 'top_p': 1.0, 'seed': SMOKE_SEED, 'ignore_eos': True,
'priority': 10, 'chat_template_kwargs': {'thinking': False}}
def _drafts(text: str) -> float:
found = [float(line.split()[-1]) for line in text.splitlines()
if line.startswith(('vllm:spec_decode_num_drafts_total{', 'vllm:spec_decode_num_drafts_total '))]
if not found:
raise ValueError('metrics lack vllm:spec_decode_num_drafts_total')
return sum(found)
def smoke(backend: Backend, arm: str, *, clock: Callable[[], float] = time.monotonic,
budget_s: float = SMOKE_BUDGET_S) -> dict[str, Json]:
"""One smoke of one arm. PASS needs every stage; the arm is ALWAYS stopped. A stop failure raises after the result
is recorded in the exception text (the runner keeps its HOLD and restores, as for any stop)."""
if not (math.isfinite(budget_s) and budget_s > 0) or not arm.strip():
raise ValueError('smoke needs an arm name and a finite positive budget')
begin = clock()
timings: dict[str, Json] = {}
result: dict[str, Json] = {'schema': SMOKE_SCHEMA, 'arm': arm, 'status': 'FAIL', 'stage': 'start',
'timings_s': timings}
def stage(name: str) -> None:
result['stage'] = name
if clock() - begin > budget_s:
raise TimeoutError(f'smoke budget {budget_s:g} s exhausted before {name}')
started = False
try:
stage('start')
started = True # start_exp.sh may mutate the Sparks even when its reply is lost
backend.start(arm)
timings['start'] = clock() - begin
stage('healthy')
served = backend.healthy(arm)
model = served.get('id')
if not isinstance(model, str) or not model:
raise ValueError('healthy proof lacks the served model id')
result['served'] = {k: served[k] for k in ('id', 'max_model_len') if k in served}
timings['healthy'] = clock() - begin
stage('greedy')
greedy = backend.greedy(arm)
if not isinstance(greedy.get('sha256'), str) or number(greedy.get('tokens', 0)) <= 0:
raise ValueError('greedy prompt returned no output digest or tokens')
result['greedy'] = {'sha256': greedy['sha256'], 'tokens': greedy['tokens']}
timings['greedy'] = clock() - begin
stage('seeded')
before = _drafts(backend.metrics(arm))
summary = backend.seeded(arm, seeded_body(model))
rounds = _drafts(backend.metrics(arm)) - before
usage = record(summary['usage'])
if usage.get('completion_tokens') != SMOKE_TOKENS or rounds <= 0:
raise ValueError(f'seeded request: {usage.get("completion_tokens")} tokens, {rounds:g} speculative rounds')
result['seeded'] = {'seed': SMOKE_SEED, 'tokens': SMOKE_TOKENS, 'rounds': rounds,
'output_sha256': summary.get('output_sha256')}
timings['seeded'] = clock() - begin
result['status'] = 'PASS'
except (Exception, KeyboardInterrupt) as exc:
result['error'] = f'{type(exc).__name__}: {exc}'
finally:
if started:
try:
backend.stop(arm)
timings['stop'] = clock() - begin
except (Exception, KeyboardInterrupt) as exc:
result['status'] = 'FAIL'
result['stop_error'] = f'{type(exc).__name__}: {exc}'
raise RuntimeError(f'smoke of {arm}: arm stop failed; HOLD stays, restore: {json.dumps(result)}') from exc
if result['status'] == 'PASS':
result['stage'] = 'done'
return result
def chain_head(backend: Backend, arms: Sequence[str], *, clock: Callable[[], float] = time.monotonic,
budget_s: float = SMOKE_BUDGET_S) -> dict[str, Json]:
"""Chain mode: smoke every candidate back to back at the chain head; failures are dropped before their windows."""
if not arms or len(set(arms)) != len(arms):
raise ValueError('chain-head smoke needs distinct candidate arms')
results = [smoke(backend, arm, clock=clock, budget_s=budget_s) for arm in arms]
return {'schema': SMOKE_SCHEMA, 'mode': 'chain-head', 'results': cast(Json, results),
'passed': [str(r['arm']) for r in results if r['status'] == 'PASS'],
'dropped': [str(r['arm']) for r in results if r['status'] != 'PASS']}
def x_first(backend: Backend, arm: str, *, clock: Callable[[], float] = time.monotonic,
budget_s: float = SMOKE_BUDGET_S) -> dict[str, Json]:
"""In-window mode: the smoke is the first start (candidate first); FAIL ends the window before any control start."""
result = smoke(backend, arm, clock=clock, budget_s=budget_s)
return {'schema': SMOKE_SCHEMA, 'mode': 'x-first', 'result': result,
'window': 'CONTINUE' if result['status'] == 'PASS' else 'END: smoke failed before any control start'}
def plan(experiment: str, arms: Sequence[str], mode: str) -> str:
return (f'DRY RUN smoke start ({mode}) for {experiment}: arms {", ".join(arms)}. Per arm: start_exp.sh (MEASURED 6.8 min), '
'/health + /v1/models, one greedy prompt, one 64-token seeded request (seed '
f'{SMOKE_SEED}; speculative rounds must move), stop the arm ALWAYS. ~8.5 min per arm, bound {SMOKE_BUDGET_S:g} s. '
'chain-head: failing candidates are dropped before their windows. x-first: a failure ends the window before any '
'control start. Execution uses the runner Live backend (--backend module:factory); this tool contacts nothing.')
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('experiment')
ap.add_argument('arm', nargs='+')
ap.add_argument('--mode', choices=('chain-head', 'x-first'), default='x-first')
ap.add_argument('--execute', action='store_true')
ap.add_argument('--backend', help="runner adapter 'module:factory'; factory(experiment, arm_list, out) -> Backend")
ap.add_argument('--out', type=Path)
args = ap.parse_args(argv)
if args.mode == 'x-first' and len(args.arm) != 1:
ap.error('x-first smokes exactly one candidate arm')
if not args.execute:
print(plan(args.experiment, args.arm, args.mode))
return 0
if os.environ.get('WINDOW_COORDINATOR') != COORDINATOR:
print(f'STOP: smoke start is coordinator-only (WINDOW_COORDINATOR={COORDINATOR})', file=sys.stderr)
return 2
if args.backend is None or ':' not in args.backend or args.out is None or args.out.exists():
print('STOP: --execute needs --backend module:factory and a fresh --out', file=sys.stderr)
return 2
module, factory = args.backend.split(':', 1)
backend = cast(Backend, getattr(importlib.import_module(module), factory)(args.experiment, list(args.arm), args.out))
if args.mode == 'chain-head':
result = chain_head(backend, args.arm)
passed = cast(list[Json], result['passed'])
else:
result = x_first(backend, args.arm[0])
passed = [args.arm[0]] if result['window'] == 'CONTINUE' else []
args.out.mkdir(parents=True)
text = json.dumps(result, indent=1, allow_nan=False) + '\n'
(args.out / 'smoke.json').write_text(text)
print(json.dumps({'mode': args.mode, 'passed': passed, 'sha256': hashlib.sha256(text.encode()).hexdigest()}))
return 0 if passed else 1
if __name__ == '__main__':
raise SystemExit(main())
|