${e(p.name)}
${e(p.model?.file||'Modelldatei nicht verfügbar')}
${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k])}: ${e(v)}`).join(' · ')}
Was fehlt zur Ausführung?
${p.blockers.map(t=>`${e(t)}
`).join('')}From 8d03a9a97901f01d448e1e92bb20fc1b9c98e1cc Mon Sep 17 00:00:00 2001
From: Mikei386 <44135113+Mikei386@users.noreply.github.com>
Date: Mon, 28 Sep 2026 19:51:49 +0200
Subject: [PATCH] Add GPU distribution and sampling controls to chat profiles
---
STUDIO.md | 23 +++++++++++++++++++++++
profiles-ui.js | 18 ++++++++++++------
profiles.py | 21 ++++++++++++++++++---
test_model_management.py | 15 +++++++++++++++
4 files changed, 68 insertions(+), 9 deletions(-)
diff --git a/STUDIO.md b/STUDIO.md
index c4af152..0e1eb7e 100644
--- a/STUDIO.md
+++ b/STUDIO.md
@@ -132,3 +132,26 @@ Validierung: 63 Tests; reale isolierte Generierungen 512 × 512 / 2 Schritte und
1024 × 1024 / 25 Schritte. Letztere dauerte ca. 99 Sekunden, PNG visuell geprüft,
GPU danach wieder frei. Nutzerprofile unverändert. Die einfache Profil-/Prompt-/
Ergebnisanordnung orientiert sich an LocalAIs ImageGen-Seite, ohne deren Code zu kopieren.
+
+
+### Chatprofile: GPU- und Sampling-Einstellungen
+
+Der Profileditor speichert Kontextbudget, Slots, Batch/Microbatch, Temperature,
+Top-p und Top-k sowie geordnete GPU-UUIDs, Split-Modus (`none`, `layer`, `row`)
+und relative Verteilungsgewichte. Zwei GPU-Auswahlen zeigen die echten Kartennamen;
+die Reihenfolge definiert die vorgesehenen logischen CUDA0/CUDA1-Geräte. UUIDs
+verhindern Verwechslungen mit wechselnden nvidia-smi-Indizes. CPU-Threads stehen
+unter „Erweitert“. Die Auswahl startet keinen Worker; die Chat-Ausführung ist
+weiterhin nicht angebunden. Die GPU-Verteilung ist keine Speicherzusage.
+
+Medium aus der Referenztabelle: Kontext 160000 insgesamt für zwei Slots,
+RTX 5080 zuerst, RTX 3060 danach, Layer-Split 85,15, Batch 2048, Microbatch 256,
+Temperature 1.0, Top-p 0.95, Top-k 20. Fast: 76800, ein Slot, nur RTX 5080,
+keine Aufteilung, Batch 2048, Microbatch 64, Temperature 0.2, Top-p 0.8, Top-k 20.
+
+API `profiles/save`: zusätzliche Chat-Parameter `gpu_devices` (geordnete UUID-Liste),
+`split_mode`, `tensor_split` (Zahlenliste), `temperature`, `top_p`, `top_k`.
+Leere Geräte-/Gewichtelisten bedeuten automatische Auswahl/Verteilung.
+Alte Profile bleiben lesbar; fehlende neue Felder erhalten bei Anzeige/Speichern
+Standardwerte (automatische Geräte, keine Aufteilung, Temperature 0.8, Top-p 0.95,
+Top-k 40). Bestehende Dateien werden beim Lesen nicht umgeschrieben.
diff --git a/profiles-ui.js b/profiles-ui.js
index d43a08c..dc3de7f 100644
--- a/profiles-ui.js
+++ b/profiles-ui.js
@@ -1,13 +1,13 @@
window.ProfilesUI=(()=>{
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
- const labels={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={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=()=>' Auf Athena gespeichert. Eine Modelldatei kann mehrere Profile mit unterschiedlichen Parametern haben. ${e(p.model?.file||'Modelldatei nicht verfügbar')} ${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k])}: ${e(v)}`).join(' · ')} ${e(t)} ${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'} ${e(p.model?.file||'Modelldatei nicht verfügbar')} ${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(k==='gpu_devices'?(v.length?v.map((id,i)=>`CUDA${i}: ${gpus.find(g=>g.uuid===id)?.name||id}`).join(', '):'Automatisch'):Array.isArray(v)?(v.join(', ')||'Automatisch'):v)}`).join(' · ')} ${e(t)} ${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'} Komponenten konnten nicht geladen werden. Öffne die Ansicht erneut.Deine Profile
${e(p.name)}
Was fehlt zur Ausführung?
${p.blockers.map(t=>`Noch keine Profile
${e(p.name)}
Was fehlt zur Ausführung?
${p.blockers.map(t=>`Noch keine Profile
Die Reihenfolge legt die logischen CUDA-Geräte des Profils fest. Für Medium: RTX 5080 zuerst, RTX 3060 danach, Layer-Split und 85,15. Leere Verteilung bedeutet automatisch. Dies ist keine Zusage, dass Modell und Kontext in den verfügbaren Speicher passen.
`; + } function editor(p=null,id=null){ 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]]))}; - el('profile-editor').innerHTML=`CPU-Threads steuern CPU-Arbeit und Offloading, nicht die Anzahl der CUDA-Rechenkerne.
';advanced.append(threads);el('profile-form').insertBefore(advanced,el('profile-form').querySelector('.note'));} 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');button.disabled=true; - try{await api('profiles/save',{id:data.id,revision:data.revision,name:values.get('name'),kind,model_id:values.get('model_id'),parameters:Object.fromEntries(Object.keys(schema).map(k=>[k,Number(values.get(k))]))});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==='chat'){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.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'}); diff --git a/profiles.py b/profiles.py index 813feb3..48696ec 100644 --- a/profiles.py +++ b/profiles.py @@ -7,11 +7,24 @@ import uuid from pathlib import Path SCHEMAS={ - 'chat':{'context':(512,2097152,8192),'slots':(1,16,1),'threads':(1,256,6),'batch':(1,8192,512),'ubatch':(1,8192,128)}, + 'chat':{'context':(512,2097152,8192),'slots':(1,16,1),'threads':(1,256,6),'batch':(1,8192,512),'ubatch':(1,8192,128),'temperature':(0,5,.8),'top_p':(0,1,.95),'top_k':(0,1000,40)}, 'image':{'width':(256,2048,1024),'height':(256,2048,1024),'steps':(1,100,25),'seed':(-1,2147483647,-1),'guidance':(0,30,1)}, 'audio':{'speed':(.25,4,1)}, 'video':{'width':(256,1920,768),'height':(256,1088,512),'frames':(1,241,33),'fps':(1,60,24),'steps':(1,100,20),'seed':(-1,2147483647,-1)} } +CHAT_GPU_DEFAULTS={'gpu_devices':[], 'split_mode':'none', 'tensor_split':[]} +CHAT_SAMPLING={'temperature':.8,'top_p':.95,'top_k':40} + +def chat_parameters(params): + params={**CHAT_GPU_DEFAULTS,**CHAT_SAMPLING,**params} + devices=params['gpu_devices'];split=params['tensor_split'] + if not isinstance(devices,list) or len(devices)>16 or any(not isinstance(v,str) or not re.fullmatch(r'GPU-[0-9a-fA-F-]{36}',v) for v in devices) or len(set(devices))!=len(devices):raise ValueError('GPUs müssen als eindeutige, geordnete GPU-UUIDs angegeben werden.') + if params['split_mode'] not in ('none','layer','row'):raise ValueError('Ungültiger GPU-Split-Modus.') + if not isinstance(split,list) or any(isinstance(v,bool) or not isinstance(v,(int,float)) or not 0