Add owned OpenAI endpoint and coordinated native model switching
This commit is contained in:
+29
-15
@@ -17,6 +17,16 @@ PYTHON=Path('/opt/deck-image-python/bin/python')
|
||||
COMFY=Path('/opt/deck-comfy')
|
||||
GIB=1024**3
|
||||
|
||||
def cgroup_headroom():
|
||||
"""Exclude reclaimable inactive file cache, not live anonymous allocations."""
|
||||
try:
|
||||
raw=Path('/sys/fs/cgroup/memory.max').read_text().strip()
|
||||
if raw=='max':return None
|
||||
used=int(Path('/sys/fs/cgroup/memory.current').read_text())
|
||||
stat=dict(line.split() for line in Path('/sys/fs/cgroup/memory.stat').read_text().splitlines())
|
||||
return int(raw)-max(0,used-int(stat.get('inactive_file',0)))
|
||||
except FileNotFoundError:return None
|
||||
|
||||
def probe():
|
||||
rows=subprocess.run(['nvidia-smi','--query-gpu=uuid,name,memory.total,memory.free','--format=csv,noheader,nounits'],capture_output=True,text=True,check=True,timeout=5).stdout
|
||||
apps=subprocess.run(['nvidia-smi','--query-compute-apps=gpu_uuid,pid','--format=csv,noheader,nounits'],capture_output=True,text=True,check=True,timeout=5).stdout
|
||||
@@ -47,7 +57,7 @@ def workflow(prompt,params,seed):
|
||||
|
||||
class ImageTests:
|
||||
def __init__(self,root,profiles):
|
||||
self.root=Path(root);self.profiles=profiles;self.runtime=None;self.lock=threading.RLock();self.process=None;self.cancel=threading.Event();self.job=None
|
||||
self.root=Path(root);self.profiles=profiles;self.runtime=None;self.lock=threading.RLock();self.process=None;self.cancel=threading.Event();self.job=None;self.acquire=lambda wait=False:lambda:None
|
||||
if (self.root/'status.json').exists():
|
||||
self.job=json.loads((self.root/'status.json').read_text())
|
||||
if self.job.get('state')=='running':self.job.update(state='interrupted',phase='Deck wurde neu gestartet; Test erneut starten.')
|
||||
@@ -73,7 +83,12 @@ class ImageTests:
|
||||
return {'cancellation_requested':True}
|
||||
def model_path(self,item):
|
||||
return (self.profiles.catalog.root/item['id']/('model'+Path(item['file']).suffix)).resolve()
|
||||
def start(self,profile_id,prompt):
|
||||
def start(self,profile_id,prompt,wait=False,reserved=False):
|
||||
if not isinstance(prompt,str) or not 1<=len(prompt.strip())<=4000:raise ValueError('Bitte einen Prompt mit 1–4000 Zeichen eingeben.')
|
||||
release=(lambda:None) if reserved else self.acquire(wait)
|
||||
try:return self._start(profile_id,prompt,release)
|
||||
except Exception:release();raise
|
||||
def _start(self,profile_id,prompt,release):
|
||||
if not isinstance(prompt,str) or not 1<=len(prompt.strip())<=4000:raise ValueError('Bitte einen Prompt mit 1–4000 Zeichen eingeben.')
|
||||
with self.lock:
|
||||
if self.job and self.job['state']=='running':raise ValueError('Ein Bildtest läuft bereits.')
|
||||
@@ -89,19 +104,16 @@ class ImageTests:
|
||||
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)
|
||||
try:
|
||||
raw=Path('/sys/fs/cgroup/memory.max').read_text().strip()
|
||||
current=int(Path('/sys/fs/cgroup/memory.current').read_text())
|
||||
if raw!='max' and int(raw)-current<required_ram:raise ValueError('Deck-RAM-Limit reicht für Textencoder und Arbeitsdaten nicht aus.')
|
||||
except FileNotFoundError:pass
|
||||
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)
|
||||
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)
|
||||
self._save();threading.Thread(target=self._run,args=(job_id,prompt,params,seed,gpu,encoder_gpu,model,encoder,vae),daemon=True).start()
|
||||
self._save();threading.Thread(target=self._run,args=(job_id,prompt,params,seed,gpu,encoder_gpu,model,encoder,vae,release),daemon=True).start()
|
||||
return dict(self.job)
|
||||
def _run(self,job_id,prompt,params,seed,gpu,encoder_gpu,model,encoder,vae):
|
||||
def _run(self,job_id,prompt,params,seed,gpu,encoder_gpu,model,encoder,vae,release=lambda:None):
|
||||
directory=self.root/job_id;process=None
|
||||
try:
|
||||
directory.mkdir(mode=0o700)
|
||||
@@ -166,12 +178,14 @@ class ImageTests:
|
||||
except InterruptedError:final_state='cancelled';phase='Bildtest abgebrochen'
|
||||
except Exception as exc:final_state='failed';phase=str(exc) if isinstance(exc,ValueError) else 'Bildlaufzeit nicht erreichbar oder nicht bereit. Komponenten und Installation prüfen.'
|
||||
finally:
|
||||
if process and process.poll() is None:
|
||||
try:os.killpg(process.pid,signal.SIGTERM);process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:os.killpg(process.pid,signal.SIGKILL);process.wait()
|
||||
except ProcessLookupError:pass
|
||||
with self.lock:
|
||||
self.process=None;self.job.update(state=final_state,phase=phase,finished_at=time.time());self._save()
|
||||
try:
|
||||
if process and process.poll() is None:
|
||||
try:os.killpg(process.pid,signal.SIGTERM);process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:os.killpg(process.pid,signal.SIGKILL);process.wait()
|
||||
except ProcessLookupError:pass
|
||||
with self.lock:
|
||||
self.process=None;self.job.update(state=final_state,phase=phase,finished_at=time.time());self._save()
|
||||
finally:release()
|
||||
def image(self,job_id):
|
||||
with self.lock:
|
||||
if not self.job or self.job['id']!=job_id or self.job['state']!='complete':raise ValueError('Ergebnisbild nicht verfügbar.')
|
||||
|
||||
Reference in New Issue
Block a user