Add guided FLUX Klein component setup and tested image pipeline
This commit is contained in:
@@ -207,3 +207,13 @@ Für einen Versuch ohne CPU-Offloading zusätzlich „Vollständig auf GPU · so
|
|||||||
Während eines Downloads können bis zu 20 weitere Dateien eingereiht werden, auch kategorienübergreifend. Genau ein Transfer läuft; nach Abschluss, Fehler oder manuellem Abbruch startet der nächste. Doppelte Dateien werden abgewiesen. Unter Downloads lassen sich wartende Einträge entfernen. Die Speicherprüfung berücksichtigt noch ausstehende Bytes aller geplanten Transfers plus 10 GiB Reserve.
|
Während eines Downloads können bis zu 20 weitere Dateien eingereiht werden, auch kategorienübergreifend. Genau ein Transfer läuft; nach Abschluss, Fehler oder manuellem Abbruch startet der nächste. Doppelte Dateien werden abgewiesen. Unter Downloads lassen sich wartende Einträge entfernen. Die Speicherprüfung berücksichtigt noch ausstehende Bytes aller geplanten Transfers plus 10 GiB Reserve.
|
||||||
|
|
||||||
Die API liefert `queued`, `bytes_per_second` und `eta_seconds`. Geschwindigkeit basiert auf einem gleitenden Zeitfenster; ETA ist eine Schätzung und enthält keine abschließende Dateiverarbeitung. Vor ausreichenden Messwerten bleibt die Anzeige offen. Beim Serverneustart werden laufende und wartende Einträge als unterbrochen markiert; keine automatische Wiederaufnahme.
|
Die API liefert `queued`, `bytes_per_second` und `eta_seconds`. Geschwindigkeit basiert auf einem gleitenden Zeitfenster; ETA ist eine Schätzung und enthält keine abschließende Dateiverarbeitung. Vor ausreichenden Messwerten bleibt die Anzeige offen. Beim Serverneustart werden laufende und wartende Einträge als unterbrochen markiert; keine automatische Wiederaufnahme.
|
||||||
|
|
||||||
|
### FLUX.2 Klein 9B einrichten und testen
|
||||||
|
|
||||||
|
Unterstützt ist die Datei `Flux.2 Klein-9B_fp16_nsfw.safetensors` aus `RunningHubAI/rh-flux.2-klein-9b-fp16-unet-2067980602644717569`. Die geprüfte Datei enthält BF16-Gewichte (16,9 GiB). Andere FLUX-Dateien werden nicht allein anhand ihres Namens automatisch freigegeben.
|
||||||
|
|
||||||
|
Bildprofil anlegen → Komponenten → Alle fehlenden Dateien herunterladen → nach Abschluss Bibliothek aktualisieren → Zuordnung speichern → Testen. Die Komponenten stammen aus `Comfy-Org/flux2-klein-9B`: `split_files/text_encoders/qwen_3_8b_fp8mixed.safetensors` und `split_files/vae/flux2-vae.safetensors`. Wenn genau eine passende Datei vorhanden ist, wird sie vorausgewählt. Downloads nutzen die bestehende Queue. Laufzeitinstallation bleibt unter Einstellungen / Laufzeiten / Bildgenerierung verfügbar.
|
||||||
|
|
||||||
|
Neue FLUX-Profile starten mit 4 Schritten und Guidance 1. Bestehende Profile behalten ihre Werte. Pipeline: ComfyUI UNETLoader, Qwen3-FLUX2-Textencoder auf zweiter GPU, EmptyFlux2LatentImage, Flux2Scheduler und Euler, VAE. Der bestehende Low-VRAM-Modus lagert Teile des 16,9-GiB-Modells in den System-RAM aus; es wird nicht vollständige GPU-Belegung versprochen. Beide GPUs müssen frei sein; fremde Prozesse werden nicht gestoppt. Zunächst Text-zu-Bild bis 1024×1024; kein Image-to-Image-Editor für diese Integration.
|
||||||
|
|
||||||
|
Verifikation auf Athena: eigene ungespeicherte Testprofilkopie, 512×512, 4 Schritte, Guidance 1, Seed 123, neutrales Fahrradmotiv. Vollständiger Job circa 43 Sekunden einschließlich Start und Entladen. PNG visuell geprüft. 150 automatisierte Tests erfolgreich. Gespeicherte Benutzerprofile und produktiver Router nicht verändert.
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import folder_paths
|
|||||||
class DeckTextEncoderLoader:
|
class DeckTextEncoderLoader:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {'required':{'clip_name':(folder_paths.get_filename_list('text_encoders'),)}}
|
return {'required':{'clip_name':(folder_paths.get_filename_list('text_encoders'),)},'optional':{'family':(['qwen_image','flux2'],)}}
|
||||||
RETURN_TYPES=('CLIP',)
|
RETURN_TYPES=('CLIP',)
|
||||||
FUNCTION='load_clip'
|
FUNCTION='load_clip'
|
||||||
CATEGORY='Deck'
|
CATEGORY='Deck'
|
||||||
def load_clip(self,clip_name):
|
def load_clip(self,clip_name,family="qwen_image"):
|
||||||
if torch.cuda.device_count()!=2:raise RuntimeError('Deck requires the two reserved GPUs.')
|
if torch.cuda.device_count()!=2:raise RuntimeError('Deck requires the two reserved GPUs.')
|
||||||
device=torch.device('cuda:1')
|
device=torch.device('cuda:1')
|
||||||
path=folder_paths.get_full_path_or_raise('text_encoders',clip_name)
|
path=folder_paths.get_full_path_or_raise('text_encoders',clip_name)
|
||||||
clip=comfy.sd.load_clip(ckpt_paths=[path],embedding_directory=folder_paths.get_folder_paths('embeddings'),clip_type=comfy.sd.CLIPType.QWEN_IMAGE,
|
clip=comfy.sd.load_clip(ckpt_paths=[path],embedding_directory=folder_paths.get_folder_paths('embeddings'),clip_type=comfy.sd.CLIPType.FLUX2 if family=='flux2' else comfy.sd.CLIPType.QWEN_IMAGE,
|
||||||
model_options={'load_device':device,'offload_device':torch.device('cpu'),'initial_device':torch.device('cpu')})
|
model_options={'load_device':device,'offload_device':torch.device('cpu'),'initial_device':torch.device('cpu')})
|
||||||
if clip.patcher.load_device!=device:raise RuntimeError('Textencoder could not be assigned to the reserved GPU.')
|
if clip.patcher.load_device!=device:raise RuntimeError('Textencoder could not be assigned to the reserved GPU.')
|
||||||
return (clip,)
|
return (clip,)
|
||||||
|
|||||||
+29
-11
@@ -11,7 +11,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import uuid
|
import uuid
|
||||||
from profiles import QWEN_REPO
|
from profiles import QWEN_REPO, FLUX_REPO, image_recipe
|
||||||
|
|
||||||
PYTHON=Path('/opt/deck-image-python/bin/python')
|
PYTHON=Path('/opt/deck-image-python/bin/python')
|
||||||
COMFY=Path('/opt/deck-comfy')
|
COMFY=Path('/opt/deck-comfy')
|
||||||
@@ -35,15 +35,32 @@ def probe():
|
|||||||
gpu,_=line.split(',',1);counts[gpu.strip()]=counts.get(gpu.strip(),0)+1
|
gpu,_=line.split(',',1);counts[gpu.strip()]=counts.get(gpu.strip(),0)+1
|
||||||
return [dict(uuid=u.strip(),name=n.strip(),total_mib=float(t),free_mib=float(f),processes=counts.get(u.strip(),0)) for u,n,t,f in (line.split(',') for line in rows.splitlines())]
|
return [dict(uuid=u.strip(),name=n.strip(),total_mib=float(t),free_mib=float(f),processes=counts.get(u.strip(),0)) for u,n,t,f in (line.split(',') for line in rows.splitlines())]
|
||||||
|
|
||||||
def select_gpus(devices,model_size,encoder_size,vae_size):
|
def select_gpus(devices,model_size,encoder_size,vae_size,offload=False):
|
||||||
needed=(model_size+vae_size+3*GIB)/1024**2
|
needed=(min(model_size,8*GIB)+vae_size+3*GIB if offload else model_size+vae_size+3*GIB)/1024**2
|
||||||
images=[g for g in devices if 'RTX 5080' in g['name'] and g['processes']==0 and g['free_mib']>needed]
|
images=[g for g in devices if 'RTX 5080' in g['name'] and g['processes']==0 and g['free_mib']>needed]
|
||||||
encoders=[g for g in devices if 'RTX 3060' in g['name'] and g['processes']==0 and g['free_mib']>(encoder_size+2*GIB)/1024**2]
|
encoders=[g for g in devices if 'RTX 3060' in g['name'] and g['processes']==0 and g['free_mib']>(encoder_size+2*GIB)/1024**2]
|
||||||
if not encoders:raise ValueError('Textencoder benötigt eine freie RTX 3060 mit ausreichend VRAM. Kein automatischer CPU-Fallback.')
|
if not encoders:raise ValueError('Textencoder benötigt eine freie RTX 3060 mit ausreichend VRAM. Kein automatischer CPU-Fallback.')
|
||||||
if not images:raise ValueError('Bildmodell benötigt eine freie RTX 5080 mit ausreichend VRAM. Deck stoppt keine anderen Modelle.')
|
if not images:raise ValueError('Bildmodell benötigt eine freie RTX 5080 mit ausreichend VRAM. Deck stoppt keine anderen Modelle.')
|
||||||
return max(images,key=lambda g:g['free_mib']),encoders[0]
|
return max(images,key=lambda g:g['free_mib']),encoders[0]
|
||||||
|
|
||||||
def workflow(prompt,params,seed):
|
def workflow(prompt,params,seed,family='qwen'):
|
||||||
|
if family=='flux':
|
||||||
|
return {
|
||||||
|
'1':{'class_type':'UNETLoader','inputs':{'unet_name':'model.safetensors','weight_dtype':'default'}},
|
||||||
|
'2':{'class_type':'DeckTextEncoderLoader','inputs':{'clip_name':'encoder.safetensors','family':'flux2'}},
|
||||||
|
'3':{'class_type':'VAELoader','inputs':{'vae_name':'vae.safetensors'}},
|
||||||
|
'4':{'class_type':'CLIPTextEncode','inputs':{'clip':['2',0],'text':prompt}},
|
||||||
|
'5':{'class_type':'EmptyFlux2LatentImage','inputs':{'width':params['width'],'height':params['height'],'batch_size':1}},
|
||||||
|
'6':{'class_type':'SamplerCustomAdvanced','inputs':{'noise':['9',0],'guider':['10',0],'sampler':['11',0],'sigmas':['12',0],'latent_image':['5',0]}},
|
||||||
|
'7':{'class_type':'VAEDecode','inputs':{'samples':['6',0],'vae':['3',0]}},
|
||||||
|
'8':{'class_type':'SaveImage','inputs':{'filename_prefix':'result','images':['7',0]}},
|
||||||
|
'9':{'class_type':'RandomNoise','inputs':{'noise_seed':seed}},
|
||||||
|
'10':{'class_type':'CFGGuider','inputs':{'model':['1',0],'positive':['4',0],'negative':['13',0],'cfg':params['guidance']}},
|
||||||
|
'11':{'class_type':'KSamplerSelect','inputs':{'sampler_name':'euler'}},
|
||||||
|
'12':{'class_type':'Flux2Scheduler','inputs':{'steps':params['steps'],'width':params['width'],'height':params['height']}},
|
||||||
|
'13':{'class_type':'ConditioningZeroOut','inputs':{'conditioning':['4',0]}}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'1':{'class_type':'UnetLoaderGGUF','inputs':{'unet_name':'model.gguf'}},
|
'1':{'class_type':'UnetLoaderGGUF','inputs':{'unet_name':'model.gguf'}},
|
||||||
'2':{'class_type':'DeckTextEncoderLoader','inputs':{'clip_name':'encoder.safetensors'}},
|
'2':{'class_type':'DeckTextEncoderLoader','inputs':{'clip_name':'encoder.safetensors'}},
|
||||||
@@ -97,17 +114,17 @@ class ImageTests:
|
|||||||
profile=next((p for p in self.profiles.status()['profiles'] if p['id']==profile_id),None)
|
profile=next((p for p in self.profiles.status()['profiles'] if p['id']==profile_id),None)
|
||||||
if not profile or profile['kind']!='image' or not profile['model']:raise ValueError('Bildprofil nicht verfügbar.')
|
if not profile or profile['kind']!='image' or not profile['model']:raise ValueError('Bildprofil nicht verfügbar.')
|
||||||
model=profile['model']
|
model=profile['model']
|
||||||
if model['repo']!=QWEN_REPO or not model['file'].endswith('.gguf'):raise ValueError('Der Bildtest unterstützt zunächst Qwen-Image-2.1 GGUF aus dem hinterlegten Rezept.')
|
if not image_recipe(model):raise ValueError('Für dieses Bildmodell fehlt ein Komponentenrezept.')
|
||||||
params=profile['parameters']
|
params=profile['parameters']
|
||||||
if params['width']>1024 or params['height']>1024:raise ValueError('Der isolierte Test unterstützt maximal 1024 × 1024 Pixel.')
|
if params['width']>1024 or params['height']>1024:raise ValueError('Der isolierte Test unterstützt maximal 1024 × 1024 Pixel.')
|
||||||
encoder=self.profiles._component(model,'text_encoder',profile.get('components',{}).get('text_encoder'))
|
encoder=self.profiles._component(model,'text_encoder',profile.get('components',{}).get('text_encoder'))
|
||||||
vae=self.profiles._component(model,'vae',profile.get('components',{}).get('vae'))
|
vae=self.profiles._component(model,'vae',profile.get('components',{}).get('vae'))
|
||||||
mem={line.split(':')[0]:int(line.split()[1])*1024 for line in Path('/proc/meminfo').read_text().splitlines() if line.startswith(('MemAvailable:','MemTotal:'))}
|
mem={line.split(':')[0]:int(line.split()[1])*1024 for line in Path('/proc/meminfo').read_text().splitlines() if line.startswith(('MemAvailable:','MemTotal:'))}
|
||||||
required_ram=max(20*GIB,encoder['size']*2+4*GIB)
|
required_ram=max(20*GIB,encoder['size']*2+4*GIB,model['size']+8*GIB if model['repo']==FLUX_REPO else 0)
|
||||||
headroom=cgroup_headroom()
|
headroom=cgroup_headroom()
|
||||||
if headroom is not None and headroom<required_ram:raise ValueError('Deck-RAM-Limit reicht für Textencoder und Arbeitsdaten nicht aus.')
|
if headroom is not None and headroom<required_ram:raise ValueError('Deck-RAM-Limit reicht für Textencoder und Arbeitsdaten nicht aus.')
|
||||||
if mem.get('MemAvailable',0)<required_ram+4*GIB:raise ValueError('Aktuell zu wenig freier System-RAM; produktive Dienste bleiben unverändert.')
|
if mem.get('MemAvailable',0)<required_ram+4*GIB:raise ValueError('Aktuell zu wenig freier System-RAM; produktive Dienste bleiben unverändert.')
|
||||||
gpu,encoder_gpu=select_gpus(probe(),model['size'],encoder['size'],vae['size']);self.root.mkdir(parents=True,exist_ok=True,mode=0o700)
|
gpu,encoder_gpu=select_gpus(probe(),model['size'],encoder['size'],vae['size'],offload=model['repo']==FLUX_REPO);self.root.mkdir(parents=True,exist_ok=True,mode=0o700)
|
||||||
if shutil.disk_usage(self.root).free<10*GIB:raise ValueError('Weniger als 10 GiB freier Plattenspeicher.')
|
if shutil.disk_usage(self.root).free<10*GIB:raise ValueError('Weniger als 10 GiB freier Plattenspeicher.')
|
||||||
job_id=uuid.uuid4().hex;seed=params['seed'] if params['seed']>=0 else secrets.randbelow(2147483648)
|
job_id=uuid.uuid4().hex;seed=params['seed'] if params['seed']>=0 else secrets.randbelow(2147483648)
|
||||||
self.cancel.clear();self.job=dict(id=job_id,state='running',phase='Bildlaufzeit startet',profile_id=profile_id,profile_name=profile['name'],started_at=time.time(),gpu=gpu['name'],encoder_gpu=encoder_gpu['name'],seed=seed,parameters=params)
|
self.cancel.clear();self.job=dict(id=job_id,state='running',phase='Bildlaufzeit startet',profile_id=profile_id,profile_name=profile['name'],started_at=time.time(),gpu=gpu['name'],encoder_gpu=encoder_gpu['name'],seed=seed,parameters=params)
|
||||||
@@ -117,7 +134,7 @@ class ImageTests:
|
|||||||
directory=self.root/job_id;process=None
|
directory=self.root/job_id;process=None
|
||||||
try:
|
try:
|
||||||
directory.mkdir(mode=0o700)
|
directory.mkdir(mode=0o700)
|
||||||
for role,item,filename in [('unet',model,'model.gguf'),('clip',encoder,'encoder.safetensors'),('vae',vae,'vae.safetensors')]:
|
for role,item,filename in [('unet',model,'model.safetensors' if model['repo']==FLUX_REPO else 'model.gguf'),('clip',encoder,'encoder.safetensors'),('vae',vae,'vae.safetensors')]:
|
||||||
dest=directory/'models'/role;dest.mkdir(parents=True);(dest/filename).symlink_to(self.model_path(item))
|
dest=directory/'models'/role;dest.mkdir(parents=True);(dest/filename).symlink_to(self.model_path(item))
|
||||||
for folder in ('output','temp','user','input'):(directory/folder).mkdir()
|
for folder in ('output','temp','user','input'):(directory/folder).mkdir()
|
||||||
custom=directory/'custom_nodes'/'deck_encoder';custom.mkdir(parents=True)
|
custom=directory/'custom_nodes'/'deck_encoder';custom.mkdir(parents=True)
|
||||||
@@ -151,10 +168,11 @@ class ImageTests:
|
|||||||
if time.monotonic()>deadline:raise ValueError('Bildlaufzeit wurde nicht rechtzeitig bereit.')
|
if time.monotonic()>deadline:raise ValueError('Bildlaufzeit wurde nicht rechtzeitig bereit.')
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
nodes=request('/object_info')
|
nodes=request('/object_info')
|
||||||
required={'UnetLoaderGGUF','DeckTextEncoderLoader','VAELoader','TextEncodeQwenImage21','EmptyLatentImage','KSampler','VAEDecode','SaveImage'}
|
graph=workflow(prompt,params,seed,'flux' if model['repo']==FLUX_REPO else 'qwen')
|
||||||
if not required.issubset(nodes):raise ValueError('Der installierten Bildlaufzeit fehlen erforderliche Qwen/GGUF-Nodes.')
|
required={node['class_type'] for node in graph.values()}
|
||||||
|
if not required.issubset(nodes):raise ValueError('Der installierten Bildlaufzeit fehlen erforderliche Nodes für dieses Bildmodell.')
|
||||||
self._phase('Auftrag wird verarbeitet · Textencoder auf RTX 3060 · Bildberechnung auf RTX 5080')
|
self._phase('Auftrag wird verarbeitet · Textencoder auf RTX 3060 · Bildberechnung auf RTX 5080')
|
||||||
response=request('/prompt',{'prompt':workflow(prompt,params,seed),'client_id':'deck-'+job_id});prompt_id=response['prompt_id']
|
response=request('/prompt',{'prompt':graph,'client_id':'deck-'+job_id});prompt_id=response['prompt_id']
|
||||||
deadline=time.monotonic()+1800
|
deadline=time.monotonic()+1800
|
||||||
while time.monotonic()<deadline:
|
while time.monotonic()<deadline:
|
||||||
guard();history=request('/history/'+prompt_id)
|
guard();history=request('/history/'+prompt_id)
|
||||||
|
|||||||
+8
-1
@@ -20,7 +20,13 @@ window.ProfilesUI=(()=>{
|
|||||||
el('profile-editor').innerHTML='<section class="card editor"><p role="status">Komponenten werden geprüft …</p></section>';
|
el('profile-editor').innerHTML='<section class="card editor"><p role="status">Komponenten werden geprüft …</p></section>';
|
||||||
try{
|
try{
|
||||||
const data=await api('profiles/components?'+new URLSearchParams({model_id:profile.model_id}));if(!root.isConnected||sequence!==panelSequence)return;
|
const data=await api('profiles/components?'+new URLSearchParams({model_id:profile.model_id}));if(!root.isConnected||sequence!==panelSequence)return;
|
||||||
el('profile-editor').innerHTML=`<section class="card editor"><div class="card-top"><h2>Komponenten · ${e(profile.name)}</h2><button class="secondary" id="components-close">Schließen</button></div><p>${e(data.message)}</p>${data.supported?`<p>Quelle: <a class="link" href="${e(data.source)}" target="_blank" rel="noopener noreferrer">Modellkarte und Lizenz ↗</a> · ${e(data.license||'Lizenz nicht angegeben')}</p><p><a class="link" href="#image-runtime">Bildlaufzeit: ${e(data.runtime)} · ${data.runtime_installed?'installiert':'noch nicht eingerichtet'} →</a></p><form id="components-form">${data.requirements.map(r=>`<section class="card component-card"><h3>${e(r.label)}</h3><label>Datei aus der Bibliothek<select name="${r.role}"><option value="">Noch nicht zugeordnet</option>${r.available.map(f=>`<option value="${f.id}" ${profile.components?.[r.role]===f.id?'selected':''}>${e(f.file)} · ${(f.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${r.candidates.map((f,i)=>`<div class="note"><p class="hub-filename">${e(f.name)} · ${(f.size/1024**3).toFixed(2)} GiB</p><button type="button" class="secondary" data-component-download="${r.role}:${i}" ${f.gated||r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'disabled':''}>${r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'Bereits in Bibliothek':'Datei herunterladen'}</button></div>`).join('')||'<p>Keine passende Quelldatei verfügbar.</p>'}</section>`).join('')}<div class="card-actions"><button id="components-save">Zuordnung auf Athena speichern</button><button type="button" class="secondary" id="components-refresh">Bibliothek aktualisieren</button></div><p class="small">Lade je eine passende Datei für Textencoder und VAE. Downloads erscheinen im Reiter Downloads. Danach Bibliothek aktualisieren, Dateien auswählen und die Zuordnung speichern. Es startet kein Modell.</p></form>`:''}<p id="components-message" role="status"></p></section>`;
|
el('profile-editor').innerHTML=`<section class="card editor"><div class="card-top"><h2>Komponenten · ${e(profile.name)}</h2><button class="secondary" id="components-close">Schließen</button></div><p>${e(data.message)}</p>${data.supported?`<p>Quelle: <a class="link" href="${e(data.source)}" target="_blank" rel="noopener noreferrer">Modellkarte und Lizenz ↗</a> · ${e(data.license||'Lizenz nicht angegeben')}</p><p><a class="link" href="#image-runtime">Bildlaufzeit: ${e(data.runtime)} · ${data.runtime_installed?'installiert':'noch nicht eingerichtet'} →</a></p><button type="button" id="components-download-all">Alle fehlenden Dateien herunterladen</button><form id="components-form">${data.requirements.map(r=>`<section class="card component-card"><h3>${e(r.label)}</h3><label>Datei aus der Bibliothek<select name="${r.role}"><option value="">Noch nicht zugeordnet</option>${r.available.map(f=>`<option value="${f.id}" ${(profile.components?.[r.role]===f.id||(!profile.components?.[r.role]&&r.available.length===1))?'selected':''}>${e(f.file)} · ${(f.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${r.candidates.map((f,i)=>`<div class="note"><p class="hub-filename">${e(f.name)} · ${(f.size/1024**3).toFixed(2)} GiB</p><button type="button" class="secondary" data-component-download="${r.role}:${i}" ${f.gated||r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'disabled':''}>${r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'Bereits in Bibliothek':'Datei herunterladen'}</button></div>`).join('')||'<p>Keine passende Quelldatei verfügbar.</p>'}</section>`).join('')}<div class="card-actions"><button id="components-save">Zuordnung auf Athena speichern</button><button type="button" class="secondary" id="components-refresh">Bibliothek aktualisieren</button></div><p class="small">Lade je eine passende Datei für Textencoder und VAE. Downloads erscheinen im Reiter Downloads. Danach Bibliothek aktualisieren, Dateien auswählen und die Zuordnung speichern. Es startet kein Modell.</p></form>`:''}<p id="components-message" role="status"></p></section>`;
|
||||||
|
const missing=data.requirements?.filter(r=>!r.available.length)||[];
|
||||||
|
if(el('components-download-all')){el('components-download-all').disabled=!missing.length;el('components-download-all').onclick=async()=>{
|
||||||
|
const b=el('components-download-all');b.disabled=true;let count=0;
|
||||||
|
try{for(const r of missing){const f=r.candidates.find(f=>!f.gated);if(!f)throw Error('Keine frei herunterladbare Datei für '+r.label);const catalog=await api('catalog');if((catalog.downloads||[]).some(j=>['queued','downloading'].includes(j.state)&&j.repo===f.repo&&j.file===f.name&&j.revision===f.revision))continue;await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:'image'});count++;}if(el('components-message'))el('components-message').textContent='Dateien sind im Downloadplan. Nach Abschluss Bibliothek aktualisieren und Zuordnung speichern; passende Dateien werden vorausgewählt.';}
|
||||||
|
catch(error){if(el('components-message'))el('components-message').textContent=error.message;b.disabled=false;}
|
||||||
|
};}
|
||||||
el('components-close').onclick=()=>el('profile-editor').replaceChildren();
|
el('components-close').onclick=()=>el('profile-editor').replaceChildren();
|
||||||
el('components-refresh')?.addEventListener('click',()=>components(profile));
|
el('components-refresh')?.addEventListener('click',()=>components(profile));
|
||||||
el('components-form')?.addEventListener('submit',async event=>{event.preventDefault();const button=el('components-save');button.disabled=true;try{await api('profiles/components',{id:profile.id,revision:profile.revision,components:Object.fromEntries(new FormData(event.target))});if(!root.isConnected)return;el('profile-editor').replaceChildren();await load();message('Komponenten dem Profil zugeordnet. Kein Modell gestartet.');}catch(error){if(el('components-message'))el('components-message').textContent=error.message;}finally{button.disabled=false;}});
|
el('components-form')?.addEventListener('submit',async event=>{event.preventDefault();const button=el('components-save');button.disabled=true;try{await api('profiles/components',{id:profile.id,revision:profile.revision,components:Object.fromEntries(new FormData(event.target))});if(!root.isConnected)return;el('profile-editor').replaceChildren();await load();message('Komponenten dem Profil zugeordnet. Kein Modell gestartet.');}catch(error){if(el('components-message'))el('components-message').textContent=error.message;}finally{button.disabled=false;}});
|
||||||
@@ -87,6 +93,7 @@ window.ProfilesUI=(()=>{
|
|||||||
function editor(p=null,id=null){
|
function editor(p=null,id=null){
|
||||||
panelSequence++;
|
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(!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==='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'){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==='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();}
|
||||||
|
|||||||
+22
-10
@@ -39,6 +39,17 @@ QWEN_COMPONENTS={
|
|||||||
'text_encoder':{'label':'Textencoder','files':['text_encoders/qwen3vl_8b_int8_convrot.safetensors','text_encoders/qwen3vl_8b_bf16.safetensors']},
|
'text_encoder':{'label':'Textencoder','files':['text_encoders/qwen3vl_8b_int8_convrot.safetensors','text_encoders/qwen3vl_8b_bf16.safetensors']},
|
||||||
'vae':{'label':'VAE','files':['vae/qwen_image_2.1_vae_bf16.safetensors']}
|
'vae':{'label':'VAE','files':['vae/qwen_image_2.1_vae_bf16.safetensors']}
|
||||||
}
|
}
|
||||||
|
FLUX_REPO='RunningHubAI/rh-flux.2-klein-9b-fp16-unet-2067980602644717569'
|
||||||
|
FLUX_COMPONENT_REPO='Comfy-Org/flux2-klein-9B'
|
||||||
|
FLUX_COMPONENTS={
|
||||||
|
'text_encoder':{'label':'Textencoder für FLUX.2 Klein 9B','repo':FLUX_COMPONENT_REPO,'files':['split_files/text_encoders/qwen_3_8b_fp8mixed.safetensors']},
|
||||||
|
'vae':{'label':'VAE für FLUX.2','repo':FLUX_COMPONENT_REPO,'files':['split_files/vae/flux2-vae.safetensors']}
|
||||||
|
}
|
||||||
|
def image_recipe(model):
|
||||||
|
if model['repo']==QWEN_REPO and model['file'].endswith('.gguf'):return QWEN_COMPONENTS
|
||||||
|
if model['repo']==FLUX_REPO and model['file']=='Flux.2 Klein-9B_fp16_nsfw.safetensors':return FLUX_COMPONENTS
|
||||||
|
return None
|
||||||
|
|
||||||
class Profiles:
|
class Profiles:
|
||||||
def __init__(self,path,catalog):
|
def __init__(self,path,catalog):
|
||||||
self.path=Path(path);self.catalog=catalog;self.lock=threading.RLock()
|
self.path=Path(path);self.catalog=catalog;self.lock=threading.RLock()
|
||||||
@@ -53,13 +64,13 @@ class Profiles:
|
|||||||
try:
|
try:
|
||||||
p['model']=self.catalog.entry(p['model_id'])
|
p['model']=self.catalog.entry(p['model_id'])
|
||||||
p['blockers']=['Für dieses Profil ist noch kein ausführbarer Worker angebunden.']
|
p['blockers']=['Für dieses Profil ist noch kein ausführbarer Worker angebunden.']
|
||||||
if p['kind']=='image' and 'qwen-image-2.1' in p['model']['repo'].lower():
|
if p['kind']=='image' and (image_recipe(p['model']) or 'qwen-image-2.1' in p['model']['repo'].lower()):
|
||||||
p['blockers']=['Eine eigene Bildlaufzeit muss eingerichtet werden; llama.cpp führt keine Bildmodelle aus.']
|
p['blockers']=['Eine eigene Bildlaufzeit muss eingerichtet werden; llama.cpp führt keine Bildmodelle aus.']
|
||||||
for role,info in reversed(list(QWEN_COMPONENTS.items())):
|
for role,info in reversed(list((image_recipe(p['model']) or QWEN_COMPONENTS).items())):
|
||||||
try:self._component(p['model'],role,p.get('components',{}).get(role))
|
try:self._component(p['model'],role,p.get('components',{}).get(role))
|
||||||
except ValueError:p['blockers'].insert(0,info['label']+' fehlt oder ist noch nicht zugeordnet.')
|
except ValueError:p['blockers'].insert(0,info['label']+' fehlt oder ist noch nicht zugeordnet.')
|
||||||
except ValueError as exc:p['model']=None;p['blockers']=[str(exc)]
|
except ValueError as exc:p['model']=None;p['blockers']=[str(exc)]
|
||||||
if p.get('model') and p['model']['repo']==QWEN_REPO and p['model']['file'].endswith('.gguf') and self.image_runtime_ready():
|
if p.get('model') and image_recipe(p['model']) and self.image_runtime_ready():
|
||||||
p['blockers']=[b for b in p['blockers'] if not b.startswith('Eine eigene Bildlaufzeit')]
|
p['blockers']=[b for b in p['blockers'] if not b.startswith('Eine eigene Bildlaufzeit')]
|
||||||
if p['kind']=='chat' and self.chat_blockers and p.get('model'):p['blockers']=self.chat_blockers(p)
|
if p['kind']=='chat' and self.chat_blockers and p.get('model'):p['blockers']=self.chat_blockers(p)
|
||||||
if p['kind']=='image' and (p['parameters']['width']>1024 or p['parameters']['height']>1024):p['blockers'].append('Die Bildlaufzeit unterstützt derzeit maximal 1024 × 1024 Pixel.')
|
if p['kind']=='image' and (p['parameters']['width']>1024 or p['parameters']['height']>1024):p['blockers'].append('Die Bildlaufzeit unterstützt derzeit maximal 1024 × 1024 Pixel.')
|
||||||
@@ -106,24 +117,25 @@ class Profiles:
|
|||||||
return {'deleted':old['id']}
|
return {'deleted':old['id']}
|
||||||
|
|
||||||
def _component(self,model,role,ident):
|
def _component(self,model,role,ident):
|
||||||
if model['repo']!=QWEN_REPO or role not in QWEN_COMPONENTS:raise ValueError('Für dieses Modell ist kein geprüftes Komponentenrezept hinterlegt.')
|
recipe=image_recipe(model)
|
||||||
|
if not recipe or role not in recipe:raise ValueError('Für dieses Modell ist kein geprüftes Komponentenrezept hinterlegt.')
|
||||||
item=self.catalog.entry(ident)
|
item=self.catalog.entry(ident)
|
||||||
if item['repo']!=QWEN_REPO or item['kind']!='image' or item['file'] not in QWEN_COMPONENTS[role]['files']:raise ValueError('Die Datei gehört nicht zu dieser Komponente des Modells.')
|
if item['repo']!=recipe[role].get('repo',model['repo']) or item['kind']!='image' or item['file'] not in recipe[role]['files']:raise ValueError('Die Datei gehört nicht zu dieser Komponente des Modells.')
|
||||||
return item
|
return item
|
||||||
def components(self,model_id):
|
def components(self,model_id):
|
||||||
model=self.catalog.entry(model_id)
|
model=self.catalog.entry(model_id)
|
||||||
if model['repo']!=QWEN_REPO or model['kind']!='image':return dict(supported=False,requirements=[],message='Für dieses Modell ist noch kein Komponentenrezept hinterlegt. Keine automatische Zuordnung.')
|
if not image_recipe(model) or model['kind']!='image':return dict(supported=False,requirements=[],message='Für dieses Modell ist noch kein Komponentenrezept hinterlegt. Keine automatische Zuordnung.')
|
||||||
source=self.catalog.files(QWEN_REPO);entries=self.catalog.status()['entries'];requirements=[]
|
recipe=image_recipe(model);source=self.catalog.files(next(iter(recipe.values())).get('repo',model['repo']));entries=self.catalog.status()['entries'];requirements=[]
|
||||||
for role,info in QWEN_COMPONENTS.items():
|
for role,info in recipe.items():
|
||||||
available=[]
|
available=[]
|
||||||
for item in entries:
|
for item in entries:
|
||||||
try:
|
try:
|
||||||
self._component(model,role,item['id']);available.append(item)
|
self._component(model,role,item['id']);available.append(item)
|
||||||
except ValueError:pass
|
except ValueError:pass
|
||||||
candidates=[dict(f,repo=QWEN_REPO,revision=source['revision'],gated=source['gated']) for name in info['files'] for f in source['files'] if f['name']==name]
|
candidates=[dict(f,repo=source.get('repo',info.get('repo',model['repo'])),revision=source['revision'],gated=source['gated']) for name in info['files'] for f in source['files'] if f['name']==name]
|
||||||
requirements.append(dict(role=role,label=info['label'],available=available,candidates=candidates))
|
requirements.append(dict(role=role,label=info['label'],available=available,candidates=candidates))
|
||||||
return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime='ComfyUI + ComfyUI-GGUF',runtime_installed=self.image_runtime_ready(),
|
return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime='ComfyUI + ComfyUI-GGUF',runtime_installed=self.image_runtime_ready(),
|
||||||
message='Zusatzdateien laut Modellkarte für ComfyUI. Kompatibilität und Speicherbedarf müssen mit der eingerichteten Laufzeit noch praktisch geprüft werden.')
|
message='Fehlende Dateien herunterladen, danach Bibliothek aktualisieren und zuordnen. Für FLUX.2 Klein empfehlen sich zum Einstieg 4 Schritte und Guidance 1. Die Bildgewichte werden bei Bedarf zwischen RAM und RTX 5080 verschoben.' if model['repo']==FLUX_REPO else 'Zusatzdateien laut Modellkarte für ComfyUI. Kompatibilität und Speicherbedarf müssen mit der eingerichteten Laufzeit noch praktisch geprüft werden.')
|
||||||
def assign(self,data):
|
def assign(self,data):
|
||||||
if set(data)!={'id','revision','components'} or not isinstance(data['components'],dict) or set(data['components'])-set(QWEN_COMPONENTS):raise ValueError('Ungültige Komponentenzuordnung.')
|
if set(data)!={'id','revision','components'} or not isinstance(data['components'],dict) or set(data['components'])-set(QWEN_COMPONENTS):raise ValueError('Ungültige Komponentenzuordnung.')
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const Studio=(()=>{
|
|||||||
document.querySelectorAll('[data-section]').forEach(b=>b.onclick=()=>{section=b.dataset.section;render(page,true);});
|
document.querySelectorAll('[data-section]').forEach(b=>b.onclick=()=>{section=b.dataset.section;render(page,true);});
|
||||||
}
|
}
|
||||||
function runtimeOverview(){return `<div class="kicker">EINSTELLUNGEN</div><h1>Laufzeiten</h1><p>Laufzeiten führen Modelle aus. Textencoder und VAE werden separat in der Modellbibliothek verwaltet und einem Profil zugeordnet.</p><div class="grid"><section class="card"><h2>llama.cpp</h2><p>Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.</p><a class="link" href="#runtime">llama.cpp öffnen →</a></section><section class="card"><h2>Bildgenerierung</h2><p>Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.</p><a class="link" href="#image-runtime">Bildlaufzeit öffnen →</a></section><section class="card"><h2>Docker</h2><p>Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.</p><a class="link" href="#docker-runtime">Docker öffnen →</a></section></div>`;}
|
function runtimeOverview(){return `<div class="kicker">EINSTELLUNGEN</div><h1>Laufzeiten</h1><p>Laufzeiten führen Modelle aus. Textencoder und VAE werden separat in der Modellbibliothek verwaltet und einem Profil zugeordnet.</p><div class="grid"><section class="card"><h2>llama.cpp</h2><p>Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.</p><a class="link" href="#runtime">llama.cpp öffnen →</a></section><section class="card"><h2>Bildgenerierung</h2><p>Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.</p><a class="link" href="#image-runtime">Bildlaufzeit öffnen →</a></section><section class="card"><h2>Docker</h2><p>Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.</p><a class="link" href="#docker-runtime">Docker öffnen →</a></section></div>`;}
|
||||||
function imageRuntime(){return `<div class="kicker">EINSTELLUNGEN / LAUFZEITEN</div><h1>Bildgenerierung</h1><section class="card"><h2>ComfyUI + ComfyUI-GGUF</h2><p>Eigene Bildlaufzeit für Qwen-Image-2.1 GGUF. Die Umgebung kann über den Deck-Installer mitgeliefert oder hier separat installiert werden. Modelle laden erst bei einem Testauftrag.</p><p>Textencoder läuft auf der RTX 3060, Diffusionsmodell und VAE auf der RTX 5080. Beide GPUs müssen frei sein. Pro Test wird ein eigener Prozess gestartet und anschließend beendet.</p><div id="image-runtime-install"><p>Installationsstatus wird geprüft …</p></div><a class="link" href="#image">Bildgenerierung → Testen öffnen</a></section>${ImageTestUI.html(true)}`;}
|
function imageRuntime(){return `<div class="kicker">EINSTELLUNGEN / LAUFZEITEN</div><h1>Bildgenerierung</h1><section class="card"><h2>ComfyUI + ComfyUI-GGUF</h2><p>Eigene Bildlaufzeit für Qwen-Image-2.1 GGUF und das unterstützte FLUX.2-Klein-9B-Modell. Die Umgebung kann über den Deck-Installer mitgeliefert oder hier separat installiert werden. Modelle laden erst bei einem Testauftrag.</p><p>Textencoder läuft auf der RTX 3060, Diffusionsmodell und VAE auf der RTX 5080. Bei großen FLUX-Gewichten wird zusätzlich System-RAM genutzt. Beide GPUs müssen frei sein. Pro Test wird ein eigener Prozess gestartet und anschließend beendet.</p><div id="image-runtime-install"><p>Installationsstatus wird geprüft …</p></div><a class="link" href="#image">Bildgenerierung → Testen öffnen</a></section>${ImageTestUI.html(true)}`;}
|
||||||
function openProfile(page,modelId){render(page,true,modelId);}
|
function openProfile(page,modelId){render(page,true,modelId);}
|
||||||
return {render,openProfile};
|
return {render,openProfile};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ class ImageTestTests(unittest.TestCase):
|
|||||||
self.assertEqual(graph['6']['inputs']['steps'],12)
|
self.assertEqual(graph['6']['inputs']['steps'],12)
|
||||||
self.assertEqual(graph['6']['inputs']['cfg'],1.5)
|
self.assertEqual(graph['6']['inputs']['cfg'],1.5)
|
||||||
self.assertEqual(graph['5']['inputs']['height'],768)
|
self.assertEqual(graph['5']['inputs']['height'],768)
|
||||||
|
def test_flux_graph_and_offload(self):
|
||||||
|
p=dict(width=512,height=512,steps=4,guidance=1)
|
||||||
|
graph=workflow('a red bicycle',p,42,'flux')
|
||||||
|
self.assertEqual(graph['1']['class_type'],'UNETLoader');self.assertEqual(graph['2']['inputs']['family'],'flux2')
|
||||||
|
self.assertEqual(graph['12']['class_type'],'Flux2Scheduler');self.assertEqual(graph['12']['inputs']['steps'],4)
|
||||||
|
self.assertEqual(graph['5']['class_type'],'EmptyFlux2LatentImage')
|
||||||
|
devices=[dict(name='RTX 5080',uuid='a',free_mib=16000,processes=0),dict(name='RTX 3060',uuid='b',free_mib=12000,processes=0)]
|
||||||
|
self.assertEqual(select_gpus(devices,18*1024**3,8*1024**3,400*1024**2,offload=True)[0]['uuid'],'a')
|
||||||
|
with self.assertRaises(ValueError):select_gpus(devices,18*1024**3,8*1024**3,400*1024**2)
|
||||||
def test_two_gpu_selection_rejects_busy_or_small_encoder_gpu(self):
|
def test_two_gpu_selection_rejects_busy_or_small_encoder_gpu(self):
|
||||||
devices=[dict(name='NVIDIA GeForce RTX 3060',uuid='encoder',processes=0,free_mib=11962),dict(name='NVIDIA GeForce RTX 5080',uuid='image',processes=0,free_mib=15923)]
|
devices=[dict(name='NVIDIA GeForce RTX 3060',uuid='encoder',processes=0,free_mib=11962),dict(name='NVIDIA GeForce RTX 5080',uuid='image',processes=0,free_mib=15923)]
|
||||||
image,encoder=select_gpus(devices,7*1024**3,9*1024**3,700*1024**2)
|
image,encoder=select_gpus(devices,7*1024**3,9*1024**3,700*1024**2)
|
||||||
|
|||||||
@@ -15,6 +15,19 @@ class ManagementTests(unittest.TestCase):
|
|||||||
(target/'entry.json').write_text(json.dumps(self.entry));self.catalog=Catalog(self.root/'models');self.profiles=Profiles(self.root/'profiles.json',self.catalog)
|
(target/'entry.json').write_text(json.dumps(self.entry));self.catalog=Catalog(self.root/'models');self.profiles=Profiles(self.root/'profiles.json',self.catalog)
|
||||||
def tearDown(self):self.tmp.cleanup()
|
def tearDown(self):self.tmp.cleanup()
|
||||||
def request(self):return dict(id=None,revision=0,name='image-test',kind='image',model_id=self.model_id,parameters=dict(width=1024,height=1024,steps=25,seed=-1,guidance=1))
|
def request(self):return dict(id=None,revision=0,name='image-test',kind='image',model_id=self.model_id,parameters=dict(width=1024,height=1024,steps=25,seed=-1,guidance=1))
|
||||||
|
def test_flux_recipe_requires_matching_components(self):
|
||||||
|
from profiles import FLUX_REPO,FLUX_COMPONENTS
|
||||||
|
from unittest.mock import patch
|
||||||
|
path=self.root/'models'/self.model_id
|
||||||
|
self.entry.update(repo=FLUX_REPO,file='Flux.2 Klein-9B_fp16_nsfw.safetensors');(path/'entry.json').write_text(json.dumps(self.entry));(path/'model.safetensors').write_bytes(b'GGUFtest')
|
||||||
|
saved=self.profiles.save(self.request());ids={};files=[]
|
||||||
|
for n,(role,info) in enumerate(FLUX_COMPONENTS.items(),1):
|
||||||
|
ident=str(n)*64;ids[role]=ident;target=self.root/'models'/ident;target.mkdir();(target/'model.safetensors').write_bytes(b'component');(target/'entry.json').write_text(json.dumps(dict(repo=info['repo'],file=info['files'][0],kind='image',size=9,revision='c'*40)));files.append(dict(name=info['files'][0],size=9))
|
||||||
|
with patch.object(self.catalog,'files',return_value=dict(files=files,revision='c'*40,gated=False,url='https://huggingface.co/example',license='test')):
|
||||||
|
self.assertTrue(self.profiles.components(self.model_id)['supported'])
|
||||||
|
self.profiles.assign(dict(id=saved['id'],revision=1,components=ids));self.profiles.image_runtime_ready=lambda:True
|
||||||
|
self.assertTrue(self.profiles.status()['profiles'][0]['runnable'])
|
||||||
|
with self.assertRaises(ValueError):self.profiles._component(self.catalog.entry(self.model_id),'vae',ids['text_encoder'])
|
||||||
def test_chat_medium_and_legacy_parameters(self):
|
def test_chat_medium_and_legacy_parameters(self):
|
||||||
self.entry['kind']='chat';(self.root/'models'/self.model_id/'entry.json').write_text(json.dumps(self.entry))
|
self.entry['kind']='chat';(self.root/'models'/self.model_id/'entry.json').write_text(json.dumps(self.entry))
|
||||||
params=dict(context=160000,slots=2,threads=6,batch=2048,ubatch=256)
|
params=dict(context=160000,slots=2,threads=6,batch=2048,ubatch=256)
|
||||||
|
|||||||
Reference in New Issue
Block a user