Add guided FLUX Klein component setup and tested image pipeline
This commit is contained in:
+29
-11
@@ -11,7 +11,7 @@ import threading
|
||||
import time
|
||||
import urllib.request
|
||||
import uuid
|
||||
from profiles import QWEN_REPO
|
||||
from profiles import QWEN_REPO, FLUX_REPO, image_recipe
|
||||
|
||||
PYTHON=Path('/opt/deck-image-python/bin/python')
|
||||
COMFY=Path('/opt/deck-comfy')
|
||||
@@ -35,15 +35,32 @@ def probe():
|
||||
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())]
|
||||
|
||||
def select_gpus(devices,model_size,encoder_size,vae_size):
|
||||
needed=(model_size+vae_size+3*GIB)/1024**2
|
||||
def select_gpus(devices,model_size,encoder_size,vae_size,offload=False):
|
||||
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]
|
||||
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 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]
|
||||
|
||||
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 {
|
||||
'1':{'class_type':'UnetLoaderGGUF','inputs':{'unet_name':'model.gguf'}},
|
||||
'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)
|
||||
if not profile or profile['kind']!='image' or not profile['model']:raise ValueError('Bildprofil nicht verfügbar.')
|
||||
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']
|
||||
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'))
|
||||
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:'))}
|
||||
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()
|
||||
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.')
|
||||
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.')
|
||||
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)
|
||||
@@ -117,7 +134,7 @@ class ImageTests:
|
||||
directory=self.root/job_id;process=None
|
||||
try:
|
||||
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))
|
||||
for folder in ('output','temp','user','input'):(directory/folder).mkdir()
|
||||
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.')
|
||||
time.sleep(1)
|
||||
nodes=request('/object_info')
|
||||
required={'UnetLoaderGGUF','DeckTextEncoderLoader','VAELoader','TextEncodeQwenImage21','EmptyLatentImage','KSampler','VAEDecode','SaveImage'}
|
||||
if not required.issubset(nodes):raise ValueError('Der installierten Bildlaufzeit fehlen erforderliche Qwen/GGUF-Nodes.')
|
||||
graph=workflow(prompt,params,seed,'flux' if model['repo']==FLUX_REPO else 'qwen')
|
||||
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')
|
||||
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
|
||||
while time.monotonic()<deadline:
|
||||
guard();history=request('/history/'+prompt_id)
|
||||
|
||||
Reference in New Issue
Block a user