Configure video pipeline and text encoder GPUs with memory guidance
This commit is contained in:
1 parent
0ec51bcf6d
commit
71e8345606
7 files changed
+132
-22
No files matched your search
@@ -159,7 +159,7 @@ Das Rezept unterstützt keine beliebigen Quantisierungen, Comfy-INT8-Encoder ode
|
||||
|
||||
Unter **Video → Profile** genau ein Profil mit **Am API-Endpunkt freigegeben** auswählen, dann in der Übersicht **Video** drücken. Ein neuer Haken ersetzt die vorige Video-Freigabe; Abwählen entfernt sie. Die Übersicht zeigt nur den Profilnamen und die Modusschalter. Änderungen der Video-Freigabe erfordern den LLM-Modus. Deck schließt zuerst die GPU-Auftragsannahme, beendet eigene Chat-/Auto-Test-/Bild-/TTS-Arbeit und entlädt seinen llama.cpp-Prozess. Erst nach Freigabe der Reservierungen und Prüfung **aller** sichtbaren GPUs startet der eigene LTX-Worker. Fremde GPU-Prozesse werden niemals beendet; der Wechsel scheitert dann mit einer sichtbaren Meldung. **LLM** beendet den Video-Prozess einschließlich einer laufenden Generierung und öffnet die GPU-Auftragsannahme wieder. Das nächste Chat-Modell lädt bei Anfrage. Nach Deck-Neustart gilt LLM; es gibt keinen automatischen Videostart.
|
||||
|
||||
Die Video-Laufzeit lässt sich unter Einstellungen → Laufzeiten → Video installieren/abbrechen. Sie verwendet eine eigene venv, den in `video_runtime.py` gepinnten offiziellen LTX-Commit sowie `deploy/video-requirements.lock` (PyTorch CUDA 12.8, keine Host-Treiberänderung). Die Module und Lockdatei gehören zu beiden Installationspaketen. Der erste Adapter unterstützt das dokumentierte LTX-2.5-Distilled-BF16-Komponentenrezept. Beide GPUs sind für Deck exklusiv reserviert, die Berechnung erfolgt zunächst auf der RTX 5080 mit Disk-Streaming; die RTX 3060 bleibt frei. „Bereit“ heißt: persistenter Worker, Pipeline und Komponentenmetadaten vorbereitet. BF16-Gewichte werden bedarfsgerecht gestreamt, nicht vollständig im VRAM gehalten. Eine freie GPU garantiert keinen OOM-freien Auftrag.
|
||||
Die Video-Laufzeit lässt sich unter Einstellungen → Laufzeiten → Video installieren/abbrechen. Sie verwendet eine eigene venv, den in `video_runtime.py` gepinnten offiziellen LTX-Commit sowie `deploy/video-requirements.lock` (PyTorch CUDA 12.8, keine Host-Treiberänderung). Die Module und Lockdatei gehören zu beiden Installationspaketen. Der erste Adapter unterstützt das dokumentierte LTX-2.5-Distilled-BF16-Komponentenrezept. Beide GPUs sind für Deck exklusiv reserviert. Standardmäßig erfolgt die Berechnung auf der RTX 5080 mit Disk-Streaming; im Videoprofil kann der Textencoder separat der RTX 3060 zugeordnet werden. „Bereit“ heißt: persistenter Worker, Pipeline und Komponentenmetadaten vorbereitet. BF16-Gewichte werden bedarfsgerecht gestreamt, nicht vollständig im VRAM gehalten. Eine freie GPU garantiert keinen OOM-freien Auftrag.
|
||||
|
||||
Eigene Video-API auf demselben API-Port und mit demselben Bearer-Token wie Chat (keine vollständige OpenAI-Videos-Kompatibilitätszusage):
|
||||
|
||||
@@ -177,3 +177,11 @@ Video-Build-Voraussetzung: Python-Entwicklungsheader passend zur verwendeten Pyt
|
||||
Verifiziert auf Athena am 29.09.2026: offizielles LTX-2.5-Distilled-BF16-Profil mit zugeordneten Komponenten vorbereitet; exklusiver Modus sperrt Chat mit HTTP 503 / `video_mode_active`; synthetischer POST-Videoauftrag liefert HTTP 202, Statusabfrage und MP4-Download funktionieren. Ergebnis: 256×256, 9 dekodierbare Bilder, 24 FPS und Audiospur (12.802 Bytes). Nach Rückwechsel auf LLM war der Video-Prozess beendet und der GPU-Speicher wieder auf Treibergrundbelegung (3060: 1 MiB, 5080: 6 MiB). Der anfängliche Triton-Kompilierfehler wurde durch Python-Entwicklungsheader im Deck-Image behoben. Kein Produktivdienst wurde für diese Prüfung verändert. Dieser kleine Funktionstest ist keine Speicher- oder Geschwindigkeitsgarantie für größere Auflösungen/Längen.
|
||||
|
||||
Video-Oberfläche: Moduswechsel erfolgen ausschließlich in der Übersicht. Eine laufende Ladeanzeige zeigt Phase und verstrichene Zeit. Video → Testen meldet einen falschen Modus mit Link zur Übersicht und bestätigt das Absenden sofort. Video → Laufend zeigt ausschließlich Worker, aktuellen Auftrag und Ergebnis. Installationsaktionen liegen unter Einstellungen → Laufzeiten → Video. Die Auftragsanzeige wird alle 1,5 Sekunden aktualisiert; der Worker liefert Phasen, jedoch keine belastbaren Prozentwerte.
|
||||
|
||||
### Video: Geräte pro Komponente
|
||||
|
||||
Unter **Video → Profile → Bearbeiten → Komponenten und Geräte** die GPU für Videopipeline und Textencoder auswählen. Gespeichert werden stabile GPU-UUIDs (`video_device`, `text_encoder_device`), keine flüchtigen CUDA-Indizes. Alte Profile nutzen weiterhin automatisch bevorzugt die RTX 5080 und denselben Ort für den Textencoder. Auswahl einer nicht vorhandenen GPU führt beim Laden zu einem klaren Fehler statt stiller Umleitung. Änderungen werden erst nach Rückwechsel auf LLM und erneutem Wechsel auf Video übernommen.
|
||||
|
||||
Der gepinnte LTX-Adapter unterstützt einen separaten CUDA-PromptEncoder mit Disk-Streaming. Seine Video-/Audio-Embeddings und Attention-Maske werden vor der Diffusion auf die Pipeline-GPU übertragen. Video-VAE, Audio-VAE und Spatial-Upsampler bleiben an die Videopipeline gebunden. CPU-Ausführung sowie unabhängige Decoder-/Upsampler-Geräte sind derzeit ausdrücklich nicht freigeschaltet. Fremde GPU-Prozesse bleiben geschützt; Video bleibt exklusiv.
|
||||
|
||||
Speicherhinweise im Editor vergleichen die tatsächliche Dateigröße mit GPU-Kapazität und verfügbarer VRAM-Momentaufnahme. Sie sind keine Spitzenbedarfsmessung und keine Passgarantie. BF16-Dateien oberhalb der Kapazität benötigen Streaming; Auflösung, Bildanzahl und Aktivierungen verursachen weiteren Bedarf. Eine zweite GPU beschleunigt die sequenzielle Pipeline nicht automatisch.
|
||||
+31
-7
@@ -1,13 +1,13 @@
|
||||
window.ProfilesUI=(()=>{
|
||||
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const labels={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',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){
|
||||
const root=document.querySelector('#live-profiles'),el=id=>root.querySelector('#'+id);let rows=[],models=[],schema={},enabled=[],gpus=[],projectors=[],panelSequence=0;
|
||||
const root=document.querySelector('#live-profiles'),el=id=>root.querySelector('#'+id);let rows=[],models=[],schema={},enabled=[],gpus=[],projectors=[],entries=[],panelSequence=0;
|
||||
const message=t=>{if(root.isConnected)el('profile-message').textContent=t;};
|
||||
async function load(){try{const [p,c,h,ep]=await Promise.all([api('profiles'),api('catalog'),kind==='chat'?api('hardware').catch(()=>({gpus:[]})):Promise.resolve({gpus:[]}),api('endpoint')]);projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));enabled=ep.profiles.filter(x=>x.enabled).map(x=>x.id);gpus=h.gpus||[];if(!root.isConnected)return;rows=p.profiles.filter(x=>x.kind===kind);models=c.entries.filter(x=>x.kind===kind&&x.profile_eligible===true);schema=p.schemas[kind];el('profile-new').disabled=!models.length;
|
||||
el('profile-list').innerHTML=rows.map(p=>`<article class="card"><span class="pill">GESPEICHERT AUF ATHENA</span><h3>${e(p.name)}</h3><p class="hub-filename">${e(p.model?.file||'Modelldatei nicht verfügbar')}</p><p>${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(k==='gpu_reserve_mode'?({auto:'Automatisch',manual:'Manuell',none:'Keine zusätzliche Reserve'}[v]||v):k==='gpu_reserve_mib'?(Object.entries(v).map(([id,n])=>`${gpus.find(g=>g.uuid===id)?.name||id}: ${n} MiB`).join(', ')||'gemäß Reservemodus'):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(' · ')}</p>${CatalogUI.executionHTML(p.execution)}<details><summary>${p.runnable?(kind==='video'?'Laufzeit bereit · Video-Modus in Übersicht aktivieren':'Laufzeit bereit · lädt bei Anfrage'):'Was fehlt zur Ausführung?'}</summary>${p.blockers.map(t=>`<p>${e(t)}</p>`).join('')}</details><div class="card-actions"><label class="endpoint-choice"><input type="checkbox" data-publish="${p.id}" ${enabled.includes(p.id)?'checked':''} ${!p.runnable&&!enabled.includes(p.id)?'disabled':''}> Am API-Endpunkt freigegeben</label><button class="secondary" data-edit="${p.id}">Bearbeiten</button><button class="secondary" data-copy="${p.id}">Duplizieren</button><button class="secondary" data-delete="${p.id}">Löschen</button>${['image','video'].includes(kind)?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}</div></article>`).join('')||`<section class="empty"><h3>Noch keine Profile</h3><p>${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}</p></section>`;
|
||||
async function load(){try{const [p,c,h,ep]=await Promise.all([api('profiles'),api('catalog'),['chat','video'].includes(kind)?api('hardware').catch(()=>({gpus:[]})):Promise.resolve({gpus:[]}),api('endpoint')]);entries=c.entries;projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));enabled=ep.profiles.filter(x=>x.enabled).map(x=>x.id);gpus=h.gpus||[];if(!root.isConnected)return;rows=p.profiles.filter(x=>x.kind===kind);models=c.entries.filter(x=>x.kind===kind&&x.profile_eligible===true);schema=p.schemas[kind];el('profile-new').disabled=!models.length;
|
||||
el('profile-list').innerHTML=rows.map(p=>`<article class="card"><span class="pill">GESPEICHERT AUF ATHENA</span><h3>${e(p.name)}</h3><p class="hub-filename">${e(p.model?.file||'Modelldatei nicht verfügbar')}</p><p>${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(['video_device','text_encoder_device'].includes(k)?({auto:'Automatisch · RTX 5080 bevorzugt',same:'Wie Videopipeline'}[v]||gpus.find(g=>g.uuid===v)?.name||v):k==='gpu_reserve_mode'?({auto:'Automatisch',manual:'Manuell',none:'Keine zusätzliche Reserve'}[v]||v):k==='gpu_reserve_mib'?(Object.entries(v).map(([id,n])=>`${gpus.find(g=>g.uuid===id)?.name||id}: ${n} MiB`).join(', ')||'gemäß Reservemodus'):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(' · ')}</p>${CatalogUI.executionHTML(p.execution)}<details><summary>${p.runnable?(kind==='video'?'Laufzeit bereit · Video-Modus in Übersicht aktivieren':'Laufzeit bereit · lädt bei Anfrage'):'Was fehlt zur Ausführung?'}</summary>${p.blockers.map(t=>`<p>${e(t)}</p>`).join('')}</details><div class="card-actions"><label class="endpoint-choice"><input type="checkbox" data-publish="${p.id}" ${enabled.includes(p.id)?'checked':''} ${!p.runnable&&!enabled.includes(p.id)?'disabled':''}> Am API-Endpunkt freigegeben</label><button class="secondary" data-edit="${p.id}">Bearbeiten</button><button class="secondary" data-copy="${p.id}">Duplizieren</button><button class="secondary" data-delete="${p.id}">Löschen</button>${['image','video'].includes(kind)?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}</div></article>`).join('')||`<section class="empty"><h3>Noch keine Profile</h3><p>${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}</p></section>`;
|
||||
el('profile-list').insertAdjacentHTML('afterbegin',`<p>${kind==='image'?'Der feste API-Name athena-image verwendet das hier freigegebene Bildprofil und dessen Auflösung. Höchstens ein Bildprofil ist am API-Endpunkt freigegeben. Ein neuer Haken ersetzt die bisherige Auswahl.':['audio','stt'].includes(kind)?'Eingerichtete Sprachprofile können am API-Endpunkt freigegeben werden.':kind==='chat'?'Mehrere Sprachmodellprofile können gleichzeitig am API-Endpunkt freigegeben sein.':kind==='video'?'Genau ein Videoprofil kann am API-Endpunkt freigegeben sein. Ein neuer Haken ersetzt die bisherige Auswahl. Die Übersicht lädt dieses Profil beim Wechsel auf Video; API-Name: athena-video. Zum Ändern der Freigabe zuerst auf LLM wechseln.':'Für diesen Bereich ist noch keine ausführbare Laufzeit am Endpunkt angebunden.'} Freigegeben bedeutet nicht bereits in den GPU-Speicher geladen.</p>`);
|
||||
root.querySelectorAll('[data-publish]').forEach(b=>b.onchange=async()=>{const checked=b.checked;root.querySelectorAll('[data-publish]').forEach(x=>x.disabled=true);try{await api('endpoint/profile',{id:b.dataset.publish,enabled:checked});await load();message(kind==='video'?'Video-Freigabe gespeichert. Die Übersicht lädt dieses Profil beim Wechsel auf Video.':'Endpunkt-Freigabe gespeichert. Geladen wird erst bei einer API-Anfrage.');}catch(error){await load();message(error.message);}});
|
||||
root.querySelectorAll('[data-delete]').forEach(b=>b.onclick=async()=>{const p=rows.find(p=>p.id===b.dataset.delete);if(!confirm(`Profil „${p.name}“ löschen? Modelldateien und Komponenten bleiben in der Bibliothek.`))return;b.disabled=true;try{await api('profiles/delete',{id:p.id,revision:p.revision});panelSequence++;el('profile-editor').replaceChildren();await load();message('Profil gelöscht. Modelldateien und Komponenten bleiben erhalten.');}catch(error){message(error.message);b.disabled=false;}});
|
||||
@@ -45,7 +45,7 @@ window.ProfilesUI=(()=>{
|
||||
const mmproj=f=>/(?:^|[\/_-])mmproj(?:[\/_.-]|$)/i.test(f.name)&&f.name.toLowerCase().endsWith('.gguf');
|
||||
async function refreshLibrary(){
|
||||
const c=await api('catalog');if(!box.isConnected)return;
|
||||
const selected=select.value;projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));
|
||||
const selected=select.value;entries=c.entries;projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));
|
||||
select.innerHTML='<option value="">Kein Projektor zugeordnet · Vision aus</option>'+projectors.map(x=>`<option value="${e(x.id)}">${e(x.repo)} / ${e(x.file)}</option>`).join('');select.value=selected;
|
||||
return c;
|
||||
}
|
||||
@@ -90,16 +90,40 @@ window.ProfilesUI=(()=>{
|
||||
}
|
||||
form.elements.model_id.addEventListener('change',check);check();
|
||||
}
|
||||
function videoDevices(data){
|
||||
const params=data.parameters;
|
||||
const options=(key,fallback,title)=>`<label>${title}<select name="${key}"><option value="${fallback}">${fallback==='auto'?'Automatisch · RTX 5080 bevorzugt':'Wie Videopipeline'}</option>${gpus.map(g=>`<option value="${e(g.uuid)}" ${params[key]===g.uuid?'selected':''}>${e(g.name)} · ${Number.isFinite(g.total_mib)?(g.total_mib/1024).toFixed(1)+' GiB':'Speicher unbekannt'}</option>`).join('')}${params[key]&&!['auto','same',...gpus.map(g=>g.uuid)].includes(params[key])?`<option selected value="${e(params[key])}">Nicht verfügbar: ${e(params[key])}</option>`:''}<option disabled>CPU · vom aktuellen Adapter nicht unterstützt</option></select></label>`;
|
||||
return `<section class="card"><h3>Komponenten und Geräte</h3><div class="form-grid">${options('video_device','auto','Videomodell / Diffusion')}${options('text_encoder_device','same','Textencoder')}</div><p>Video-VAE, Audio-VAE und Spatial-Upsampler laufen derzeit auf der GPU der Videopipeline. Unabhängige Zuordnung dieser Komponenten ist noch nicht unterstützt.</p><div id="video-device-advice" role="status"></div><p>Disk-Streaming bleibt aktiv. Dateigröße ist keine VRAM-Messung: Aktivierungen, Auflösung und Bildanzahl benötigen zusätzlichen Speicher. Änderungen gelten nach erneutem Wechsel in den Videomodus.</p></section>`;
|
||||
}
|
||||
function bindVideoDevices(data){
|
||||
const form=el('profile-form'),target=el('video-device-advice');
|
||||
function refresh(){
|
||||
const main=models.find(m=>m.id===form.elements.model_id.value),encoder=main?.id===data.model_id?entries.find(m=>m.id===data.components?.text_encoder):null;
|
||||
const primary=form.elements.video_device.value==='auto'?(gpus.find(g=>g.name.includes('5080'))||[...gpus].sort((a,b)=>(b.total_mib||0)-(a.total_mib||0))[0]):gpus.find(g=>g.uuid===form.elements.video_device.value);
|
||||
const secondary=form.elements.text_encoder_device.value==='same'?primary:gpus.find(g=>g.uuid===form.elements.text_encoder_device.value);
|
||||
function assessment(label,file,gpu){
|
||||
if(!file)return `<p>${label}: Komponente noch nicht zugeordnet; Größenvergleich nicht möglich.</p>`;
|
||||
if(!gpu)return `<p>${label}: ausgewählte GPU nicht verfügbar. Hardware-Verbindung prüfen.</p>`;
|
||||
const size=file.size/1024**3,total=gpu.total_mib/1024,free=Number.isFinite(gpu.used_mib)&&Number.isFinite(gpu.total_mib)?(gpu.total_mib-gpu.used_mib)/1024:null;
|
||||
const valid=Number.isFinite(size)&&size>0&&Number.isFinite(total)&&total>0;
|
||||
return `<p><strong>${label} → ${e(gpu.name)}</strong><br>Datei: ${valid?size.toFixed(2)+' GiB':'unbekannt'} · VRAM gesamt: ${Number.isFinite(total)?total.toFixed(1)+' GiB':'unbekannt'} · aktuell frei: ${free===null?'unbekannt':free.toFixed(1)+' GiB'}. ${!valid?'Speichereignung unbekannt.':size>total?'Datei größer als gesamter VRAM: vollständiges Laden in unverändertem BF16 ist nicht vorgesehen; Streaming nötig.':free!==null&&size>free?'Datei größer als aktuell freier VRAM. Laufende Belegung kann sich beim Moduswechsel ändern.':'Dateigröße unter VRAM-Kapazität; tatsächlicher Spitzenbedarf noch nicht gemessen.'}</p>`;
|
||||
}
|
||||
target.innerHTML='<p><strong>Empfehlung:</strong> RTX 5080 für die Videopipeline. Textencoder zunächst auf derselben GPU lassen; RTX 3060 ist eine optionale getrennte Ausführung mit Streaming, keine Zusage für mehr Geschwindigkeit oder vollständige VRAM-Residenz.</p>'+assessment('Videomodell',main,primary)+assessment('Textencoder',encoder,secondary)+'<p>Hardwarewerte sind eine Momentaufnahme beim Öffnen der Profilansicht. Ein erfolgreicher Test mit deiner Auflösung bleibt erforderlich.</p>';
|
||||
}
|
||||
for(const key of ['video_device','text_encoder_device','model_id'])form.elements[key].addEventListener('change',refresh);
|
||||
refresh();
|
||||
}
|
||||
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]]))};
|
||||
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==='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" 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>`;
|
||||
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==='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.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'});
|
||||
|
||||
+12
-1
@@ -13,6 +13,15 @@ SCHEMAS={
|
||||
'stt':{},'music':{},'voice':{},
|
||||
'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)}
|
||||
}
|
||||
VIDEO_DEVICE_DEFAULTS={'video_device':'auto','text_encoder_device':'same'}
|
||||
def video_parameters(params):
|
||||
params={**VIDEO_DEVICE_DEFAULTS,**params}
|
||||
for key in VIDEO_DEVICE_DEFAULTS:
|
||||
value=params[key]
|
||||
if not isinstance(value,str) or not (value in (('auto',) if key=='video_device' else ('same',)) or re.fullmatch(r'GPU-[a-zA-Z0-9-]+',value)):
|
||||
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_SAMPLING={'temperature':.8,'top_p':.95,'top_k':40,'mtp_tokens':2,'mtp_min_p':.05}
|
||||
|
||||
@@ -75,6 +84,7 @@ class Profiles:
|
||||
with self.lock:
|
||||
rows=json.loads(json.dumps(self.rows))
|
||||
for p in rows:
|
||||
if p['kind']=='video':p['parameters']={**VIDEO_DEVICE_DEFAULTS,**p['parameters']}
|
||||
if p['kind']=='chat':p['parameters']={**CHAT_GPU_DEFAULTS,**CHAT_SAMPLING,**p['parameters']}
|
||||
try:
|
||||
p['model']=self.catalog.entry(p['model_id'])
|
||||
@@ -110,7 +120,8 @@ class Profiles:
|
||||
if params['vision_projector']:
|
||||
projector=self.catalog.entry(params['vision_projector'])
|
||||
if projector.get('role')!='vision_projector' or not projector['file'].endswith('.gguf'):raise ValueError('Eine heruntergeladene mmproj-GGUF-Datei auswählen.')
|
||||
if not isinstance(params,dict) or set(params)!=(set(SCHEMAS[kind]) | (set(CHAT_GPU_DEFAULTS) if kind=='chat' else set())):raise ValueError('Unvollständige oder unbekannte Profilparameter.')
|
||||
if kind=='video' and isinstance(params,dict):params=video_parameters(params)
|
||||
if not isinstance(params,dict) or set(params)!=(set(SCHEMAS[kind]) | (set(CHAT_GPU_DEFAULTS) if kind=='chat' else set(VIDEO_DEVICE_DEFAULTS) if kind=='video' else set())):raise ValueError('Unvollständige oder unbekannte Profilparameter.')
|
||||
for key,(lo,hi,_) in SCHEMAS[kind].items():
|
||||
value=params[key]
|
||||
floating=key in ('guidance','speed','temperature','top_p','mtp_min_p')
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections import namedtuple
|
||||
from contextlib import nullcontext
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock,patch
|
||||
from profiles import Profiles,SCHEMAS,video_parameters
|
||||
from video import video_devices
|
||||
from video_worker import EncoderOnDevice
|
||||
|
||||
class VideoDeviceTests(unittest.TestCase):
|
||||
def test_selection_is_by_uuid_not_physical_index(self):
|
||||
cards=[dict(uuid='GPU-second',name='RTX 3060',total_mib=12000),dict(uuid='GPU-first',name='RTX 5080',total_mib=16000)]
|
||||
main,encoder,ordered=video_devices({'parameters':{'text_encoder_device':'GPU-second'}},cards)
|
||||
self.assertEqual([g['uuid'] for g in ordered],['GPU-first','GPU-second'])
|
||||
self.assertEqual(video_devices({'parameters':{}},cards)[2],[main])
|
||||
self.assertEqual(video_devices({'parameters':{'video_device':'GPU-second'}},cards)[2],[encoder])
|
||||
with self.assertRaises(ValueError):video_devices({'parameters':{'text_encoder_device':'GPU-missing'}},cards)
|
||||
def test_parameters_persist_and_old_profiles_default_to_same_gpu(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
catalog=Mock();catalog.entry.return_value=dict(id='m',kind='video',repo='example/repo',file='model.safetensors',profile_eligible=True)
|
||||
store=Profiles(Path(d)/'profiles.json',catalog)
|
||||
params={k:v[2] for k,v in SCHEMAS['video'].items()}
|
||||
row=store.save(dict(id=None,revision=0,name='video-test',kind='video',model_id='m',parameters=params))
|
||||
self.assertEqual(row['parameters']['text_encoder_device'],'same')
|
||||
row['parameters'].update(video_device='GPU-first',text_encoder_device='GPU-second')
|
||||
store.save({k:row[k] for k in ('id','revision','name','kind','model_id','parameters')})
|
||||
self.assertEqual(Profiles(Path(d)/'profiles.json',catalog).rows[0]['parameters']['text_encoder_device'],'GPU-second')
|
||||
for value in ['cpu','cuda:1',None,{},'GPU-abc;bad']:
|
||||
with self.assertRaises(ValueError):video_parameters({'text_encoder_device':value})
|
||||
def test_encoder_moves_conditioning_back_and_preserves_optional_audio(self):
|
||||
result=namedtuple('Embedding','video_encoding audio_encoding attention_mask')
|
||||
video,mask=Mock(),Mock();encoder=Mock(return_value=[result(video,None,mask)])
|
||||
torch=Mock();torch.cuda.device.side_effect=lambda device:nullcontext()
|
||||
with patch.dict('sys.modules',{'torch':torch}):
|
||||
output=EncoderOnDevice(encoder,'cuda:1','cuda:0')(['synthetic'])
|
||||
torch.cuda.device.assert_called_once_with('cuda:1')
|
||||
video.to.assert_called_once_with('cuda:0');mask.to.assert_called_once_with('cuda:0')
|
||||
self.assertIsNone(output[0].audio_encoding)
|
||||
+2
-2
@@ -6,7 +6,7 @@ window.VideoUI=(()=>{
|
||||
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)}:{}),s=await r.json();if(!r.ok)throw Error(s.error||'Anfrage fehlgeschlagen');return s;}
|
||||
function workerState(s){
|
||||
if(s.state==='switching')return busy(`Wechsel zu ${labels[s.target_mode]||'anderem Modus'}`,`${s.switch_phase||'Laufende Aufträge beenden und Speicher freigeben'}${s.switch_started_at?' · '+elapsed(s.switch_started_at):''}`);
|
||||
return `<div class="video-notice ${s.error?'video-error':''}" role="status"><strong>${e(labels[s.state]||s.state)}</strong><p>GPU-Modus: ${e(labels[s.mode]||s.mode)}${s.profile_name?' · Geladen: '+e(s.profile_name):''}</p>${s.error?`<p role="alert">${e(s.error)}</p>`:''}</div>`;
|
||||
return `<div class="video-notice ${s.error?'video-error':''}" role="status"><strong>${e(labels[s.state]||s.state)}</strong><p>GPU-Modus: ${e(labels[s.mode]||s.mode)}${s.profile_name?' · Vorbereitet: '+e(s.profile_name):''}</p>${s.profile_name&&s.devices?`<p>Videopipeline: ${e(s.devices.video||'unbekannt')} · Textencoder: ${e(s.devices.text_encoder||'unbekannt')}</p>`:''}${s.error?`<p role="alert">${e(s.error)}</p>`:''}</div>`;
|
||||
}
|
||||
function jobState(job){
|
||||
if(!job)return '<p>Noch kein Video-Test gestartet.</p>';
|
||||
@@ -28,7 +28,7 @@ window.VideoUI=(()=>{
|
||||
}
|
||||
function html(view='test'){
|
||||
const test=view==='test',runtime=view==='runtime';
|
||||
return `<section id="video-runtime-page" data-video-view="${view}"><h2>${runtime?'LTX-Video-Laufzeit':test?'Video testen':'Video · Laufend'}</h2>${runtime?'<p>Eigene LTX-Python-Umgebung mit CUDA und Disk-Streaming auf der RTX 5080.</p><div id="video-install-status"></div><button id="video-install">Laufzeit installieren</button><button id="video-cancel">Installation abbrechen</button>':'<div id="video-worker-state"></div>'}<div id="video-message" role="status" aria-live="polite"></div>${test?`<section class="card"><p>Kurzer Text-zu-Video-Test mit dem freigegebenen Profil. Werte gelten nur für diesen Auftrag. Noch kein Bild-/Audio-Upload.</p><p id="video-mode-hint"></p><form id="video-test"><label>Prompt<textarea name="prompt" required maxlength="8000" rows="4" placeholder="Zum Beispiel: Ein roter Ball auf einem Holztisch, ruhige Kamera."></textarea></label><div class="video-test-fields">${[['width','Breite',256,256,1920,64],['height','Höhe',256,256,1088,64],['frames','Bilder (8n+1)',9,9,241,8],['fps','FPS',24,1,60,1],['seed','Seed',42,-1,2147483647,1]].map(([n,l,v,min,max,step])=>`<label>${l}<input type="number" name="${n}" value="${v}" min="${min}" max="${max}" step="${step}" required></label>`).join('')}</div><button id="video-generate" disabled>Video erzeugen</button></form></section>`:''}${!runtime?'<section class="card"><h3>Auftragsstatus</h3><div id="video-job" aria-live="polite"></div><div id="video-result"></div></section>':'<p>Video → Testen bietet einen kurzen Funktionstest; Video → Laufend zeigt den Worker und den aktuellen Auftrag.</p>'}</section>`;
|
||||
return `<section id="video-runtime-page" data-video-view="${view}"><h2>${runtime?'LTX-Video-Laufzeit':test?'Video testen':'Video · Laufend'}</h2>${runtime?'<p>Eigene LTX-Python-Umgebung mit CUDA und Disk-Streaming; Gerätezuordnung im Videoprofil.</p><div id="video-install-status"></div><button id="video-install">Laufzeit installieren</button><button id="video-cancel">Installation abbrechen</button>':'<div id="video-worker-state"></div>'}<div id="video-message" role="status" aria-live="polite"></div>${test?`<section class="card"><p>Kurzer Text-zu-Video-Test mit dem freigegebenen Profil. Werte gelten nur für diesen Auftrag. Noch kein Bild-/Audio-Upload.</p><p id="video-mode-hint"></p><form id="video-test"><label>Prompt<textarea name="prompt" required maxlength="8000" rows="4" placeholder="Zum Beispiel: Ein roter Ball auf einem Holztisch, ruhige Kamera."></textarea></label><div class="video-test-fields">${[['width','Breite',256,256,1920,64],['height','Höhe',256,256,1088,64],['frames','Bilder (8n+1)',9,9,241,8],['fps','FPS',24,1,60,1],['seed','Seed',42,-1,2147483647,1]].map(([n,l,v,min,max,step])=>`<label>${l}<input type="number" name="${n}" value="${v}" min="${min}" max="${max}" step="${step}" required></label>`).join('')}</div><button id="video-generate" disabled>Video erzeugen</button></form></section>`:''}${!runtime?'<section class="card"><h3>Auftragsstatus</h3><div id="video-job" aria-live="polite"></div><div id="video-result"></div></section>':'<p>Video → Testen bietet einen kurzen Funktionstest; Video → Laufend zeigt den Worker und den aktuellen Auftrag.</p>'}</section>`;
|
||||
}
|
||||
function runtime(){const view=document.querySelector('#view');if(view.querySelector('[data-video-view="runtime"]'))return;view.innerHTML=html('runtime');bind(view.firstElementChild);}
|
||||
function bind(root=document.querySelector('#video-runtime-page')){
|
||||
|
||||
@@ -11,18 +11,30 @@ import threading
|
||||
import time
|
||||
import uuid
|
||||
from image_test import probe,cgroup_headroom,GIB
|
||||
from profiles import video_recipe
|
||||
from profiles import video_recipe,video_parameters
|
||||
|
||||
def video_devices(profile,gpus):
|
||||
if not gpus:raise ValueError('Keine Video-GPU verfügbar.')
|
||||
params=video_parameters(profile.get('parameters',{}))
|
||||
def selected(value):
|
||||
found=next((g for g in gpus if g['uuid']==value),None)
|
||||
if not found:raise ValueError('Im Videoprofil ausgewählte GPU nicht verfügbar: '+value)
|
||||
return found
|
||||
gpu=(next((g for g in gpus if '5080' in g.get('name','')),None) or max(gpus,key=lambda g:g['total_mib'])) if params['video_device']=='auto' else selected(params['video_device'])
|
||||
encoder=gpu if params['text_encoder_device']=='same' else selected(params['text_encoder_device'])
|
||||
devices=[gpu] if encoder['uuid']==gpu['uuid'] else [gpu,encoder]
|
||||
return gpu,encoder,devices
|
||||
|
||||
class Video:
|
||||
def __init__(self,root,profiles,runtime,scheduler,stop_owned):
|
||||
self.root=Path(root);self.profiles=profiles;self.runtime=runtime;self.scheduler=scheduler;self.stop_owned=stop_owned
|
||||
self.lock=threading.RLock();self.mode='llm';self.state='idle';self.error=None;self.process=None;self.profile=None;self.job=None;self.thread=None;self.gpu=None
|
||||
self.lock=threading.RLock();self.mode='llm';self.state='idle';self.error=None;self.process=None;self.profile=None;self.job=None;self.thread=None;self.gpu=None;self.device_names={}
|
||||
self.switch_phase=None;self.switch_started_at=None;self.target_mode=None;self.closing=False;self.selected=None
|
||||
if (self.root/'selection.json').exists():self.selected=json.loads((self.root/'selection.json').read_text()).get('profile_id')
|
||||
def status(self):
|
||||
with self.lock:
|
||||
if self.process and self.process.poll() is not None and self.state=='ready':self.state='failed';self.error='Video-Worker beendet; RAM-OOM oder Laufzeitfehler möglich.'
|
||||
return dict(target_mode=self.target_mode,switch_phase=self.switch_phase,switch_started_at=self.switch_started_at,mode=self.mode,state=self.state,error=self.error,profile_name=self.profile['name'] if self.profile else None,profile_id=self.selected,loaded_profile_id=self.profile['id'] if self.profile else None,gpu=self.gpu,job=dict(self.job) if self.job else None,runtime=self.runtime.status(),memory_policy='Disk-Streaming auf RTX 5080; beide GPUs exklusiv reserviert. Gewichte werden bedarfsgerecht geladen.')
|
||||
return dict(devices=dict(self.device_names),target_mode=self.target_mode,switch_phase=self.switch_phase,switch_started_at=self.switch_started_at,mode=self.mode,state=self.state,error=self.error,profile_name=self.profile['name'] if self.profile else None,profile_id=self.selected,loaded_profile_id=self.profile['id'] if self.profile else None,gpu=self.gpu,job=dict(self.job) if self.job else None,runtime=self.runtime.status(),memory_policy='Disk-Streaming auf den im Profil gewählten GPUs; GPUs exklusiv reserviert. Gewichte werden bedarfsgerecht geladen.')
|
||||
def blockers(self,p):
|
||||
if not video_recipe(p.get('model') or {}):return ['Für diese Videovariante ist kein Worker angebunden.']
|
||||
errors=[]
|
||||
@@ -119,8 +131,7 @@ class Video:
|
||||
def load(self,profile):
|
||||
gpus=probe()
|
||||
if not gpus or any(g['processes'] for g in gpus):raise ValueError('GPU durch fremden Dienst belegt. Fremden Dienst zuerst freigeben; Deck beendet ihn nicht.')
|
||||
gpu=next((g for g in gpus if '5080' in g.get('name','')),None)
|
||||
if gpu is None:gpu=max(gpus,key=lambda g:g['total_mib'])
|
||||
gpu,encoder,devices=video_devices(profile,gpus)
|
||||
if gpu['free_mib']<12000:raise ValueError('Mindestens 12 GiB freier GPU-Speicher für den Video-Test erforderlich.')
|
||||
available=next((int(line.split()[1])*1024 for line in Path('/proc/meminfo').read_text().splitlines() if line.startswith('MemAvailable:')),0)
|
||||
if available<10*GIB:raise ValueError('Mindestens 10 GiB verfügbarer Host-RAM erforderlich.')
|
||||
@@ -129,12 +140,14 @@ class Video:
|
||||
def path(entry):return str((self.profiles.catalog.root/entry['id']/('model'+Path(entry['file']).suffix)).resolve())
|
||||
components={role:self.profiles._component(profile['model'],role,profile['components'][role]) for role in video_recipe(profile['model'])}
|
||||
config=dict(paths=dict(transformer_path=path(profile['model']),text_encoder_path=path(components['text_encoder']),video_vae_path=path(components['video_vae']),audio_vae_path=path(components['audio_vae'])),spatial_upsampler=path(components['spatial_upsampler']))
|
||||
config['text_encoder_device']='cuda:0' if len(devices)==1 else 'cuda:1'
|
||||
config['device_names']={'video':gpu['name'],'text_encoder':encoder['name']}
|
||||
env={k:v for k,v in os.environ.items() if not k.startswith(('HF_','DECK_','LLAMA_'))}
|
||||
env.update(CUDA_VISIBLE_DEVICES=gpu['uuid'],HF_HUB_OFFLINE='1',TRANSFORMERS_OFFLINE='1',OMP_NUM_THREADS='2',TOKENIZERS_PARALLELISM='false')
|
||||
env.update(CUDA_VISIBLE_DEVICES=','.join(g['uuid'] for g in devices),HF_HUB_OFFLINE='1',TRANSFORMERS_OFFLINE='1',OMP_NUM_THREADS='2',TOKENIZERS_PARALLELISM='false')
|
||||
self.root.mkdir(parents=True,exist_ok=True,mode=0o700);env.update(HOME=str(self.root),TMPDIR=str(self.root))
|
||||
python,_=self.runtime.paths()
|
||||
p=subprocess.Popen([str(python),str(Path(__file__).with_name('video_worker.py'))],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,bufsize=0,start_new_session=True,env=env)
|
||||
with self.lock:self.process=p;self.gpu=gpu['uuid']
|
||||
with self.lock:self.process=p;self.gpu=gpu['uuid'];self.device_names=dict(config['device_names'])
|
||||
if self.closing:self.stop_process();raise ValueError('Deck wird beendet.')
|
||||
with self.lock:self.switch_phase='Video-Worker laden und Komponenten prüfen'
|
||||
p.stdin.write((json.dumps(config)+'\n').encode());p.stdin.flush();result=self.read(p,180)
|
||||
|
||||
+19
-4
@@ -3,10 +3,16 @@ import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Keep stdout exclusively for bounded protocol replies, discard library output.
|
||||
protocol=os.fdopen(os.dup(sys.stdout.fileno()),'w',buffering=1)
|
||||
os.dup2(os.open(os.devnull,os.O_WRONLY),sys.stdout.fileno())
|
||||
def reply(value):protocol.write(json.dumps(value)+'\n')
|
||||
|
||||
class EncoderOnDevice:
|
||||
"""Encode on a separate GPU; move all returned conditioning to the diffusion GPU."""
|
||||
def __init__(self,encoder,source,target):self.encoder=encoder;self.source=source;self.target=target
|
||||
def __call__(self,*args,**kwargs):
|
||||
import torch
|
||||
with torch.cuda.device(self.source):outputs=self.encoder(*args,**kwargs)
|
||||
return [type(output)(*(value.to(self.target) if value is not None else None for value in output)) for output in outputs]
|
||||
|
||||
def main():
|
||||
import torch
|
||||
from ltx_pipelines.distilled import DistilledPipeline
|
||||
@@ -17,6 +23,11 @@ def main():
|
||||
config=json.loads(sys.stdin.readline())
|
||||
paths=ModelPaths.from_split(**config['paths'])
|
||||
pipeline=DistilledPipeline(model_paths=paths,spatial_upsampler_path=config['spatial_upsampler'],loras=(),device=torch.device('cuda:0'),offload_mode=OffloadMode.DISK)
|
||||
encoder_device=torch.device(config.get('text_encoder_device','cuda:0'))
|
||||
if encoder_device!=pipeline.device:
|
||||
from ltx_pipelines.utils.blocks import PromptEncoder
|
||||
encoder=PromptEncoder(paths,torch.bfloat16,encoder_device,offload_mode=OffloadMode.DISK)
|
||||
pipeline.prompt_encoder=EncoderOnDevice(encoder,encoder_device,pipeline.device)
|
||||
# Validate all split pack metadata without materializing BF16 weights in RAM.
|
||||
from ltx_core.text_encoders.gemma.gemma_assets import GemmaAssets
|
||||
GemmaAssets.load(config['paths']['text_encoder_path'])
|
||||
@@ -24,7 +35,7 @@ def main():
|
||||
def __init__(self,target,label):self.target=target;self.label=label
|
||||
def __getattr__(self,name):return getattr(self.target,name)
|
||||
def __call__(self,*args,**kwargs):
|
||||
reply({'state':'progress','phase':self.label})
|
||||
reply({'state':'progress','phase':self.label+(' · '+config.get('device_names',{}).get('text_encoder' if self.label.startswith('Textencoder') else 'video',''))})
|
||||
return self.target(*args,**kwargs)
|
||||
for attr,label in [('prompt_encoder','Textencoder · Prompt verarbeiten'),('stage','Videogewichte laden und Diffusion berechnen'),('upsampler','Video hochskalieren'),('video_decoder','Videobilder dekodieren'),('audio_decoder','Audiospur dekodieren')]:setattr(pipeline,attr,Phase(getattr(pipeline,attr),label))
|
||||
reply({'state':'ready'})
|
||||
@@ -37,5 +48,9 @@ def main():
|
||||
reply({'state':'complete'})
|
||||
except torch.OutOfMemoryError:reply({'state':'failed','error':'GPU-Speicher erschöpft (CUDA OOM). Auflösung oder Bildanzahl reduzieren.'});return
|
||||
except Exception:reply({'state':'failed','error':'LTX-Generierung fehlgeschlagen. Modellformat, Laufzeit und Speicher prüfen.'});return
|
||||
if __name__=='__main__':
|
||||
# Keep stdout exclusively for bounded protocol replies, discard library output.
|
||||
protocol=os.fdopen(os.dup(sys.stdout.fileno()),'w',buffering=1)
|
||||
os.dup2(os.open(os.devnull,os.O_WRONLY),sys.stdout.fileno())
|
||||
try:main()
|
||||
except Exception:reply({'state':'failed','error':'LTX-Profil konnte nicht vorbereitet werden. Komponenten oder Laufzeit inkompatibel.'})
|
||||
Reference in new issue
Block a user