Allow readable profile names and configurable KV cache
This commit is contained in:
1 parent
fac4a12d52
commit
cbcd5a0131
7 files changed
+32
-14
No files matched your search
+3
-3
@@ -1,11 +1,11 @@
|
||||
window.AutoTestUI=(()=>{
|
||||
const e=x=>String(x??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
async function api(path,data){const r=await fetch('/api/v1/'+path,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const v=await r.json();if(!r.ok)throw Error(v.error||'Anfrage fehlgeschlagen');return v;}
|
||||
function html(){return `<section id="auto-test"><h2>Automatische Einpassung & Leistungstest</h2><p>Priorität: RTX 5080 allein → möglichst kleiner Anteil auf RTX 3060 → erst zuletzt CPU-Auslagerung. Nach den Grundverteilungen aller gewählten Kontextstufen wird die 5080 für passende Dual-GPU-Kandidaten in Ein-Layer-Schritten weiter gefüllt. Synthetische Tests belegen die Deck-Laufzeit exklusiv; API-Anfragen warten. Fremde GPU-Dienste werden nicht beendet.</p><form id="auto-form" class="card"><label>Heruntergeladenes Sprachmodell<select id="auto-model" required></select></label><label>Start-Kontext<select id="auto-start-context">${[2048,4096,8192,16384,32768,65536,131072,160000,192000,262144].map(x=>`<option ${x===8192?'selected':''}>${x}</option>`).join('')}</select></label><label>End-Kontext<select id="auto-context">${[2048,4096,8192,16384,32768,65536,131072,160000,192000,262144].map(x=>`<option ${x===32768?'selected':''}>${x}</option>`).join('')}</select></label><label><input id="auto-mtp" type="checkbox"> MTP testen (2 Draft-Token, p-min 0,05; kompatible Gewichte erforderlich)</label><p>Ein Slot, Batch 2048, Microbatch 128 / 64 / 256. Nur Kontextstufen im gewählten Bereich; pro Kandidat werden bis nahe an das volle Kontextbudget Eingabetoken geprüft und 64 Token erzeugt. Alle Grundverteilungen 5080 allein sowie 98:2, 95:5, 90:10, 85:15, 75:25 und 50:50 werden je Stufe vor der Layer-Feinsuche geprüft. Höchstens 300 Kandidaten oder zwei Stunden; bei Abbruch wird die Stufe als unvollständig markiert. Kein absichtliches Übergehen der Speicherreserve.</p><button id="auto-start">Auto-Test starten</button><button id="auto-stop" type="button" class="secondary">Abbrechen</button></form><p id="auto-message" role="alert"></p><p id="auto-phase" role="status"></p><p>Dies ist eine begrenzte Suche, kein Beweis des absoluten Optimums oder der Stabilität bei vollem Kontext und paralleler Last. Erfolgreiche Profile werden nur auf Wunsch gespeichert, nicht am Endpunkt freigegeben.</p><label>Name für das zu speichernde Profil<input id="auto-name" value="auto-llm" pattern="[a-zA-Z0-9][a-zA-Z0-9_-]{1,63}" maxlength="64"></label><div id="auto-results"></div></section>`;}
|
||||
function html(){return `<section id="auto-test"><h2>Automatische Einpassung & Leistungstest</h2><p>Priorität: RTX 5080 allein → möglichst kleiner Anteil auf RTX 3060 → erst zuletzt CPU-Auslagerung. Nach den Grundverteilungen aller gewählten Kontextstufen wird die 5080 für passende Dual-GPU-Kandidaten in Ein-Layer-Schritten weiter gefüllt. Synthetische Tests belegen die Deck-Laufzeit exklusiv; API-Anfragen warten. Fremde GPU-Dienste werden nicht beendet.</p><form id="auto-form" class="card"><label>Heruntergeladenes Sprachmodell<select id="auto-model" required></select></label><label>Start-Kontext<select id="auto-start-context">${[2048,4096,8192,16384,32768,65536,131072,160000,192000,262144].map(x=>`<option ${x===8192?'selected':''}>${x}</option>`).join('')}</select></label><label>End-Kontext<select id="auto-context">${[2048,4096,8192,16384,32768,65536,131072,160000,192000,262144].map(x=>`<option ${x===32768?'selected':''}>${x}</option>`).join('')}</select></label><label>KV-Cache K<select id="auto-cache-k"><option>q4_0</option><option>q8_0</option><option>f16</option></select></label><label>KV-Cache V<select id="auto-cache-v"><option>q4_0</option><option>q8_0</option><option>f16</option></select></label><label><input id="auto-mtp" type="checkbox"> MTP testen (2 Draft-Token, p-min 0,05; kompatible Gewichte erforderlich)</label><p>Ein Slot, Batch 2048, Microbatch 128 / 64 / 256. Nur Kontextstufen im gewählten Bereich; pro Kandidat werden bis nahe an das volle Kontextbudget Eingabetoken geprüft und 64 Token erzeugt. Alle Grundverteilungen 5080 allein sowie 98:2, 95:5, 90:10, 85:15, 75:25 und 50:50 werden je Stufe vor der Layer-Feinsuche geprüft. Höchstens 300 Kandidaten oder zwei Stunden; bei Abbruch wird die Stufe als unvollständig markiert. Kein absichtliches Übergehen der Speicherreserve.</p><button id="auto-start">Auto-Test starten</button><button id="auto-stop" type="button" class="secondary">Abbrechen</button></form><p id="auto-message" role="alert"></p><p id="auto-phase" role="status"></p><p>Dies ist eine begrenzte Suche, kein Beweis des absoluten Optimums oder der Stabilität bei vollem Kontext und paralleler Last. Erfolgreiche Profile werden nur auf Wunsch gespeichert, nicht am Endpunkt freigegeben.</p><label>Name für das zu speichernde Profil<input id="auto-name" value="auto-llm" maxlength="64"></label><div id="auto-results"></div></section>`;}
|
||||
function bind(){const root=document.querySelector('#auto-test'),el=x=>root.querySelector('#'+x);let job=null,last='';const msg=x=>{if(root.isConnected)el('auto-message').textContent=x;};
|
||||
api('catalog').then(s=>{if(root.isConnected)el('auto-model').innerHTML=s.entries.filter(x=>x.kind==='chat'&&x.profile_eligible&&x.file.endsWith('.gguf')).map(x=>`<option value="${e(x.id)}">${e(x.file)}</option>`).join('');}).catch(x=>msg(x.message));
|
||||
async function refresh(){try{const s=await api('auto-tests');if(!root.isConnected)return;job=s.job;const running=job?.state==='running';el('auto-start').disabled=running;el('auto-stop').disabled=!running;el('auto-phase').textContent=job?`${job.state} · ${job.phase} · ${job.attempts}/${job.max_candidates||96} Kandidaten · vollständig geprüfte Stufen: ${(job.completed_contexts||[]).map(x=>x.toLocaleString('de-DE')).join(', ')||'keine'}${job.error?' · '+job.error:''}${job.fine_search_note?' · '+job.fine_search_note:''}`:'Noch kein Auto-Test.';const rows=job?.results||[],signature=JSON.stringify(rows);if(last!==signature){last=signature;const best=rows.reduce((a,r,i)=>r.success&&(!a||r.timings.predicted_per_second>a.speed)?{i,speed:r.timings.predicted_per_second}:a,null);el('auto-results').innerHTML=rows.map((r,i)=>`<article class="card"><h3>${r.context.toLocaleString('de-DE')} Kontext · ${e(r.tier)} ${best?.i===i?'· schnellster gemessener Kandidat':''}</h3><p>Verteilung ${e(r.parameters.tensor_split.map(v=>Number(v.toFixed(3))).join(':')||'nur 5080')} · Microbatch ${r.parameters.ubatch}${r.primary_gpu_layers?` · geplant ${r.primary_gpu_layers} GPU-Layer auf der 5080 (inkl. Ausgabe/MTP gemäß GGUF)`:""}</p>${r.success?`<p>Prefill ${Number(r.timings.prompt_per_second).toFixed(1)} Token/s · Ausgabe ${Number(r.timings.predicted_per_second).toFixed(1)} Token/s · ${Number(r.timings.prompt_n||0).toLocaleString('de-DE')} / ${r.context.toLocaleString('de-DE')} Eingabe-Token geprüft (${(100*(r.tested_context_fraction||0)).toFixed(1)} %)</p><p>GPU-Layerlimit: ${r.memory_plan.gpu_layers===999?'alle':r.memory_plan.gpu_layers}</p><button data-save="${i}">Als neues Profil speichern</button>`:`<p>${r.failure_stage==='prediction'?'Speicherprognose abgelehnt · kein Modellstart':r.failure_stage==='model_start'?'Modellstart fehlgeschlagen · Ursache nicht als GPU-OOM bewiesen':'Messung fehlgeschlagen · Ursache nicht als GPU-OOM bewiesen'}: ${e(r.error)}</p>`}</article>`).join('');root.querySelectorAll('[data-save]').forEach(b=>b.onclick=async()=>{try{await api('auto-tests/save',{job_id:job.id,index:Number(b.dataset.save),name:el('auto-name').value});msg('Profil gespeichert.');}catch(x){msg(x.message);}});}}catch(x){msg(x.message);}finally{if(root.isConnected)setTimeout(refresh,2000);}}
|
||||
el('auto-form').onsubmit=async ev=>{ev.preventDefault();try{await api('auto-tests/start',{model_id:el('auto-model').value,start_context:Number(el('auto-start-context').value),max_context:Number(el('auto-context').value),mtp:el('auto-mtp').checked});msg('Auto-Test gestartet.');}catch(x){msg(x.message);}};
|
||||
async function refresh(){try{const s=await api('auto-tests');if(!root.isConnected)return;job=s.job;const running=job?.state==='running';el('auto-start').disabled=running;el('auto-stop').disabled=!running;el('auto-phase').textContent=job?`${job.state} · ${job.phase} · ${job.attempts}/${job.max_candidates||96} Kandidaten · vollständig geprüfte Stufen: ${(job.completed_contexts||[]).map(x=>x.toLocaleString('de-DE')).join(', ')||'keine'}${job.error?' · '+job.error:''}${job.fine_search_note?' · '+job.fine_search_note:''}`:'Noch kein Auto-Test.';const rows=job?.results||[],signature=JSON.stringify(rows);if(last!==signature){last=signature;const best=rows.reduce((a,r,i)=>r.success&&(!a||r.timings.predicted_per_second>a.speed)?{i,speed:r.timings.predicted_per_second}:a,null);el('auto-results').innerHTML=rows.map((r,i)=>`<article class="card"><h3>${r.context.toLocaleString('de-DE')} Kontext · ${e(r.tier)} ${best?.i===i?'· schnellster gemessener Kandidat':''}</h3><p>Verteilung ${e(r.parameters.tensor_split.map(v=>Number(v.toFixed(3))).join(':')||'nur 5080')} · Microbatch ${r.parameters.ubatch} · KV ${e(r.parameters.cache_type_k||"q4_0")}/${e(r.parameters.cache_type_v||"q4_0")}${r.primary_gpu_layers?` · geplant ${r.primary_gpu_layers} GPU-Layer auf der 5080 (inkl. Ausgabe/MTP gemäß GGUF)`:""}</p>${r.success?`<p>Prefill ${Number(r.timings.prompt_per_second).toFixed(1)} Token/s · Ausgabe ${Number(r.timings.predicted_per_second).toFixed(1)} Token/s · ${Number(r.timings.prompt_n||0).toLocaleString('de-DE')} / ${r.context.toLocaleString('de-DE')} Eingabe-Token geprüft (${(100*(r.tested_context_fraction||0)).toFixed(1)} %)</p><p>GPU-Layerlimit: ${r.memory_plan.gpu_layers===999?'alle':r.memory_plan.gpu_layers}</p><button data-save="${i}">Als neues Profil speichern</button>`:`<p>${r.failure_stage==='prediction'?'Speicherprognose abgelehnt · kein Modellstart':r.failure_stage==='model_start'?'Modellstart fehlgeschlagen · Ursache nicht als GPU-OOM bewiesen':'Messung fehlgeschlagen · Ursache nicht als GPU-OOM bewiesen'}: ${e(r.error)}</p>`}</article>`).join('');root.querySelectorAll('[data-save]').forEach(b=>b.onclick=async()=>{try{await api('auto-tests/save',{job_id:job.id,index:Number(b.dataset.save),name:el('auto-name').value});msg('Profil gespeichert.');}catch(x){msg(x.message);}});}}catch(x){msg(x.message);}finally{if(root.isConnected)setTimeout(refresh,2000);}}
|
||||
el('auto-form').onsubmit=async ev=>{ev.preventDefault();try{await api('auto-tests/start',{model_id:el('auto-model').value,start_context:Number(el('auto-start-context').value),max_context:Number(el('auto-context').value),mtp:el('auto-mtp').checked,cache_type_k:el('auto-cache-k').value,cache_type_v:el('auto-cache-v').value});msg('Auto-Test gestartet.');}catch(x){msg(x.message);}};
|
||||
el('auto-stop').onclick=()=>api('auto-tests/cancel',{}).then(()=>msg('Abbruch angefordert.')).catch(x=>msg(x.message));refresh();
|
||||
}return {html,bind};
|
||||
})();
|
||||
+5
-3
@@ -73,16 +73,18 @@ class AutoTests:
|
||||
def update(self,**kw):
|
||||
with self.lock:self.job.update(kw);self.persist()
|
||||
def start(self,data):
|
||||
if set(data) not in ({'model_id','max_context','mtp'},{'model_id','start_context','max_context','mtp'}):raise ValueError('Modell, Start- und Endkontext sowie MTP auswählen.')
|
||||
fields=set(data);base={'model_id','max_context','mtp'}
|
||||
if fields-base-{'start_context','cache_type_k','cache_type_v'} or not base<=fields or (('cache_type_k' in fields)!=('cache_type_v' in fields)):raise ValueError('Modell, Start- und Endkontext, MTP und beide KV-Cache-Typen auswählen.')
|
||||
start_context=data.get('start_context',2048)
|
||||
if type(start_context)!=int or start_context not in CONTEXT_STEPS or type(data['max_context'])!=int or data['max_context'] not in CONTEXT_STEPS or start_context>data['max_context'] or type(data['mtp'])!=bool:raise ValueError('Start- und Endkontext müssen gültige Stufen in aufsteigender Reihenfolge sein.')
|
||||
if data.get('cache_type_k','q4_0') not in ('f16','q8_0','q4_0') or data.get('cache_type_v','q4_0') not in ('f16','q8_0','q4_0'):raise ValueError('KV-Cache: nur f16, q8_0 oder q4_0 unterstützt.')
|
||||
model=self.worker.catalog.entry(data['model_id'])
|
||||
if model['kind']!='chat' or not model['file'].endswith('.gguf') or not model['profile_eligible']:raise ValueError('Ein heruntergeladenes Chat-GGUF auswählen.')
|
||||
devices=probe();primary=next((g for g in devices if '5080' in g['name']),None);secondary=next((g for g in devices if '3060' in g['name']),None)
|
||||
if not primary or not secondary:raise ValueError('Dieser Auto-Test benötigt RTX 5080 und RTX 3060. Keine stillschweigende andere GPU-Zuordnung.')
|
||||
with self.lock:
|
||||
if self.thread and self.thread.is_alive():raise ValueError('Ein Auto-Test läuft bereits.')
|
||||
self.cancel.clear();self.job=dict(id=uuid.uuid4().hex,state='running',phase='Wartet auf exklusive Modellreservierung',model_id=model['id'],model_file=model['file'],start_context=start_context,max_context=data['max_context'],mtp=data['mtp'],results=[],attempts=0,max_candidates=MAX_CANDIDATES,completed_contexts=[],started_at=time.time(),error=None);self.persist()
|
||||
self.cancel.clear();self.job=dict(id=uuid.uuid4().hex,state='running',phase='Wartet auf exklusive Modellreservierung',model_id=model['id'],model_file=model['file'],start_context=start_context,max_context=data['max_context'],mtp=data['mtp'],cache_type_k=data.get('cache_type_k','q4_0'),cache_type_v=data.get('cache_type_v','q4_0'),results=[],attempts=0,max_candidates=MAX_CANDIDATES,completed_contexts=[],started_at=time.time(),error=None);self.persist()
|
||||
self.thread=threading.Thread(target=self.run,args=(primary['uuid'],secondary['uuid']),daemon=True);self.thread.start()
|
||||
return self.status()
|
||||
def stop(self):
|
||||
@@ -120,7 +122,7 @@ class AutoTests:
|
||||
if self.cancel.is_set():raise InterruptedError()
|
||||
if self.job['attempts']>=self.job.get('max_candidates',MAX_CANDIDATES):raise TestBudget('Kandidatenlimit erreicht')
|
||||
if time.time()-self.job['started_at']>MAX_SECONDS:raise TestBudget('Zeitlimit erreicht')
|
||||
params={**{k:v[2] for k,v in SCHEMAS['chat'].items()},**CHAT_GPU_DEFAULTS,'context':context,'slots':1,'batch':2048,'ubatch':micro,'gpu_devices':devices,'split_mode':split,'tensor_split':ratio,'gpu_offload':offload,'mtp':self.job['mtp']}
|
||||
params={**{k:v[2] for k,v in SCHEMAS['chat'].items()},**CHAT_GPU_DEFAULTS,'context':context,'slots':1,'batch':2048,'ubatch':micro,'gpu_devices':devices,'split_mode':split,'tensor_split':ratio,'gpu_offload':offload,'mtp':self.job['mtp'],'cache_type_k':self.job.get('cache_type_k','q4_0'),'cache_type_v':self.job.get('cache_type_v','q4_0')}
|
||||
p=dict(id='auto-'+self.job['id'],revision=self.job['attempts']+1,name='deck-auto-test',kind='chat',model_id=self.job['model_id'],parameters=params)
|
||||
self.update(attempts=p['revision'],phase=f'{context} Kontext · {tier} · Microbatch {micro}')
|
||||
row=dict(context=context,tier=tier,parameters=params,success=False,primary_gpu_layers=layer_count)
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ class LlamaWorker:
|
||||
env.update(CUDA_VISIBLE_DEVICES=','.join(g['uuid'] for g in selected),HF_HUB_OFFLINE='1',OMP_NUM_THREADS=str(params['threads']))
|
||||
reserve_mode=params.get('gpu_reserve_mode','auto')
|
||||
margins=[0 if reserve_mode=='none' else params.get('gpu_reserve_mib',{}).get(g['uuid'],512) if reserve_mode=='manual' else max(512,round(g['total_mib']*.025)) for g in selected]
|
||||
common=['--model',str(model),'--ctx-size',str(params['context']),'--parallel',str(params['slots']),'--batch-size',str(params['batch']),'--ubatch-size',str(params['ubatch']),'--cache-type-k','q4_0','--cache-type-v','q4_0','--flash-attn','on','--split-mode',params['split_mode'],'--fit-target',','.join(map(str,margins))]
|
||||
common=['--model',str(model),'--ctx-size',str(params['context']),'--parallel',str(params['slots']),'--batch-size',str(params['batch']),'--ubatch-size',str(params['ubatch']),'--cache-type-k',params.get('cache_type_k','q4_0'),'--cache-type-v',params.get('cache_type_v','q4_0'),'--flash-attn','on','--split-mode',params['split_mode'],'--fit-target',','.join(map(str,margins))]
|
||||
if params['tensor_split']:common+=['--tensor-split',','.join(map(str,params['tensor_split']))]
|
||||
tool=str(directory/'build/bin/llama-fit-params')
|
||||
mtp_fit=['--deck-mtp'] if mtp else []
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
window.ProfilesUI=(()=>{
|
||||
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const labels={video_device:'Videopipeline',text_encoder_device:'Textencoder-Gerät',gpu_reserve_mode:'GPU-Reservemodus',gpu_reserve_mib:'GPU-Reserve (MiB)',vision_projector:'Vision-Projektor',vision_device:'Projektor-Gerät',gpu_offload:'GPU-Ausführung',mtp:'MTP',mtp_tokens:'MTP: maximale Draft-Token',mtp_min_p:'MTP: Mindestwahrscheinlichkeit',temperature:'Temperature',top_p:'Top-p',top_k:'Top-k',gpu_devices:'GPUs (in Reihenfolge)',split_mode:'GPU-Split',tensor_split:'GPU-Verteilung',context:'Gesamtes Kontextbudget (Token)',slots:'Parallele Slots',threads:'CPU-Threads',batch:'Batch-Größe',ubatch:'Microbatch-Größe',width:'Breite (Pixel)',height:'Höhe (Pixel)',steps:'Schritte',seed:'Seed (−1 = zufällig)',guidance:'Guidance / CFG',speed:'Sprechgeschwindigkeit',frames:'Bildanzahl',fps:'Bilder pro Sekunde'};
|
||||
const labels={video_device:'Videopipeline',text_encoder_device:'Textencoder-Gerät',gpu_reserve_mode:'GPU-Reservemodus',gpu_reserve_mib:'GPU-Reserve (MiB)',vision_projector:'Vision-Projektor',vision_device:'Projektor-Gerät',cache_type_k:'KV-Cache K',cache_type_v:'KV-Cache V',gpu_offload:'GPU-Ausführung',mtp:'MTP',mtp_tokens:'MTP: maximale Draft-Token',mtp_min_p:'MTP: Mindestwahrscheinlichkeit',temperature:'Temperature',top_p:'Top-p',top_k:'Top-k',gpu_devices:'GPUs (in Reihenfolge)',split_mode:'GPU-Split',tensor_split:'GPU-Verteilung',context:'Gesamtes Kontextbudget (Token)',slots:'Parallele Slots',threads:'CPU-Threads',batch:'Batch-Größe',ubatch:'Microbatch-Größe',width:'Breite (Pixel)',height:'Höhe (Pixel)',steps:'Schritte',seed:'Seed (−1 = zufällig)',guidance:'Guidance / CFG',speed:'Sprechgeschwindigkeit',frames:'Bildanzahl',fps:'Bilder pro Sekunde'};
|
||||
const html=()=>'<section id="live-profiles"><div class="section-heading"><div><h2>Deine Profile</h2><p>Auf Athena gespeichert. Eine Modelldatei kann mehrere Profile mit unterschiedlichen Parametern haben.</p></div><button id="profile-new">Neues Profil</button></div><p id="profile-message" role="status"></p><div id="profile-list" class="profile-list"></div><div id="profile-editor"></div></section>';
|
||||
async function api(path,data){const r=await fetch('/api/v1/'+path,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Anfrage fehlgeschlagen');return v;}
|
||||
function bind(kind,modelId){
|
||||
@@ -133,13 +133,13 @@ window.ProfilesUI=(()=>{
|
||||
panelSequence++;
|
||||
if(!models.length)return;const data=p||{id:null,revision:0,name:'',model_id:id||models[0].id,parameters:Object.fromEntries(Object.entries(schema).map(([k,v])=>[k,v[2]]))};
|
||||
if(!p&&kind==='image'&&models.find(m=>m.id===data.model_id)?.repo==='RunningHubAI/rh-flux.2-klein-9b-fp16-unet-2067980602644717569'){data.parameters.steps=4;data.parameters.guidance=1;}
|
||||
el('profile-editor').innerHTML=`<section class="card editor"><h2>${data.id?'Profil bearbeiten':'Profil anlegen'}</h2><form id="profile-form"><div class="form-grid"><label>API-Profilname<input name="name" required minlength="2" maxlength="64" pattern="[a-zA-Z0-9][a-zA-Z0-9_-]{1,63}" placeholder="z. B. qwen-image-test" value="${e(data.name)}"></label><label>Heruntergeladene Modelldatei<select name="model_id">${models.map(m=>`<option value="${m.id}" ${m.id===data.model_id?'selected':''}>${e(m.file)} · ${(m.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${Object.entries(schema).map(([k,[min,max,defaultValue]])=>`<label>${e(labels[k])}<input name="${k}" type="number" required min="${min}" max="${max}" step="${['guidance','speed','temperature','top_p','mtp_min_p'].includes(k)?'any':['width','height'].includes(k)?64:1}" value="${e(data.parameters[k]??defaultValue)}"></label>`).join('')}</div>${kind==='video'?videoDevices(data):''}${kind==='chat'?gpuEditor(data.parameters)+`<h3>Vision-Projektor</h3><label>Projektor<select name="vision_projector"><option value="">Kein Projektor zugeordnet · Vision aus</option>${projectors.map(x=>`<option value="${e(x.id)}" ${data.parameters.vision_projector===x.id?'selected':''}>${e(x.repo)} / ${e(x.file)}</option>`).join('')}</select></label><section id="projector-discovery" class="card"></section><label>Projektor ausführen auf<select name="vision_device"><option value="cpu">CPU</option>${gpus.map(g=>`<option value="${e(g.uuid)}" ${data.parameters.vision_device===g.uuid?'selected':''}>${e(g.name)}</option>`).join('')}</select></label><p>Passende mmproj.gguf unter Entdecken herunterladen – auch aus einem anderen Repository als das quantisierte Modell. Modell und Projektor müssen zusammenpassen; der Dateiname allein garantiert keine Kompatibilität. GPU-Ziel muss oben ausgewählt sein. GPU-Projektoren erhalten eine zusätzliche konservative Speicherreserve.</p><label><input name="mtp" type="checkbox" ${data.parameters.mtp?'checked':''}> MTP aktivieren (eingebautes Draft-Modul)</label><p class="small">Nur für GGUF-Modelle mit MTP-Gewichten und kompatible Builds. Zusätzlicher Speicherbedarf; Geschwindigkeit hängt vom Modell ab. Referenz Medium: 2 Draft-Token, Mindestwahrscheinlichkeit 0,05, Draft-KV f16.</p>`:''}<p class="note">Speichern startet kein Modell. Die Ausführung benötigt eine passende Laufzeit und gegebenenfalls zusätzliche Modelldateien. ${kind==='chat'?'Das Kontextbudget gilt insgesamt für alle Slots.':''}</p><button>Profil auf Athena speichern</button><button class="secondary" type="button" id="profile-close">Schließen</button><p id="profile-error" role="alert"></p></form></section>`;
|
||||
el('profile-editor').innerHTML=`<section class="card editor"><h2>${data.id?'Profil bearbeiten':'Profil anlegen'}</h2><form id="profile-form"><div class="form-grid"><label>API-Profilname<input name="name" required minlength="2" maxlength="64" placeholder="z. B. Qwen3.8 27B MEDIUM" value="${e(data.name)}"></label><label>Heruntergeladene Modelldatei<select name="model_id">${models.map(m=>`<option value="${m.id}" ${m.id===data.model_id?'selected':''}>${e(m.file)} · ${(m.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${Object.entries(schema).map(([k,[min,max,defaultValue]])=>`<label>${e(labels[k])}<input name="${k}" type="number" required min="${min}" max="${max}" step="${['guidance','speed','temperature','top_p','mtp_min_p'].includes(k)?'any':['width','height'].includes(k)?64:1}" value="${e(data.parameters[k]??defaultValue)}"></label>`).join('')}</div>${kind==='video'?videoDevices(data):''}${kind==='chat'?gpuEditor(data.parameters)+`<h3>KV-Cache</h3><div class="form-grid">${["k","v"].map(axis=>`<label>${axis.toUpperCase()}-Cache<select name="cache_type_${axis}">${["q4_0","q8_0","f16"].map(type=>`<option value="${type}" ${(data.parameters[`cache_type_${axis}`]||"q4_0")===type?"selected":""}>${type}</option>`).join("")}</select></label>`).join("")}</div><p class="small">q4_0 spart VRAM; q8_0 und f16 brauchen mehr Speicher. K und V werden getrennt eingestellt. MTP-Draft-KV bleibt separat f16.</p><h3>Vision-Projektor</h3><label>Projektor<select name="vision_projector"><option value="">Kein Projektor zugeordnet · Vision aus</option>${projectors.map(x=>`<option value="${e(x.id)}" ${data.parameters.vision_projector===x.id?'selected':''}>${e(x.repo)} / ${e(x.file)}</option>`).join('')}</select></label><section id="projector-discovery" class="card"></section><label>Projektor ausführen auf<select name="vision_device"><option value="cpu">CPU</option>${gpus.map(g=>`<option value="${e(g.uuid)}" ${data.parameters.vision_device===g.uuid?'selected':''}>${e(g.name)}</option>`).join('')}</select></label><p>Passende mmproj.gguf unter Entdecken herunterladen – auch aus einem anderen Repository als das quantisierte Modell. Modell und Projektor müssen zusammenpassen; der Dateiname allein garantiert keine Kompatibilität. GPU-Ziel muss oben ausgewählt sein. GPU-Projektoren erhalten eine zusätzliche konservative Speicherreserve.</p><label><input name="mtp" type="checkbox" ${data.parameters.mtp?'checked':''}> MTP aktivieren (eingebautes Draft-Modul)</label><p class="small">Nur für GGUF-Modelle mit MTP-Gewichten und kompatible Builds. Zusätzlicher Speicherbedarf; Geschwindigkeit hängt vom Modell ab. Referenz Medium: 2 Draft-Token, Mindestwahrscheinlichkeit 0,05, Draft-KV f16.</p>`:''}<p class="note">Speichern startet kein Modell. Die Ausführung benötigt eine passende Laufzeit und gegebenenfalls zusätzliche Modelldateien. ${kind==='chat'?'Das Kontextbudget gilt insgesamt für alle Slots.':''}</p><button>Profil auf Athena speichern</button><button class="secondary" type="button" id="profile-close">Schließen</button><p id="profile-error" role="alert"></p></form></section>`;
|
||||
if(kind==='chat'){const threads=el('profile-form').querySelector('[name=threads]').closest('label');const advanced=document.createElement('details');advanced.innerHTML='<summary>Erweitert: CPU-Threads</summary><p>CPU-Threads steuern CPU-Arbeit und Offloading, nicht die Anzahl der CUDA-Rechenkerne.</p>';advanced.append(threads);el('profile-form').insertBefore(advanced,el('profile-form').querySelector('.note'));}
|
||||
if(kind==='chat'){bindProjectorDiscovery();const f=el('profile-form');const reserveFields=()=>{for(const n of ['reserve_first','reserve_second'])f.elements[n].disabled=f.elements.gpu_reserve_mode.value!=='manual';};f.elements.gpu_reserve_mode.onchange=reserveFields;reserveFields();}
|
||||
if(kind==='video')bindVideoDevices(data);
|
||||
el('profile-close').onclick=()=>el('profile-editor').replaceChildren();
|
||||
el('profile-form').onsubmit=async event=>{event.preventDefault();const form=event.target,values=new FormData(form),button=form.querySelector('button:not([type=button])');button.disabled=true;
|
||||
try{const parameters=Object.fromEntries(Object.keys(schema).map(k=>[k,Number(values.get(k))]));if(kind==='video'){parameters.video_device=values.get('video_device');parameters.text_encoder_device=values.get('text_encoder_device');}if(kind==='chat'){parameters.vision_projector=values.get('vision_projector')||null;parameters.vision_device=values.get('vision_device');parameters.mtp=values.has('mtp');parameters.gpu_offload=values.get('gpu_offload');if(values.get('gpu_second')&&!values.get('gpu_first'))throw Error('Bitte zuerst die erste GPU auswählen.');if((data.parameters.gpu_devices||[]).length>2)throw Error('Dieses Profil enthält mehr als zwei GPUs; der Editor unterstützt derzeit zwei.');parameters.gpu_devices=[values.get('gpu_first'),values.get('gpu_second')].filter(Boolean);parameters.gpu_reserve_mode=values.get('gpu_reserve_mode');parameters.gpu_reserve_mib=parameters.gpu_reserve_mode==='manual'?Object.fromEntries([['gpu_first','reserve_first'],['gpu_second','reserve_second']].filter(([g])=>values.get(g)).map(([g,r])=>[values.get(g),Number(values.get(r))])):{};parameters.split_mode=values.get('split_mode');const raw=String(values.get('tensor_split')).trim();parameters.tensor_split=raw?raw.split(',').map(x=>x.trim()?Number(x):NaN):[];}await api('profiles/save',{id:data.id,revision:data.revision,name:values.get('name'),kind,model_id:values.get('model_id'),parameters});if(!root.isConnected)return;el('profile-editor').replaceChildren();await load();message('Profil auf Athena gespeichert. Kein Modell gestartet.');}
|
||||
try{const parameters=Object.fromEntries(Object.keys(schema).map(k=>[k,Number(values.get(k))]));if(kind==='video'){parameters.video_device=values.get('video_device');parameters.text_encoder_device=values.get('text_encoder_device');}if(kind==='chat'){parameters.vision_projector=values.get('vision_projector')||null;parameters.vision_device=values.get('vision_device');parameters.mtp=values.has('mtp');parameters.cache_type_k=values.get('cache_type_k');parameters.cache_type_v=values.get('cache_type_v');parameters.gpu_offload=values.get('gpu_offload');if(values.get('gpu_second')&&!values.get('gpu_first'))throw Error('Bitte zuerst die erste GPU auswählen.');if((data.parameters.gpu_devices||[]).length>2)throw Error('Dieses Profil enthält mehr als zwei GPUs; der Editor unterstützt derzeit zwei.');parameters.gpu_devices=[values.get('gpu_first'),values.get('gpu_second')].filter(Boolean);parameters.gpu_reserve_mode=values.get('gpu_reserve_mode');parameters.gpu_reserve_mib=parameters.gpu_reserve_mode==='manual'?Object.fromEntries([['gpu_first','reserve_first'],['gpu_second','reserve_second']].filter(([g])=>values.get(g)).map(([g,r])=>[values.get(g),Number(values.get(r))])):{};parameters.split_mode=values.get('split_mode');const raw=String(values.get('tensor_split')).trim();parameters.tensor_split=raw?raw.split(',').map(x=>x.trim()?Number(x):NaN):[];}await api('profiles/save',{id:data.id,revision:data.revision,name:values.get('name'),kind,model_id:values.get('model_id'),parameters});if(!root.isConnected)return;el('profile-editor').replaceChildren();await load();message('Profil auf Athena gespeichert. Kein Modell gestartet.');}
|
||||
catch(error){if(root.isConnected)el('profile-error').textContent=error.message;}finally{button.disabled=false;}
|
||||
};
|
||||
el('profile-editor').scrollIntoView({behavior:'smooth',block:'start'});
|
||||
|
||||
+3
-2
@@ -24,7 +24,7 @@ def video_parameters(params):
|
||||
raise ValueError('Video-Gerät muss eine GPU-UUID sein; CPU wird im Disk-Streaming-Adapter noch nicht unterstützt.')
|
||||
return params
|
||||
|
||||
CHAT_GPU_DEFAULTS={'gpu_devices':[], 'split_mode':'none', 'tensor_split':[], 'mtp':False,'gpu_offload':'auto','gpu_reserve_mode':'auto','gpu_reserve_mib':{},'vision_projector':None,'vision_device':'cpu'}
|
||||
CHAT_GPU_DEFAULTS={'gpu_devices':[], 'split_mode':'none', 'tensor_split':[], 'mtp':False,'gpu_offload':'auto','gpu_reserve_mode':'auto','gpu_reserve_mib':{},'vision_projector':None,'vision_device':'cpu','cache_type_k':'q4_0','cache_type_v':'q4_0'}
|
||||
CHAT_SAMPLING={'temperature':.8,'top_p':.95,'top_k':40,'mtp_tokens':2,'mtp_min_p':.05}
|
||||
|
||||
def chat_parameters(params):
|
||||
@@ -34,6 +34,7 @@ def chat_parameters(params):
|
||||
if not isinstance(reserve,dict) or any(k not in params['gpu_devices'] or type(v) is not int or not 0<=v<=32768 for k,v in reserve.items()):raise ValueError('GPU-Reserve muss je ausgewählter GPU zwischen 0 und 32768 MiB liegen.')
|
||||
if params['gpu_reserve_mode']=='manual' and (not params['gpu_devices'] or set(reserve)!=set(params['gpu_devices'])):raise ValueError('Manuelle Reserve erfordert einen Wert für jede ausgewählte GPU.')
|
||||
if params['gpu_offload'] not in ('auto','full'):raise ValueError('Ungültiger GPU-Auslagerungsmodus.')
|
||||
if params['cache_type_k'] not in ('f16','q8_0','q4_0') or params['cache_type_v'] not in ('f16','q8_0','q4_0'):raise ValueError('KV-Cache: nur f16, q8_0 oder q4_0 unterstützt.')
|
||||
if type(params['mtp']) is not bool:raise ValueError('MTP muss an oder aus sein.')
|
||||
if params['vision_projector'] is not None and (not isinstance(params['vision_projector'],str) or not re.fullmatch(r'[a-f0-9]{64}',params['vision_projector'])):raise ValueError('Ungültige Projektor-ID.')
|
||||
if params['vision_device']!='cpu' and params['vision_device'] not in params['gpu_devices']:raise ValueError('Projektor-GPU muss in der GPU-Auswahl enthalten sein.')
|
||||
@@ -124,7 +125,7 @@ class Profiles:
|
||||
if set(data)!={'id','revision','name','kind','model_id','parameters'}:raise ValueError('Ungültige Profilfelder.')
|
||||
kind=data['kind'];name=data['name'];params=data['parameters']
|
||||
if kind=='video':raise ValueError('Videoprofile werden nicht mehr in Deck verwaltet. Originale LTX-Oberfläche verwenden.')
|
||||
if not isinstance(kind,str) or kind not in SCHEMAS or not isinstance(name,str) or not re.fullmatch('[a-zA-Z0-9][a-zA-Z0-9_-]{1,63}',name):raise ValueError('API-Name: 2–64 Buchstaben, Ziffern, Bindestrich oder Unterstrich.')
|
||||
if not isinstance(kind,str) or kind not in SCHEMAS or not isinstance(name,str) or not (2<=len(name)<=64 and name[0].isalnum() and name[-1].isalnum() and re.fullmatch(r'[\w .-]+',name)):raise ValueError('API-Name: 2–64 Zeichen; Buchstaben, Ziffern, Leerzeichen, Punkt, Bindestrich oder Unterstrich. Anfang und Ende müssen Buchstabe oder Ziffer sein.')
|
||||
model=self.catalog.entry(data['model_id'])
|
||||
if model['repo']==QWEN_REPO and any(model['file'] in r['files'] for r in QWEN_COMPONENTS.values()):raise ValueError('Textencoder und VAE werden über Komponenten zugeordnet, nicht als Hauptmodell.')
|
||||
if model['kind']!=kind or not model['profile_eligible']:raise ValueError('Eine Gewichtsdatei dieses Bereichs auswählen, keine Konfiguration.')
|
||||
|
||||
@@ -22,6 +22,15 @@ class AutoTestsTests(unittest.TestCase):
|
||||
self.assertIn((75,25),tested_splits);self.assertIn((50,50),tested_splits)
|
||||
self.auto.save(dict(job_id=job['id'],index=0,name='saved'));self.profiles.save.assert_called_once()
|
||||
self.assertEqual(AutoTests(self.auto.path,self.profiles,self.worker,self.scheduler).job['state'],'complete')
|
||||
def test_kv_cache_choice_is_used_and_saved(self):
|
||||
self.auto.request=Mock(return_value={});self.auto.benchmark=Mock(return_value={'predicted_per_second':40})
|
||||
with patch('auto_test.probe',return_value=self.gpus):
|
||||
self.auto.start(dict(model_id='m',start_context=4096,max_context=4096,mtp=False,cache_type_k='q8_0',cache_type_v='f16'))
|
||||
self.auto.thread.join(3)
|
||||
row=next(r for r in self.auto.job['results'] if r['success'])
|
||||
self.assertEqual((row['parameters']['cache_type_k'],row['parameters']['cache_type_v']),('q8_0','f16'))
|
||||
self.auto.save(dict(job_id=self.auto.job['id'],index=self.auto.job['results'].index(row),name='KV Test'))
|
||||
self.assertEqual(self.profiles.save.call_args.args[0]['parameters']['cache_type_v'],'f16')
|
||||
def test_secondary_before_cpu(self):
|
||||
def plan(p,cancel):
|
||||
if p['parameters']['gpu_offload']=='full':raise InferenceError('does not fit')
|
||||
|
||||
@@ -39,7 +39,7 @@ class ManagementTests(unittest.TestCase):
|
||||
req.update(id=saved['id'],revision=1)
|
||||
self.profiles.save(req)
|
||||
restarted=Profiles(self.root/'profiles.json',self.catalog)
|
||||
self.assertEqual(restarted.status()['profiles'][0]['parameters'],dict(params,mtp=False,mtp_tokens=2,mtp_min_p=.05,gpu_offload='auto',gpu_reserve_mode='auto',gpu_reserve_mib={},vision_projector=None,vision_device='cpu'))
|
||||
self.assertEqual(restarted.status()['profiles'][0]['parameters'],dict(params,mtp=False,mtp_tokens=2,mtp_min_p=.05,gpu_offload='auto',gpu_reserve_mode='auto',gpu_reserve_mib={},vision_projector=None,vision_device='cpu',cache_type_k='q4_0',cache_type_v='q4_0'))
|
||||
for change in [dict(gpu_offload='unknown'),dict(mtp='yes'),dict(mtp_tokens=0),dict(mtp_tokens=True),dict(mtp_min_p=1.1),dict(gpu_devices=devices*2),dict(tensor_split=[85]),dict(split_mode='none'),dict(top_p=1.1),dict(temperature=float('nan')),dict(top_k=True),dict(tensor_split=[0,100]),dict(gpu_devices=['CUDA0','CUDA1'])]:
|
||||
with self.subTest(change=change),self.assertRaises(ValueError):self.profiles.save(dict(req,revision=2,parameters=dict(params,**change)))
|
||||
|
||||
@@ -49,8 +49,14 @@ class ManagementTests(unittest.TestCase):
|
||||
change=self.request();change.update(id=saved['id'],revision=saved['revision']);change['parameters']['width']=512;other.save(change)
|
||||
with self.assertRaises(ValueError):other.save(change)
|
||||
self.assertEqual(other.status()['profiles'][0]['parameters']['width'],512)
|
||||
def test_readable_api_profile_name(self):
|
||||
row=self.profiles.save(dict(self.request(),name='Qwen3.8 27B MEDIUM'))
|
||||
self.assertEqual(row['name'],'Qwen3.8 27B MEDIUM')
|
||||
self.assertEqual(Profiles(self.root/'profiles.json',self.catalog).status()['profiles'][0]['name'],row['name'])
|
||||
for bad in (' Qwen','Qwen ','Qwen/Medium','Qwen\nMedium'):
|
||||
with self.subTest(bad=bad),self.assertRaises(ValueError):self.profiles.save(dict(self.request(),name=bad))
|
||||
def test_profile_rejects_invalid_model_kind_parameters(self):
|
||||
for change in [dict(model_id='../secrets'),dict(kind='chat'),dict(name='bad name')]:
|
||||
for change in [dict(model_id='../secrets'),dict(kind='chat'),dict(name='bad/name')]:
|
||||
with self.subTest(change=change),self.assertRaises(ValueError):self.profiles.save(dict(self.request(),**change))
|
||||
for key,value in [('width',1000),('steps',True),('guidance',float('nan')),('seed',-2)]:
|
||||
req=self.request();req['parameters'][key]=value
|
||||
|
||||
Reference in new issue
Block a user