Separate vision projector device from LLM GPU split
This commit is contained in:
@@ -228,3 +228,5 @@ Backup und Wiederherstellung unter **Einstellungen → Backup & Restore**: [Umfa
|
|||||||
Die optionale LadyPoly-WebUI läuft als markierter Anwendungscontainer unter
|
Die optionale LadyPoly-WebUI läuft als markierter Anwendungscontainer unter
|
||||||
Weitere Dienste und nutzt Decks Musikworker statt einer eigenen GPU-Laufzeit.
|
Weitere Dienste und nutzt Decks Musikworker statt einer eigenen GPU-Laufzeit.
|
||||||
Installation, Verbindung und Einschränkungen: [LadyPoly](deploy/ladypoly/README.md).
|
Installation, Verbindung und Einschränkungen: [LadyPoly](deploy/ladypoly/README.md).
|
||||||
|
|
||||||
|
Sprachmodell-GPUs und Vision-Projektor werden unabhängig zugeordnet. Der Modell-Split verteilt ausschließlich LLM-Gewichte; eine zusätzliche Projektor-GPU wird nur für mmproj sichtbar gemacht, nicht für Modell-Offload. Die Speicherprüfung berücksichtigt beide Geräte einschließlich Projektorbedarf.
|
||||||
|
|||||||
+11
-4
@@ -158,6 +158,7 @@ class LlamaWorker:
|
|||||||
if params['split_mode']=='none':selected=selected[:1]
|
if params['split_mode']=='none':selected=selected[:1]
|
||||||
if not selected or any(g['processes'] or g['free_mib']<2048 for g in selected):raise InferenceError('Benötigte GPU ist durch einen anderen Dienst belegt. Deck stoppt keine fremden Prozesse.')
|
if not selected or any(g['processes'] or g['free_mib']<2048 for g in selected):raise InferenceError('Benötigte GPU ist durch einen anderen Dienst belegt. Deck stoppt keine fremden Prozesse.')
|
||||||
mem={line.split(':')[0]:int(line.split()[1])*1024 for line in Path('/proc/meminfo').read_text().splitlines() if line.startswith('MemAvailable:')}
|
mem={line.split(':')[0]:int(line.split()[1])*1024 for line in Path('/proc/meminfo').read_text().splitlines() if line.startswith('MemAvailable:')}
|
||||||
|
model_devices=list(selected)
|
||||||
projector=None;vision_args=[];vision_bytes=0;vision_device=params.get('vision_device','cpu')
|
projector=None;vision_args=[];vision_bytes=0;vision_device=params.get('vision_device','cpu')
|
||||||
if params.get('vision_projector'):
|
if params.get('vision_projector'):
|
||||||
projector=self.catalog.entry(params['vision_projector'])
|
projector=self.catalog.entry(params['vision_projector'])
|
||||||
@@ -166,7 +167,11 @@ class LlamaWorker:
|
|||||||
vision_args=['--mmproj',str((self.catalog.root/projector['id']/'model.gguf').resolve())]
|
vision_args=['--mmproj',str((self.catalog.root/projector['id']/'model.gguf').resolve())]
|
||||||
if vision_device=='cpu':vision_args+=['--no-mmproj-offload']
|
if vision_device=='cpu':vision_args+=['--no-mmproj-offload']
|
||||||
else:
|
else:
|
||||||
if vision_device not in [g['uuid'] for g in selected]:raise InferenceError('Projektor-GPU ist nicht ausgewählt.')
|
if vision_device not in [g['uuid'] for g in selected]:
|
||||||
|
target=next((g for g in devices if g['uuid']==vision_device),None)
|
||||||
|
if target is None:raise InferenceError('Projektor-GPU ist nicht verfügbar.')
|
||||||
|
if target['processes'] or target['free_mib']<2048:raise InferenceError('Projektor-GPU ist durch einen anderen Dienst belegt.')
|
||||||
|
selected.append(target)
|
||||||
vision_args+=['--mmproj-device','CUDA'+str([g['uuid'] for g in selected].index(vision_device)),'--mmproj-offload']
|
vision_args+=['--mmproj-device','CUDA'+str([g['uuid'] for g in selected].index(vision_device)),'--mmproj-offload']
|
||||||
staging=entry['size']+4*GIB+vision_bytes
|
staging=entry['size']+4*GIB+vision_bytes
|
||||||
if mem.get('MemAvailable',0)<staging+4*GIB:raise InferenceError('Zu wenig freier RAM für Modellladen und Host-Reserve.')
|
if mem.get('MemAvailable',0)<staging+4*GIB:raise InferenceError('Zu wenig freier RAM für Modellladen und Host-Reserve.')
|
||||||
@@ -177,7 +182,8 @@ class LlamaWorker:
|
|||||||
env.update(CUDA_VISIBLE_DEVICES=','.join(g['uuid'] for g in selected),HF_HUB_OFFLINE='1',OMP_NUM_THREADS=str(params['threads']))
|
env.update(CUDA_VISIBLE_DEVICES=','.join(g['uuid'] for g in selected),HF_HUB_OFFLINE='1',OMP_NUM_THREADS=str(params['threads']))
|
||||||
reserve_mode=params.get('gpu_reserve_mode','auto')
|
reserve_mode=params.get('gpu_reserve_mode','auto')
|
||||||
margins=[0 if reserve_mode=='none' else params.get('gpu_reserve_mib',{}).get(g['uuid'],512) if reserve_mode=='manual' else max(512,round(g['total_mib']*.025)) for g in selected]
|
margins=[0 if reserve_mode=='none' else params.get('gpu_reserve_mib',{}).get(g['uuid'],512) if reserve_mode=='manual' else max(512,round(g['total_mib']*.025)) for g in selected]
|
||||||
common=['--model',str(model),'--ctx-size',str(params['context']),'--parallel',str(params['slots']),'--batch-size',str(params['batch']),'--ubatch-size',str(params['ubatch']),'--cache-type-k',params.get('cache_type_k','q4_0'),'--cache-type-v',params.get('cache_type_v','q4_0'),'--flash-attn','on','--split-mode',params['split_mode'],'--fit-target',','.join(map(str,margins))]
|
common=['--model',str(model),'--ctx-size',str(params['context']),'--parallel',str(params['slots']),'--batch-size',str(params['batch']),'--ubatch-size',str(params['ubatch']),'--cache-type-k',params.get('cache_type_k','q4_0'),'--cache-type-v',params.get('cache_type_v','q4_0'),'--flash-attn','on','--split-mode',params['split_mode'],'--fit-target',','.join(map(str,margins[:len(model_devices)]))]
|
||||||
|
common+=['--device',','.join('CUDA'+str(i) for i in range(len(model_devices)))]
|
||||||
if params['tensor_split']:common+=['--tensor-split',','.join(map(str,params['tensor_split']))]
|
if params['tensor_split']:common+=['--tensor-split',','.join(map(str,params['tensor_split']))]
|
||||||
tool=str(directory/'build/bin/llama-fit-params')
|
tool=str(directory/'build/bin/llama-fit-params')
|
||||||
mtp_fit=['--deck-mtp'] if mtp else []
|
mtp_fit=['--deck-mtp'] if mtp else []
|
||||||
@@ -188,7 +194,8 @@ class LlamaWorker:
|
|||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
parts=line.split()
|
parts=line.split()
|
||||||
if len(parts)==4 and all(x.isdigit() for x in parts[1:]):rows[parts[0]]=sum(map(int,parts[1:]))
|
if len(parts)==4 and all(x.isdigit() for x in parts[1:]):rows[parts[0]]=sum(map(int,parts[1:]))
|
||||||
if 'Host' not in rows or any('CUDA'+str(i) not in rows for i in range(len(selected))):raise InferenceError('Speicherprognose unvollständig; Start abgebrochen.')
|
if 'Host' not in rows or any('CUDA'+str(i) not in rows for i in range(len(model_devices))):raise InferenceError('Speicherprognose unvollständig; Start abgebrochen.')
|
||||||
|
for i in range(len(model_devices),len(selected)):rows.setdefault('CUDA'+str(i),0)
|
||||||
if projector:
|
if projector:
|
||||||
target='Host' if vision_device=='cpu' else 'CUDA'+str([g['uuid'] for g in selected].index(vision_device))
|
target='Host' if vision_device=='cpu' else 'CUDA'+str([g['uuid'] for g in selected].index(vision_device))
|
||||||
rows[target]+=round(vision_bytes/1024**2)
|
rows[target]+=round(vision_bytes/1024**2)
|
||||||
@@ -226,7 +233,7 @@ class LlamaWorker:
|
|||||||
layers=int(fit['-ngl'])
|
layers=int(fit['-ngl'])
|
||||||
if '-ts' in fit:
|
if '-ts' in fit:
|
||||||
parts=fit['-ts'].split(',')
|
parts=fit['-ts'].split(',')
|
||||||
if len(parts)!=len(selected) or any(not x.replace('.','',1).isdigit() for x in parts):raise InferenceError('Ungültige automatische GPU-Verteilung.')
|
if len(parts)!=len(model_devices) or any(not x.replace('.','',1).isdigit() for x in parts):raise InferenceError('Ungültige automatische GPU-Verteilung.')
|
||||||
extra=['--tensor-split',fit['-ts']]
|
extra=['--tensor-split',fit['-ts']]
|
||||||
fits,memory=estimate(layers,extra)
|
fits,memory=estimate(layers,extra)
|
||||||
if not fits:raise InferenceError('Speicherprognose überschreitet die GPU-/RAM-Reserve.')
|
if not fits:raise InferenceError('Speicherprognose überschreitet die GPU-/RAM-Reserve.')
|
||||||
|
|||||||
+2
-2
@@ -54,7 +54,7 @@ window.ProfilesUI=(()=>{
|
|||||||
function gpuEditor(params){
|
function gpuEditor(params){
|
||||||
const selected=params.gpu_devices||[],available=[...gpus];for(const uuid of selected)if(!available.some(g=>g.uuid===uuid))available.push({uuid,name:'Derzeit nicht verfügbar'});
|
const selected=params.gpu_devices||[],available=[...gpus];for(const uuid of selected)if(!available.some(g=>g.uuid===uuid))available.push({uuid,name:'Derzeit nicht verfügbar'});
|
||||||
const options=(value,empty)=>`<option value="">${empty}</option>`+available.map(g=>`<option value="${e(g.uuid)}" ${value===g.uuid?'selected':''}>${e(g.name)} · ${e(g.uuid)}</option>`).join('');
|
const options=(value,empty)=>`<option value="">${empty}</option>`+available.map(g=>`<option value="${e(g.uuid)}" ${value===g.uuid?'selected':''}>${e(g.name)} · ${e(g.uuid)}</option>`).join('');
|
||||||
return `<h3>GPU-Auswahl und Verteilung</h3><div class="form-grid"><label>GPU-Ausführung<select name="gpu_offload"><option value="auto" ${(params.gpu_offload||'auto')==='auto'?'selected':''}>Automatisch · CPU-Auslagerung bei Platzmangel</option><option value="full" ${params.gpu_offload==='full'?'selected':''}>Vollständig auf GPU · sonst Start abbrechen</option></select></label><label>Zusätzliche GPU-Reserve<select name="gpu_reserve_mode">${[['auto','Automatisch · mindestens 512 MiB je GPU'],['manual','Manuell je GPU'],['none','Keine zusätzliche Reserve']].map(([v,t])=>`<option value="${v}" ${(params.gpu_reserve_mode||'auto')===v?'selected':''}>${t}</option>`).join('')}</select></label><label>Reserve erste GPU (MiB)<input name="reserve_first" type="number" min="0" max="32768" step="1" value="${params.gpu_reserve_mib?.[selected[0]]??512}"></label><label>Reserve zweite GPU (MiB)<input name="reserve_second" type="number" min="0" max="32768" step="1" value="${params.gpu_reserve_mib?.[selected[1]]??512}"></label><label>Erste GPU · CUDA0<select name="gpu_first">${options(selected[0],'Automatische Auswahl')}</select></label><label>Zweite GPU · CUDA1<select name="gpu_second">${options(selected[1],'Keine zweite GPU')}</select></label><label>GPU-Split<select name="split_mode">${[['none','Keine Aufteilung'],['layer','Layer-Split'],['row','Row-Split']].map(([v,t])=>`<option value="${v}" ${(params.split_mode||'none')===v?'selected':''}>${t}</option>`).join('')}</select></label><label>GPU-Verteilung (erste, zweite GPU)<input name="tensor_split" placeholder="z. B. 85,15" value="${e((params.tensor_split||[]).join(','))}"></label></div><p class="small">Manuelle Reserven gelten für die jeweils ausgewählte GPU. Ohne Reserve entfällt nur der zusätzliche GPU-Puffer; Modell, KV-Cache und die konservative Projektor-Prognose bleiben berücksichtigt. OOM bleibt möglich. Für einen Abbruch statt CPU-Auslagerung „Vollständig auf GPU“ wählen. Die Reihenfolge legt die logischen CUDA-Geräte des Profils fest. Für Medium: RTX 5080 zuerst, RTX 3060 danach, Layer-Split und 85,15. Leere Verteilung bedeutet automatisch. Dies ist keine Zusage, dass Modell und Kontext in den verfügbaren Speicher passen.</p>`;
|
return `<h3>Sprachmodell · GPU-Auswahl und Verteilung</h3><div class="form-grid"><label>GPU-Ausführung<select name="gpu_offload"><option value="auto" ${(params.gpu_offload||'auto')==='auto'?'selected':''}>Automatisch · CPU-Auslagerung bei Platzmangel</option><option value="full" ${params.gpu_offload==='full'?'selected':''}>Vollständig auf GPU · sonst Start abbrechen</option></select></label><label>Zusätzliche GPU-Reserve<select name="gpu_reserve_mode">${[['auto','Automatisch · mindestens 512 MiB je GPU'],['manual','Manuell je GPU'],['none','Keine zusätzliche Reserve']].map(([v,t])=>`<option value="${v}" ${(params.gpu_reserve_mode||'auto')===v?'selected':''}>${t}</option>`).join('')}</select></label><label>Reserve erste GPU (MiB)<input name="reserve_first" type="number" min="0" max="32768" step="1" value="${params.gpu_reserve_mib?.[selected[0]]??512}"></label><label>Reserve zweite GPU (MiB)<input name="reserve_second" type="number" min="0" max="32768" step="1" value="${params.gpu_reserve_mib?.[selected[1]]??512}"></label><label>Erste GPU · CUDA0<select name="gpu_first">${options(selected[0],'Automatische Auswahl')}</select></label><label>Zweite GPU · CUDA1<select name="gpu_second">${options(selected[1],'Keine zweite GPU')}</select></label><label>Sprachmodell-Split<select name="split_mode">${[['none','Keine Aufteilung'],['layer','Layer-Split'],['row','Row-Split']].map(([v,t])=>`<option value="${v}" ${(params.split_mode||'none')===v?'selected':''}>${t}</option>`).join('')}</select></label><label>GPU-Verteilung (erste, zweite GPU)<input name="tensor_split" placeholder="z. B. 85,15" value="${e((params.tensor_split||[]).join(','))}"></label></div><p class="small">Manuelle Reserven gelten für die jeweils ausgewählte GPU. Ohne Reserve entfällt nur der zusätzliche GPU-Puffer; Modell, KV-Cache und die konservative Projektor-Prognose bleiben berücksichtigt. OOM bleibt möglich. Für einen Abbruch statt CPU-Auslagerung „Vollständig auf GPU“ wählen. Die Reihenfolge legt die logischen CUDA-Geräte des Profils fest. Für Medium: RTX 5080 zuerst, RTX 3060 danach, Layer-Split und 85,15. Leere Verteilung bedeutet automatisch. Dies ist keine Zusage, dass Modell und Kontext in den verfügbaren Speicher passen.</p>`;
|
||||||
}
|
}
|
||||||
function bindProjectorDiscovery(){
|
function bindProjectorDiscovery(){
|
||||||
const form=el('profile-form'),box=el('projector-discovery'),select=form.elements.vision_projector;
|
const form=el('profile-form'),box=el('projector-discovery'),select=form.elements.vision_projector;
|
||||||
@@ -135,7 +135,7 @@ window.ProfilesUI=(()=>{
|
|||||||
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;}
|
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" placeholder="z. B. Qwen3.8 27B MEDIUM" 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>KV-Cache</h3><div class="form-grid">${["k","v"].map(axis=>`<label>${axis.toUpperCase()}-Cache<select name="cache_type_${axis}">${["q4_0","q8_0","f16"].map(type=>`<option value="${type}" ${(data.parameters[`cache_type_${axis}`]||"q4_0")===type?"selected":""}>${type}</option>`).join("")}</select></label>`).join("")}</div><p class="small">q4_0 spart VRAM; q8_0 und f16 brauchen mehr Speicher. K und V werden getrennt eingestellt. MTP-Draft-KV bleibt separat f16.</p><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" placeholder="z. B. Qwen3.8 27B MEDIUM" 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>KV-Cache</h3><div class="form-grid">${["k","v"].map(axis=>`<label>${axis.toUpperCase()}-Cache<select name="cache_type_${axis}">${["q4_0","q8_0","f16"].map(type=>`<option value="${type}" ${(data.parameters[`cache_type_${axis}`]||"q4_0")===type?"selected":""}>${type}</option>`).join("")}</select></label>`).join("")}</div><p class="small">q4_0 spart VRAM; q8_0 und f16 brauchen mehr Speicher. K und V werden getrennt eingestellt. MTP-Draft-KV bleibt separat f16.</p><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. Das Projektor-Gerät wird unabhängig von den Sprachmodell-GPUs gewählt. Der Sprachmodell-Split verteilt ausschließlich die Sprachmodell-Gewichte. 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();}
|
||||||
if(kind==='video')bindVideoDevices(data);
|
if(kind==='video')bindVideoDevices(data);
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ def chat_parameters(params):
|
|||||||
if params['cache_type_k'] not in ('f16','q8_0','q4_0') or params['cache_type_v'] not in ('f16','q8_0','q4_0'):raise ValueError('KV-Cache: nur f16, q8_0 oder q4_0 unterstützt.')
|
if params['cache_type_k'] not in ('f16','q8_0','q4_0') or params['cache_type_v'] not in ('f16','q8_0','q4_0'):raise ValueError('KV-Cache: nur f16, q8_0 oder q4_0 unterstützt.')
|
||||||
if type(params['mtp']) is not bool:raise ValueError('MTP muss an oder aus sein.')
|
if type(params['mtp']) is not bool:raise ValueError('MTP muss an oder aus sein.')
|
||||||
if params['vision_projector'] is not None and (not isinstance(params['vision_projector'],str) or not re.fullmatch(r'[a-f0-9]{64}',params['vision_projector'])):raise ValueError('Ungültige Projektor-ID.')
|
if params['vision_projector'] is not None and (not isinstance(params['vision_projector'],str) or not re.fullmatch(r'[a-f0-9]{64}',params['vision_projector'])):raise ValueError('Ungültige Projektor-ID.')
|
||||||
if params['vision_device']!='cpu' and params['vision_device'] not in params['gpu_devices']:raise ValueError('Projektor-GPU muss in der GPU-Auswahl enthalten sein.')
|
if params['vision_device']!='cpu' and (not isinstance(params['vision_device'],str) or not re.fullmatch(r'GPU-[0-9a-fA-F-]{36}',params['vision_device'])):raise ValueError('Projektor-Gerät muss CPU oder eine gültige GPU-UUID sein.')
|
||||||
devices=params['gpu_devices'];split=params['tensor_split']
|
devices=params['gpu_devices'];split=params['tensor_split']
|
||||||
if not isinstance(devices,list) or len(devices)>16 or any(not isinstance(v,str) or not re.fullmatch(r'GPU-[0-9a-fA-F-]{36}',v) for v in devices) or len(set(devices))!=len(devices):raise ValueError('GPUs müssen als eindeutige, geordnete GPU-UUIDs angegeben werden.')
|
if not isinstance(devices,list) or len(devices)>16 or any(not isinstance(v,str) or not re.fullmatch(r'GPU-[0-9a-fA-F-]{36}',v) for v in devices) or len(set(devices))!=len(devices):raise ValueError('GPUs müssen als eindeutige, geordnete GPU-UUIDs angegeben werden.')
|
||||||
if params['split_mode'] not in ('none','layer','row'):raise ValueError('Ungültiger GPU-Split-Modus.')
|
if params['split_mode'] not in ('none','layer','row'):raise ValueError('Ungültiger GPU-Split-Modus.')
|
||||||
|
|||||||
+15
-1
@@ -17,8 +17,10 @@ class CapabilitiesTests(unittest.TestCase):
|
|||||||
for value in (-1,32769,True,1.5):
|
for value in (-1,32769,True,1.5):
|
||||||
with self.assertRaises(ValueError):chat_parameters({'gpu_devices':['GPU-a'],'gpu_reserve_mode':'manual','gpu_reserve_mib':{'GPU-a':value}})
|
with self.assertRaises(ValueError):chat_parameters({'gpu_devices':['GPU-a'],'gpu_reserve_mode':'manual','gpu_reserve_mib':{'GPU-a':value}})
|
||||||
with self.assertRaises(ValueError):chat_parameters({'gpu_reserve_mode':'manual'})
|
with self.assertRaises(ValueError):chat_parameters({'gpu_reserve_mode':'manual'})
|
||||||
def test_projector_device_requires_selected_gpu(self):
|
def test_projector_device_validates_uuid_independently(self):
|
||||||
with self.assertRaises(ValueError):chat_parameters({'vision_device':'GPU-missing'})
|
with self.assertRaises(ValueError):chat_parameters({'vision_device':'GPU-missing'})
|
||||||
|
uuid='GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b'
|
||||||
|
self.assertEqual(chat_parameters({'vision_device':uuid})['vision_device'],uuid)
|
||||||
|
|
||||||
class ProjectorLaunchTests(WorkerTests):
|
class ProjectorLaunchTests(WorkerTests):
|
||||||
def test_cpu_and_gpu_projector_flags_and_reserve(self):
|
def test_cpu_and_gpu_projector_flags_and_reserve(self):
|
||||||
@@ -32,4 +34,16 @@ class ProjectorLaunchTests(WorkerTests):
|
|||||||
if device=='cpu':self.assertIn('--no-mmproj-offload',args)
|
if device=='cpu':self.assertIn('--no-mmproj-offload',args)
|
||||||
else:self.assertEqual(args[args.index('--mmproj-device')+1],'CUDA1');self.assertEqual(self.worker.memory_plan['gpus'][1]['required_mib'],7072)
|
else:self.assertEqual(args[args.index('--mmproj-device')+1],'CUDA1');self.assertEqual(self.worker.memory_plan['gpus'][1]['required_mib'],7072)
|
||||||
self.worker.process=None
|
self.worker.process=None
|
||||||
|
def test_projector_only_gpu_excluded_from_model_devices(self):
|
||||||
|
self.profile['parameters'].update(gpu_devices=['gpu-first'],split_mode='none',tensor_split=[],vision_projector='p',vision_device='gpu-second',gpu_offload='full')
|
||||||
|
self.worker.catalog.entry=lambda ident:dict(id='p',file='mmproj.gguf',size=1024**3,role='vision_projector') if ident=='p' else self.entry
|
||||||
|
process=Mock();process.poll.return_value=None;http=Mock();http.getresponse.return_value.status=200
|
||||||
|
with self.patches(),patch('inference.probe',return_value=self.gpus),patch('inference.cgroup_headroom',return_value=32*1024**3),patch.object(self.worker,'_fit_command',return_value='CUDA0 9000 1000 1000\nHost 512 0 256\n'),patch('inference.subprocess.Popen',return_value=process) as launch,patch('inference.http.client.HTTPConnection',return_value=http),patch('inference.threading.Thread'):
|
||||||
|
self.worker.ensure(self.profile)
|
||||||
|
args=launch.call_args.args[0]
|
||||||
|
self.assertEqual(args[args.index('--device')+1],'CUDA0')
|
||||||
|
self.assertEqual(args[args.index('--mmproj-device')+1],'CUDA1')
|
||||||
|
self.assertEqual(self.worker.memory_plan['gpus'][1]['required_mib'],3072)
|
||||||
|
self.assertEqual(launch.call_args.kwargs['env']['CUDA_VISIBLE_DEVICES'],'gpu-first,gpu-second')
|
||||||
|
self.worker.process=None
|
||||||
if __name__=='__main__':unittest.main()
|
if __name__=='__main__':unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user