Test Medium microbatch 256 against matched 512 control

This commit is contained in:
Mikei386
2026-09-20 21:25:23 +02:00
parent 2425c999b0
commit ba808e105b
24 changed files with 543 additions and 0 deletions
+2
View File
@@ -143,3 +143,5 @@ veröffentlicht werden.
Bei neuen Modelltests die [gesicherte Qwen-Referenz vom 20.09.2026](benchmarks/athena-qwen38-reference-20260920/README.md) verwenden. **Qwen nicht automatisch erneut benchmarken.** Das Paket enthält sieben Pure-/MIX-Fälle, Originalantworten, feste Requests, Modell-SHA256 und Laufzeit-/GPU-Konfiguration. Neue Kandidaten separat messen; notwendige Abweichungen dokumentieren. Nur bei begründetem Rekalibrierungsbedarf eine neue Referenzversion anlegen, bestehende Ergebnisse unverändert erhalten.
Abschluss des 20.09.: [Effizienz-/NVFP4-Prüfung](docs/ATHENA_EFFICIENCY_REVIEW_20260920.md). Pure/MIX bleiben produktiv; Medium startet nach abgefangenem GPU-Pufferfehler ohne Pipeline-Parallelisierung. Bei künftigem Tuning zuerst Speicherreserve und tatsächlichen Durchsatz prüfen. Native NVIDIA-NVFP4-Gewichte wurden nur auf Kapazität geprüft, nicht benchmarked.
[Microbatch-Folgetest](docs/MEDIUM_MICROBATCH_20260920.md): 256 lädt, unterschreitet aber die VRAM-Reserve (461 MiB frei auf 5080); keine Inferenzmessung mit 256. Produktiv bleibt 512, kein belegter Gewinn und keine automatische Folgeänderung.
@@ -41,3 +41,7 @@ Ein-Slot-Folgeversuche: [Bericht](DFLASH2_ONE_SLOT_20260920.md). Draft auf 5080
## Abschlussbewertung und nächste Priorität
[Effizienzcheck](ATHENA_EFFICIENCY_REVIEW_20260920.md): Pure/MIX bleiben produktiv. Medium fällt beim Start wegen fehlender Rechenpuffer auf Betrieb ohne Pipeline-Parallelisierung zurück. Ein späterer Punkt-2-Test sollte gezielt Microbatch/Speicherreserve und tatsächlichen Durchsatz vergleichen. Native NVIDIA-NVFP4-Dateien umfassen 20,42 GiB; nur Kapazität geprüft, kein neuer Inferenzversuch. Keine weiteren Tests automatisch eingeplant.
## Medium-Microbatch-Kurztest
[512 gegen 256](MEDIUM_MICROBATCH_20260920.md): 256 lädt ohne vorherige Pufferfehler-Meldung, lässt aber nur 461 MiB auf der 5080 frei. Schutzabbruch vor Inferenz; kein Geschwindigkeitsgewinn belegt. 512 wiederhergestellt. Für einen eventuellen Folgetest zuerst mehr 5080-Reserve durch angepassten Split schaffen.
+71
View File
@@ -0,0 +1,71 @@
# Medium: Microbatch 512 gegen 256 — Kurztest
20.09.2026. **256 lädt ohne den bisherigen Pufferfehler, hat aber zu wenig
Reserve für die festgelegte Testfreigabe. Deshalb keine produktive Umstellung
und noch kein Geschwindigkeitsvergleich für 256.**
Die echten Medium-Startargumente wurden vor dem Versuch aus Docker übernommen.
Zwischen beiden Testarmen unterschied sich nachweislich nur `--ubatch-size`.
Modell Pure, Kontext 160.000, zwei Slots mit gemeinsamem KV-Pool, Split 85:15,
Vision auf 3060, MTP3, Flash Attention, q4_0-K/V, Batch 2048 und sechs Threads
blieben gleich. Das Testendpoint war isoliert; TTS blieb auf 3060 resident.
## Ergebnis
| Prüfung | Microbatch 512 | Microbatch 256 |
|---|---|---|
| Modell geladen | ja | ja |
| Meldung: Rückfall ohne Pipeline-Parallelisierung | ja | nicht aufgetreten |
| 5080 belegt nach Laden | 15.704 MiB | 15.842 MiB |
| 5080 frei nach Laden | 599 MiB | **461 MiB** |
| 3060 belegt nach Laden, inklusive TTS | 10.918 MiB | 10.642 MiB |
| Mindestreserve von 512 MiB erfüllt | ja | **nein** |
| Inferenzaufgaben durchgeführt | ja | **nein, Schutzabbruch vorher** |
Bei 512 reproduziert sich der bekannte abgefangene Rechenpufferfehler mit
anschließendem Rückfall. Bei 256 fehlt diese Meldung; das Modell lädt erfolgreich.
Die höhere Belegung der 5080 trotz kleinerer Microbatch ist mit einem anderen
Rechenpuffer-/Pipeline-Pfad vereinbar. **Die Abwesenheit der Fehlermeldung allein
beweist jedoch weder aktive Pipeline-Auslastung noch einen Geschwindigkeitsgewinn.**
Der Abbruch bei 256 war eine Entscheidung des Testtreibers aufgrund der
festgelegten Reserve, kein CUDA-OOM und kein Modellabsturz. Die Grenze wurde
nicht abgesenkt, um doch noch eine Inferenz zu erzwingen. Auch die Reserve ist
keine Garantie, dass jede Langkontext-/Vision-Anfrage passt.
## Kleiner Kontrolllauf mit 512
| Aufgabe | Messwert |
|---|---:|
| Deutsche Ausgabe, 768 Tokens | 57,2 tok/s |
| Code-Ausgabe, 768 Tokens | 74,9 tok/s |
| Prefill, 24.674 Eingabetokens | 1.782,0 tok/s |
| Ausgabe nach dieser Eingabe, 512 Tokens | 54,8 tok/s |
| Drei eingebaute Fakten wiedergefunden | 3/3 |
Ein Lauf pro Aufgabe; kein umfassender Qualitätstest, kein Vollkontexttest und
kein Test zweier gleichzeitiger Anfragen. Die Vision-Komponente war geladen,
Bilder wurden nicht verarbeitet. TTFT wurde nicht separat gestreamt gemessen.
Die Anfragen verwenden das gespeicherte Text-/Sampling-Protokoll mit deaktiviertem
Promptcache; die Serverkonfiguration einschließlich RAM-Cache blieb unverändert.
Die 512er-Kontrolle ist ein kleiner neuer Vergleichspunkt für das konkrete
Medium-Profil und ersetzt oder verändert die bisherige Qwen-Referenz nicht.
## Entscheidung und möglicher nächster Schritt
**512 bleibt vorerst bestehen.** Ein alleiniger Wechsel auf 256 ist auf Basis
dieses Tests nicht freigegeben. Für einen weiteren Versuch müsste zuerst mehr
Reserve auf der 5080 geschaffen werden, etwa durch eine kleine Verschiebung
weiterer Qwen-Schichten auf die 3060. Dann müsste 256 tatsächlich gemessen werden;
auch dieser mögliche Schritt ist keine Zusage für einen Geschwindigkeitsgewinn.
Kein weiterer Test wurde automatisch gestartet oder eingeplant.
Das ursprüngliche Medium mit Microbatch 512 wurde wiederhergestellt. Medium,
Router, Controller, Gateway und TTS gesund; Router readiness und minimale
Modellantwort geprüft. Keine Xid-, Kernel-Panic-, OOM-Kill- oder
GPU-fallen-off-Meldungen im geprüften Kerneljournal seit Testbeginn. Kein
Host-Neustart und keine Treiber-/Kernel-/Netzwerkänderung.
Rohdaten, genaue Argumente, GPU-Samples, Logs und Wiederherstellungsnachweis:
`experiments/medium-microbatch-20260920/` im Repository und
`/data/benchmarks/medium-microbatch-20260920/` auf Athena.
@@ -0,0 +1,35 @@
# Medium microbatch quick A/B, 2026-09-20
User-authorized first-priority test: microbatch512 versus256, all other Medium
model settings unchanged. Three sequential requests per arm: frozen German and
code decode prompts (768-token caps), then frozen24576-target prefill/recall
(24674 actual prompt tokens,512 output-token cap). No reasoning in these speed
probes; same sampling/seeds as archived requests. Small matched512 control is
necessary because the frozen baseline has different slot/vision/context settings;
this is not a repeat of the broad Qwen benchmark or an overwrite of that baseline.
Supervisor snapshots actual production image/Args into production.json. Runner
copies them, changes only microbatch and isolated bind address/port, and sets
log verbosity3. Vision remains loaded on3060; context160000 shared by two slots,
Pure model,85:15 split,MTP3, q4_0 target KV, f16 MTP KV, batch2048, six CPU threads,
cache-RAM32768, TTS resident. Requests disable prompt cache for cold measurement.
No concurrency/vision task/full-context validation in this small speed probe.
Bounded600-second child; production requests drain before stopping the existing
router/controller/Medium. Isolated read-only model mount,26GiB memory cap with no
additional swap allowance,512MiB loaded VRAM reserve,85C/3GiB host RAM guard,
no core dumps. Supervisor restores the same production containers and waits for
healthy status. Kernel/network/driver untouched. Each result directory is unique
and cannot be overwritten. New scripts are not run automatically from checkout.
Check startup logs for successful allocation versus explicit fallback without
pipeline parallelism. Absence of a fallback line alone is not proof of actual
pipeline utilization; source/verbose logs may be needed to establish activation.
Judge walltime, prompt/decode rates, VRAM and the three-needle recall together.
## Outcome
512 control completed;256 loaded without the earlier fallback message but had
only461MiB free on5080, below the512MiB guard. No256 inference was executed.
Args differ only in --ubatch-size. Production512 restored and checked. See
../../docs/MEDIUM_MICROBATCH_20260920.md. Do not claim a256 speedup or quality result.
@@ -0,0 +1,10 @@
{
"only_model_argument_change": [
{
"index": 27,
"flag": "--ubatch-size",
"before": "512",
"after": "256"
}
]
}
@@ -0,0 +1,76 @@
{
"image": "sha256:5e3c12c145b8045e5731b44b6b97033f24b327ae3d4a3fa85ecdd159cc844907",
"args": [
"--model",
"/models/qwen3.8-27b-iq4-xs-pure/qwen3.8-27b-IQ4_XS-pure.gguf",
"--mmproj",
"/models/qwen/mmproj-BF16.gguf",
"--mmproj-offload",
"--mmproj-device",
"CUDA1",
"--alias",
"qwen-medium",
"--ctx-size",
"160000",
"--flash-attn",
"on",
"--cache-type-k",
"q4_0",
"--cache-type-v",
"q4_0",
"--cache-prompt",
"--cache-ram",
"32768",
"--threads",
"6",
"--threads-batch",
"6",
"--batch-size",
"2048",
"--ubatch-size",
"512",
"--parallel",
"2",
"--kv-unified",
"--jinja",
"--reasoning",
"auto",
"--reasoning-preserve",
"--host",
"0.0.0.0",
"--port",
"8080",
"--metrics",
"--fit",
"off",
"--n-gpu-layers",
"all",
"--load-mode",
"none",
"--no-ui",
"--temperature",
"1.0",
"--top-p",
"0.95",
"--top-k",
"20",
"--device",
"CUDA0,CUDA1",
"--main-gpu",
"0",
"--split-mode",
"layer",
"--tensor-split",
"85,15",
"--spec-type",
"draft-mtp",
"--spec-draft-n-max",
"3",
"--spec-draft-type-k",
"f16",
"--spec-draft-type-v",
"f16",
"--spec-draft-p-min",
"0.05"
]
}
@@ -0,0 +1,32 @@
[
{
"label": "medium-ub512",
"model": "pure",
"ctx": 160000,
"parallel": 2,
"single": false,
"split": "85,15",
"ubatch": 512,
"mtp": 3,
"vision": true,
"prompts": [
24576
],
"minimum_headroom_mib": 512
},
{
"label": "medium-ub256",
"model": "pure",
"ctx": 160000,
"parallel": 2,
"single": false,
"split": "85,15",
"ubatch": 256,
"mtp": 3,
"vision": true,
"prompts": [
24576
],
"minimum_headroom_mib": 512
}
]
@@ -0,0 +1,61 @@
{
"checked_at": 1789932247.2439013,
"uptime": "21:24:07 up 3 days, 10:19, 1 user, load average: 0.49, 0.39, 0.47",
"containers": {
"mike-ai-llama-medium": {
"running": true,
"health": "healthy",
"image": "sha256:5e3c12c145b8045e5731b44b6b97033f24b327ae3d4a3fa85ecdd159cc844907",
"started": "2026-09-20T19:23:24.996629585Z"
},
"mike-ai-router": {
"running": true,
"health": "healthy",
"image": "sha256:0448758bec6968b29263bcac0f8b4682c6d3029d626f7c12334698c11ef096bb",
"started": "2026-09-20T19:23:35.441398885Z"
},
"mike-ai-profile-controller": {
"running": true,
"health": "healthy",
"image": "sha256:a5f156d94c4921e671fafa524cf9c0fe91e1cbec0113c1f19c136204d63f243f",
"started": "2026-09-20T19:23:35.287332793Z"
},
"mike-ai-wireguard-gateway": {
"running": true,
"health": "healthy",
"image": "sha256:0d24e93c85a1c420b52b17666ede5fbd4672d92ab8dc28ea4ac5ba144ebc41d0",
"started": "2026-09-17T09:05:07.164533904Z"
},
"mike-ai-qwen3-tts": {
"running": true,
"health": "healthy",
"image": "sha256:b363a01d08b1bbecbfc3ca6f585368fae2cfdc591f9ecca6643738369f9a9d98",
"started": "2026-09-19T13:47:00.227813668Z"
}
},
"router": {
"/health": {
"status": "ok",
"router": "alive"
},
"/ready": {
"status": "ok",
"router": "alive",
"upstream": "ready"
}
},
"smoke": {
"content": "OK",
"usage": {
"completion_tokens": 2,
"prompt_tokens": 19,
"total_tokens": 21,
"prompt_tokens_details": {
"cached_tokens": 0
}
}
},
"kernel_errors": [],
"gpu": "name, memory.used [MiB], memory.total [MiB], temperature.gpu\nNVIDIA GeForce RTX 3060, 10920 MiB, 12288 MiB, 44\nNVIDIA GeForce RTX 5080, 15714 MiB, 16303 MiB, 52",
"disk": "Filesystem Size Used Avail Use% Mounted on\n/dev/nvme0n1p2 868G 187G 637G 23% /\n/dev/nvme1n1p1 916G 821G 49G 95% /data"
}
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Bounded, isolated Qwen quantization benchmark. Supervisor restores production."""
import json, pathlib, subprocess, sys, time, urllib.request, threading, signal
ROOT = pathlib.Path('/data/benchmarks/medium-microbatch-20260920')
NAME = 'mike-ai-microbatch-test'
BASE = 'http://127.0.0.1:5005'
GPU0 = 'GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe'
GPU1 = 'GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b'
IMAGE = 'sha256:5e3c12c145b8045e5731b44b6b97033f24b327ae3d4a3fa85ecdd159cc844907'
MODELS = {'mix':'qwen3.8-27b-iq4-mix/Qwen3.8-27B-IQ4-MIX.gguf', 'pure':'qwen3.8-27b-iq4-xs-pure/qwen3.8-27b-IQ4_XS-pure.gguf', 'byteshape':'byteshape-qwen38-gpu5/model.gguf'}
def cmd(*args, check=True, timeout=90):
r = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
if check and r.returncode: raise RuntimeError(str(args[:3])+': '+r.stderr[-2000:])
return r.stdout
def api(path, data=None, timeout=900):
req = urllib.request.Request(BASE+path, data=None if data is None else json.dumps(data).encode(), headers={'Content-Type':'application/json'})
with urllib.request.urlopen(req, timeout=timeout) as r: return json.load(r)
def save(path, data):
path.write_text(json.dumps(data, indent=2, ensure_ascii=False)+'\n')
def gpu():
rows = cmd('nvidia-smi','--query-gpu=name,memory.used,memory.total,temperature.gpu,utilization.gpu','--format=csv,noheader,nounits',timeout=15)
return [dict(zip(['name','used','total','temp','util'], [v.strip() for v in row.split(',')])) for row in rows.splitlines()]
def health_check():
rows=gpu()
if any(int(x['temp']) >= 85 for x in rows): raise RuntimeError('GPU temperature limit')
mem = dict((a.split(':')[0],int(a.split()[1])) for a in pathlib.Path('/proc/meminfo').read_text().splitlines())
if mem['MemAvailable'] < 3*1024*1024: raise RuntimeError('Host RAM reserve below 3 GiB')
return rows
def chat(prompt, max_tokens=512, effort='none', seed=42, tools=None):
health_check()
p={'model':'qwen-medium','messages':[{'role':'user','content':prompt}], 'max_tokens':max_tokens,'temperature':1.0,'top_p':0.95,'top_k':20,'min_p':0.0,'seed':seed,'reasoning_effort':effort,'cache_prompt':False}
if tools: p.update(tools=tools,tool_choice='auto')
start=time.monotonic(); r=api('/v1/chat/completions',p); r['wall_seconds']=time.monotonic()-start
health_check()
return r
def prefill(n, seed):
import gzip
payload=json.loads(gzip.decompress(pathlib.Path('/opt/mike-ai/stack/benchmarks/athena-qwen38-reference-20260920/frozen-requests.json.gz').read_bytes()))['prefill-'+str(n)]
r=chat(payload['messages'][0]['content'],payload['max_tokens'],seed=payload['seed'])
content=r['choices'][0]['message'].get('content','')
r['recall']={x:x in content for x in ['RAVEN-417','CEDAR-928','ORBIT-563']}
return r
def run_case(case):
label=case['label']; out=ROOT/label; out.mkdir(exist_ok=True)
if (out/'result.json').exists(): raise RuntimeError('Refusing to overwrite completed case '+label)
save(out/'config.json',case)
print('START',label,flush=True)
single=case.get('single',False)
production=json.loads((ROOT/'production.json').read_text())
assert production['image']==IMAGE
args=list(production['args'])
for flag,value in [('--ubatch-size',str(case['ubatch'])),('--host','127.0.0.1'),('--port','5005')]:
args[args.index(flag)+1]=value
args += ['--verbosity','3']
save(out/'server-args.json',args)
cmd('docker','run','-d','--name',NAME,'--gpus','all','--network','host','--read-only','--tmpfs','/tmp:rw,nosuid,nodev,size=256m','--security-opt','no-new-privileges:true','--cap-drop','ALL','--pids-limit','512','--ulimit','core=0','--memory','26g','--memory-swap','26g','--shm-size','1g','--log-opt','max-size=32m','--log-opt','max-file=1','-e','NVIDIA_VISIBLE_DEVICES='+GPU0+','+GPU1,'-e','NVIDIA_DRIVER_CAPABILITIES=compute,utility','-v','/data/models:/models:ro',IMAGE,*args)
stop=threading.Event(); samples=[]
def monitor():
while not stop.wait(2):
try:
rows=health_check()
samples.append({'time':time.time(),'gpus':rows})
except RuntimeError as e:
samples.append({'error':str(e),'aborted':True})
cmd('docker','stop','-t','10',NAME,check=False)
return
except Exception as e: samples.append({'error':str(e)})
thread=threading.Thread(target=monitor,daemon=True); thread.start()
result={'case':case,'started':time.time()}
try:
for _ in range(150):
try:
if api('/health',timeout=3).get('status')=='ok': break
except Exception: pass
if cmd('docker','inspect',NAME,'--format','{{.State.Running}}').strip()!='true': raise RuntimeError('Test container exited during load')
time.sleep(2)
else: raise RuntimeError('Startup exceeded 300s')
result['idle_gpu']=health_check(); result['props']=api('/props'); result['slots']=api('/slots')
save(out/'loaded.json',result)
# Added after the 88:12 trial: model loading alone can succeed while
# the first real attention graph still needs more CUDA workspace.
minimum=case.get('minimum_headroom_mib',512)
used_devices=['5080'] if single else ['5080','3060']
for g in result['idle_gpu']:
if any(device in g['name'] for device in used_devices):
free=int(g['total'])-int(g['used'])
if free<minimum:
raise RuntimeError(f"Insufficient loaded VRAM reserve on {g['name']}: {free} < {minimum} MiB; refusing inference")
result['smoke']=chat('Antworte nur mit OK.',8)
if case.get('quality'):
tasks=json.loads(pathlib.Path('/opt/mike-ai/stack/dev/QWEN38-FINAL-ACCEPTANCE-v1.json').read_text())
result['quality']=[]
for task in tasks:
ans=chat(task['prompt'],task['max_tokens'],'medium')
result['quality'].append({'id':task['id'],'response':ans})
save(out/'partial.json',result); print(label,task['id'],round(ans['wall_seconds'],1),flush=True)
tool={'type':'function','function':{'name':'read_server_status','description':'Read-only server status lookup','parameters':{'type':'object','properties':{'server':{'type':'string'}},'required':['server'],'additionalProperties':False}}}
result['tool']=chat('Read the current status of server alpha. Use the provided tool exactly once and do not invent its result.',512,tools=[tool])
if case.get('quality_followup'):
tasks=json.loads(pathlib.Path('/opt/mike-ai/stack/dev/QWEN38-FINAL-ACCEPTANCE-v1.json').read_text())
result['quality_followup']=[]
for task in tasks:
if task['id'] not in ['i3_code_debugging','i4_capacity_planning','i6_state_vs_configuration']: continue
ans=chat(task['prompt'],8192,'medium',seed=43)
result['quality_followup'].append({'id':task['id'],'seed':43,'budget':8192,'response':ans})
save(out/'partial.json',result); print(label,'followup',task['id'],round(ans['wall_seconds'],1),flush=True)
if not case.get('load_only'):
result['decode']=[]
prompts=['Erkläre ausführlich auf Deutsch, wie ein Reverse Proxy funktioniert, welche Fehler bei Container-IP-Wechseln auftreten können und wie man sie anhand von Logs eingrenzt. Schreibe mindestens 600 Wörter.', 'Write a Python implementation of an asynchronous first_success function: start all awaitables concurrently, return the first successful result, cancel and await remaining tasks, collect exceptions if all fail. Include an explanation and usage example.']
for i,p in enumerate(prompts):
result['decode'].append(chat(p,768,seed=42+i))
save(out/'partial.json',result)
print(label,'decode',i,result['decode'][-1].get('timings'),flush=True)
result['prefill']=[]
for n in case.get('prompts',[4096,16384]):
r=prefill(n,42); result['prefill'].append({'target':n,'response':r}); save(out/'partial.json',result)
print(label,'prefill',n,r.get('timings'),flush=True)
result['finished']=time.time()
except Exception as exc:
result['error']=str(exc)
raise
finally:
stop.set(); thread.join(5)
r=subprocess.run(['docker','logs',NAME],capture_output=True,text=True,timeout=30)
(out/'server.log').write_text(r.stdout+r.stderr)
save(out/'gpu.json',samples); save(out/'result.json',result)
cmd('docker','rm','-f',NAME,check=False)
print('DONE',label,flush=True)
return result
def capacity_case(case):
"""Bounded growth from a previously working context, with 768 MiB reserve.
0.04 MiB/token exceeds the measured 512-ubatch steady-state slope.
A failed 114688/512 ByteShape startup revealed additional transient MTP
buffers, so reserve is deliberately larger than steady-state extrapolation.
Smaller ubatches start at an already working context, not a guessed OOM edge.
The final context is tested with an actual almost-full prompt.
"""
context=case['ctx']
previous=None
for attempt in range(8):
pilot={**case,'ctx':context,'label':case['label']+'-pilot-'+str(context),'load_only':True,'quality':False,'quality_followup':False}
result=run_case(pilot)
rows=[g for g in result['idle_gpu'] if '5080' in g['name']]
samples=json.loads((ROOT/pilot['label']/'gpu.json').read_text())
peak=max([int(rows[0]['used'])+32]+[int(g['used']) for s in samples for g in s.get('gpus',[]) if '5080' in g['name']])
free=int(rows[0]['total'])-peak
if free<768:
if previous is None: raise RuntimeError('Initial capacity pilot has insufficient reserve')
context=previous
break
growth=min(16384,int((free-768)/0.04)//1024*1024)
if growth<1024 or attempt==7 or context>=262144: break
previous=context
context=min(262144,context+growth)
final={**case,'ctx':context,'label':case['label']+'-validated-'+str(context),'load_only':False,'prompts':[49152,context-1024]}
return run_case(final)
if __name__=='__main__':
for case in json.loads(pathlib.Path(sys.argv[1]).read_text()):
if case.get('capacity_search'): capacity_case(case)
else: run_case(case)
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Stop only existing router/controller/model; always restore the same containers."""
import json, pathlib, subprocess, sys, time, signal
ROOT=pathlib.Path('/data/benchmarks/medium-microbatch-20260920')
NAMES=['mike-ai-router','mike-ai-profile-controller','mike-ai-llama-medium']
def run(*args,check=True,timeout=90):
return subprocess.run(args,capture_output=True,text=True,check=check,timeout=timeout)
def stop_signal(*_): raise RuntimeError('Supervisor interrupted')
signal.signal(signal.SIGTERM,stop_signal); signal.signal(signal.SIGINT,stop_signal)
# Refuse if the known production state has changed, or if requests are active.
for name in NAMES:
assert run('docker','inspect',name,'--format','{{.State.Running}}').stdout.strip()=='true',name
for attempt in range(60):
slots=json.loads(run('docker','exec',NAMES[-1],'curl','-fsS','http://127.0.0.1:8080/slots').stdout)
if not any(s['is_processing'] for s in slots): break
if attempt==0: print('WAIT production request active; no interruption',flush=True)
time.sleep(3)
else: raise RuntimeError('Production remained busy for 180s; no services stopped')
assert not run('docker','ps','-q','--filter','name=^mike-ai-microbatch-test$').stdout.strip(),'Existing experiment'
production=json.loads(run('docker','inspect',NAMES[-1]).stdout)[0]
(ROOT/'production.json').write_text(json.dumps({'image':production['Image'],'args':production['Args']},indent=2)+'\n')
child=None
try:
run('docker','stop','-t','30',*NAMES[:2])
# Drain requests already handed to the model, before unloading it.
for _ in range(120):
slots=json.loads(run('docker','exec',NAMES[-1],'curl','-fsS','http://127.0.0.1:8080/slots').stdout)
if not any(s['is_processing'] for s in slots): break
time.sleep(2)
else: raise RuntimeError('Model did not drain')
run('docker','stop','-t','30',NAMES[-1])
child=subprocess.Popen(['python3',str(ROOT/'run.py'),sys.argv[1]])
code=child.wait(timeout=600)
if code: raise RuntimeError('Benchmark failed: '+str(code))
finally:
if child is not None and child.poll() is None:
child.terminate()
try: child.wait(timeout=20)
except subprocess.TimeoutExpired: child.kill(); child.wait(timeout=10)
run('docker','rm','-f','mike-ai-microbatch-test',check=False)
run('docker','start',NAMES[-1])
for _ in range(150):
if run('docker','inspect',NAMES[-1],'--format','{{.State.Health.Status}}').stdout.strip()=='healthy': break
time.sleep(2)
else: raise RuntimeError('Restored Medium did not become healthy')
run('docker','start',NAMES[1],NAMES[0])
for _ in range(60):
statuses=[run('docker','inspect',name,'--format','{{.State.Health.Status}}').stdout.strip() for name in NAMES[:2]]
if all(status=='healthy' for status in statuses): break
time.sleep(2)
else: raise RuntimeError('Restored router/controller did not become healthy')
print('RESTORED existing medium/controller/router; all healthy',flush=True)
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Read-only restoration verification plus a two-token model smoke request."""
import json,pathlib,re,subprocess,time
ROOT=pathlib.Path('/data/benchmarks/medium-microbatch-20260920')
def run(*args):return subprocess.check_output(args,text=True,timeout=30)
report={'checked_at':time.time(),'uptime':run('uptime').strip(),'containers':{}}
for name in ['mike-ai-llama-medium','mike-ai-router','mike-ai-profile-controller','mike-ai-wireguard-gateway','mike-ai-qwen3-tts']:
d=json.loads(run('docker','inspect',name))[0]
report['containers'][name]={'running':d['State']['Running'],'health':d['State'].get('Health',{}).get('Status'),'image':d['Image'],'started':d['State']['StartedAt']}
assert d['State']['Running'],name
if name in ['mike-ai-llama-medium','mike-ai-router','mike-ai-profile-controller','mike-ai-wireguard-gateway']:
assert d['State'].get('Health',{}).get('Status')=='healthy',name
assert report['containers']['mike-ai-llama-medium']['image']=='sha256:5e3c12c145b8045e5731b44b6b97033f24b327ae3d4a3fa85ecdd159cc844907'
assert not run('docker','ps','-q','--filter','name=^mike-ai-microbatch-test$').strip()
probe='import urllib.request,json; print(json.dumps({p:json.load(urllib.request.urlopen("http://127.0.0.1:8081"+p,timeout=20)) for p in ["/health","/ready"]}))'
report['router']=json.loads(run('docker','exec','mike-ai-router','python','-c',probe))
payload={'model':'qwen-medium','messages':[{'role':'user','content':'Antworte ausschließlich mit OK.'}],'reasoning_effort':'none','max_tokens':8,'temperature':0}
r=json.loads(run('docker','exec','mike-ai-llama-medium','curl','-fsS','--max-time','20','-H','Content-Type: application/json','--data',json.dumps(payload),'http://127.0.0.1:8080/v1/chat/completions'))
report['smoke']={'content':r['choices'][0]['message'].get('content',''),'usage':r.get('usage')}
assert report['smoke']['content'].strip()=='OK',report['smoke']
started=min(json.loads(p.read_text())['started'] for p in ROOT.glob('*/result.json'))
journal=run('journalctl','-k','--since','@'+str(int(started)-60),'--no-pager')
pattern=re.compile(r'NVRM.*Xid|oom-kill|Out of memory: Killed process|Kernel panic|GPU has fallen off',re.I)
report['kernel_errors']=[line for line in journal.splitlines() if pattern.search(line)]
report['gpu']=run('nvidia-smi','--query-gpu=name,memory.used,memory.total,temperature.gpu','--format=csv').strip()
report['disk']=run('df','-h','/','/data').strip()
(ROOT/'restore-verification.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))
assert not report['kernel_errors'],'Kernel/GPU errors require review'