genai-electron 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/config/defaults.d.ts +29 -13
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +43 -12
- package/dist/config/defaults.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/DiffusionServerManager.d.ts +11 -3
- package/dist/managers/DiffusionServerManager.d.ts.map +1 -1
- package/dist/managers/DiffusionServerManager.js +468 -32
- package/dist/managers/DiffusionServerManager.js.map +1 -1
- package/dist/managers/ResourceOrchestrator.d.ts +2 -2
- package/dist/managers/ResourceOrchestrator.d.ts.map +1 -1
- package/dist/managers/ResourceOrchestrator.js.map +1 -1
- package/dist/types/images.d.ts +77 -1
- package/dist/types/images.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -6,11 +6,12 @@ import { ResourceOrchestrator } from './ResourceOrchestrator.js';
|
|
|
6
6
|
import { GenerationRegistry } from './GenerationRegistry.js';
|
|
7
7
|
import http from 'node:http';
|
|
8
8
|
import { promises as fs } from 'node:fs';
|
|
9
|
-
import { getTempPath
|
|
10
|
-
import { BINARY_VERSIONS, DEFAULT_PORTS, DIFFUSION_VRAM_THRESHOLDS, DIFFUSION_COMPONENT_FLAGS, DIFFUSION_COMPONENT_ORDER, } from '../config/defaults.js';
|
|
9
|
+
import { getTempPath } from '../config/paths.js';
|
|
10
|
+
import { BINARY_VERSIONS, DEFAULT_PORTS, DIFFUSION_VRAM_THRESHOLDS, DIFFUSION_COMPONENT_FLAGS, DIFFUSION_COMPONENT_ORDER, DIFFUSION_CALIBRATION_DEFAULTS, } from '../config/defaults.js';
|
|
11
11
|
import { deleteFile } from '../utils/file-utils.js';
|
|
12
|
+
import { debugLog } from '../utils/debug-log.js';
|
|
12
13
|
import { findFreePort } from '../process/port-utils.js';
|
|
13
|
-
import { ServerError, ModelNotFoundError, InsufficientResourcesError } from '../errors/index.js';
|
|
14
|
+
import { GenaiElectronError, ServerError, ModelNotFoundError, InsufficientResourcesError, } from '../errors/index.js';
|
|
14
15
|
export class DiffusionServerManager extends ServerManager {
|
|
15
16
|
static VALID_CONFIG_FIELDS = new Set([
|
|
16
17
|
'modelId',
|
|
@@ -34,6 +35,8 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
34
35
|
currentGeneration;
|
|
35
36
|
activeGeneration;
|
|
36
37
|
currentModelInfo;
|
|
38
|
+
lastResolvedOptimizations;
|
|
39
|
+
calibrating = false;
|
|
37
40
|
modelLoadTime = 2000;
|
|
38
41
|
diffusionTimePerStepPerMegapixel = 1000;
|
|
39
42
|
vaeTimePerMegapixel = 8000;
|
|
@@ -71,6 +74,11 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
71
74
|
suggestion: 'Stop the server first with stop()',
|
|
72
75
|
});
|
|
73
76
|
}
|
|
77
|
+
if (this.calibrating) {
|
|
78
|
+
throw new ServerError('Cannot start server while offload calibration is in progress', {
|
|
79
|
+
suggestion: 'Wait for calibrate() to finish, or abort it via its AbortSignal',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
74
82
|
this.validateConfigFields(config, DiffusionServerManager.VALID_CONFIG_FIELDS, 'DiffusionServerManager');
|
|
75
83
|
this.setStatus('starting');
|
|
76
84
|
this._config = config;
|
|
@@ -173,6 +181,389 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
173
181
|
}
|
|
174
182
|
await this.logManager?.write(`Generation ${id} cancelled`, 'info');
|
|
175
183
|
}
|
|
184
|
+
async calibrate(config) {
|
|
185
|
+
if (this._status !== 'stopped') {
|
|
186
|
+
throw new ServerError('Cannot calibrate while the server is running', {
|
|
187
|
+
suggestion: 'Stop the server with stop() before calibrating',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (this.calibrating) {
|
|
191
|
+
throw new ServerError('Calibration is already in progress', {
|
|
192
|
+
suggestion: 'Wait for the current calibrate() call to finish',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const defaults = DIFFUSION_CALIBRATION_DEFAULTS;
|
|
196
|
+
const sizes = config.sizes && config.sizes.length > 0 ? config.sizes : [...defaults.sizes];
|
|
197
|
+
for (const size of sizes) {
|
|
198
|
+
if (!Number.isInteger(size.width) ||
|
|
199
|
+
!Number.isInteger(size.height) ||
|
|
200
|
+
size.width <= 0 ||
|
|
201
|
+
size.height <= 0 ||
|
|
202
|
+
size.width % 64 !== 0 ||
|
|
203
|
+
size.height % 64 !== 0) {
|
|
204
|
+
throw new ServerError(`Invalid calibration size ${size.width}x${size.height}: dimensions must be positive multiples of 64`, { suggestion: 'Use sd.cpp-compatible dimensions, e.g. 512x512, 768x768, 512x1024' });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const samples = Math.max(1, Math.floor(config.samples ?? defaults.samples));
|
|
208
|
+
const steps = config.steps ?? defaults.steps;
|
|
209
|
+
const seed = config.seed ?? defaults.seed;
|
|
210
|
+
const sampler = config.sampler ?? defaults.sampler;
|
|
211
|
+
const prompt = config.prompt ?? defaults.prompt;
|
|
212
|
+
const runs = [];
|
|
213
|
+
if (config.signal?.aborted) {
|
|
214
|
+
throw this.calibrationAbortError(runs);
|
|
215
|
+
}
|
|
216
|
+
this.calibrating = true;
|
|
217
|
+
const savedConfig = this._config;
|
|
218
|
+
const savedModelInfo = this.currentModelInfo;
|
|
219
|
+
const savedBinaryPath = this.binaryPath;
|
|
220
|
+
const abortListener = () => {
|
|
221
|
+
this.currentGeneration?.cancel();
|
|
222
|
+
};
|
|
223
|
+
config.signal?.addEventListener('abort', abortListener);
|
|
224
|
+
let emitFn;
|
|
225
|
+
let comboCountForProgress = 0;
|
|
226
|
+
let lastOverallPercent = 0;
|
|
227
|
+
let succeeded = false;
|
|
228
|
+
try {
|
|
229
|
+
const modelInfo = await this.modelManager.getModelInfo(config.modelId);
|
|
230
|
+
if (modelInfo.type !== 'diffusion') {
|
|
231
|
+
throw new ModelNotFoundError(`Model ${config.modelId} is not a diffusion model (type: ${modelInfo.type})`);
|
|
232
|
+
}
|
|
233
|
+
const requestedCombos = (config.combos && config.combos.length > 0 ? config.combos : defaults.combos).map((combo) => ({ ...combo }));
|
|
234
|
+
const skippedCombos = [];
|
|
235
|
+
let combos = requestedCombos;
|
|
236
|
+
if (defaults.sd35LargePattern.test(modelInfo.id) ||
|
|
237
|
+
defaults.sd35LargePattern.test(modelInfo.name)) {
|
|
238
|
+
combos = requestedCombos.filter((combo) => {
|
|
239
|
+
if (combo.clipOnCpu === true) {
|
|
240
|
+
skippedCombos.push({
|
|
241
|
+
combo,
|
|
242
|
+
reason: 'SD3.5-Large produces garbled output with --clip-on-cpu (leejet/stable-diffusion.cpp#1578)',
|
|
243
|
+
});
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (combos.length === 0) {
|
|
250
|
+
throw new ServerError('No offload combos left to benchmark after SD3.5-Large filtering', {
|
|
251
|
+
skippedCombos,
|
|
252
|
+
suggestion: 'Provide combos without clipOnCpu: true for this model',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
comboCountForProgress = combos.length;
|
|
256
|
+
const totalUnits = combos.length * (1 + samples * sizes.length);
|
|
257
|
+
let completedUnits = 0;
|
|
258
|
+
const emit = (p) => {
|
|
259
|
+
try {
|
|
260
|
+
config.onProgress?.(p);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
debugLog('[Calibrate] onProgress callback threw:', error);
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
this.emit('calibration-progress', p);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
debugLog('[Calibrate] calibration-progress listener threw:', error);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
emitFn = emit;
|
|
273
|
+
const overallPercent = (generationFraction = 0) => {
|
|
274
|
+
const pct = Math.min(100, ((completedUnits + generationFraction) / totalUnits) * 100);
|
|
275
|
+
lastOverallPercent = Math.max(lastOverallPercent, pct);
|
|
276
|
+
return lastOverallPercent;
|
|
277
|
+
};
|
|
278
|
+
emit({
|
|
279
|
+
phase: 'preparing',
|
|
280
|
+
comboIndex: 0,
|
|
281
|
+
comboCount: combos.length,
|
|
282
|
+
sizeIndex: 0,
|
|
283
|
+
sizeCount: sizes.length,
|
|
284
|
+
overallPercent: 0,
|
|
285
|
+
});
|
|
286
|
+
const canRun = await this.systemInfo.canRunModel(modelInfo, { checkTotalMemory: true });
|
|
287
|
+
if (!canRun.possible) {
|
|
288
|
+
const memoryInfo = this.systemInfo.getMemoryInfo();
|
|
289
|
+
throw new InsufficientResourcesError(`System cannot run model: ${canRun.reason || 'Insufficient resources'}`, {
|
|
290
|
+
required: `Model size: ${Math.round(modelInfo.size / 1024 / 1024 / 1024)}GB`,
|
|
291
|
+
available: `Total RAM: ${Math.round(memoryInfo.total / 1024 / 1024 / 1024)}GB`,
|
|
292
|
+
suggestion: canRun.suggestion || canRun.reason || 'Try a smaller model',
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (!this.logManager) {
|
|
296
|
+
await this.initializeLogManager('diffusion-server.log', 'Offload calibration starting');
|
|
297
|
+
}
|
|
298
|
+
void this.logManager
|
|
299
|
+
?.write(`Calibration: model=${config.modelId}, sizes=${sizes
|
|
300
|
+
.map((s) => `${s.width}x${s.height}`)
|
|
301
|
+
.join(',')}, combos=${combos.length}${skippedCombos.length > 0 ? ` (${skippedCombos.length} skipped: SD3.5-Large)` : ''}, steps=${steps}, samples=${samples}`, 'info')
|
|
302
|
+
.catch(() => void 0);
|
|
303
|
+
this.currentModelInfo = modelInfo;
|
|
304
|
+
this.binaryPath = await this.ensureBinary(modelInfo);
|
|
305
|
+
if (config.signal?.aborted) {
|
|
306
|
+
throw this.calibrationAbortError(runs);
|
|
307
|
+
}
|
|
308
|
+
const syntheticConfig = { modelId: config.modelId };
|
|
309
|
+
if (config.threads !== undefined) {
|
|
310
|
+
syntheticConfig.threads = config.threads;
|
|
311
|
+
}
|
|
312
|
+
if (config.batchSize !== undefined) {
|
|
313
|
+
syntheticConfig.batchSize = config.batchSize;
|
|
314
|
+
}
|
|
315
|
+
this._config = syntheticConfig;
|
|
316
|
+
await this.orchestrator?.waitForReload();
|
|
317
|
+
await this.orchestrator?.offloadLLM();
|
|
318
|
+
for (let comboIndex = 0; comboIndex < combos.length; comboIndex++) {
|
|
319
|
+
const combo = combos[comboIndex];
|
|
320
|
+
const baseProgress = {
|
|
321
|
+
comboIndex,
|
|
322
|
+
comboCount: combos.length,
|
|
323
|
+
combo,
|
|
324
|
+
sizeCount: sizes.length,
|
|
325
|
+
};
|
|
326
|
+
let warmupFailure;
|
|
327
|
+
if (config.signal?.aborted) {
|
|
328
|
+
throw this.calibrationAbortError(runs);
|
|
329
|
+
}
|
|
330
|
+
emit({
|
|
331
|
+
phase: 'warmup',
|
|
332
|
+
...baseProgress,
|
|
333
|
+
sizeIndex: 0,
|
|
334
|
+
size: sizes[0],
|
|
335
|
+
overallPercent: overallPercent(),
|
|
336
|
+
});
|
|
337
|
+
try {
|
|
338
|
+
await this.runCalibrationGeneration({
|
|
339
|
+
prompt,
|
|
340
|
+
size: sizes[0],
|
|
341
|
+
steps,
|
|
342
|
+
cfgScale: config.cfgScale,
|
|
343
|
+
seed,
|
|
344
|
+
sampler,
|
|
345
|
+
combo,
|
|
346
|
+
onGenerationProgress: (pct) => emit({
|
|
347
|
+
phase: 'warmup',
|
|
348
|
+
...baseProgress,
|
|
349
|
+
sizeIndex: 0,
|
|
350
|
+
size: sizes[0],
|
|
351
|
+
generationPercent: pct,
|
|
352
|
+
overallPercent: overallPercent(pct / 100),
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (config.signal?.aborted) {
|
|
358
|
+
throw this.calibrationAbortError(runs);
|
|
359
|
+
}
|
|
360
|
+
warmupFailure = this.classifyCalibrationFailure(error);
|
|
361
|
+
}
|
|
362
|
+
completedUnits++;
|
|
363
|
+
for (let sizeIndex = 0; sizeIndex < sizes.length; sizeIndex++) {
|
|
364
|
+
const size = sizes[sizeIndex];
|
|
365
|
+
const samplesMs = [];
|
|
366
|
+
const snapshots = [];
|
|
367
|
+
let resolved;
|
|
368
|
+
let failure;
|
|
369
|
+
if (sizeIndex === 0 && warmupFailure) {
|
|
370
|
+
failure = warmupFailure;
|
|
371
|
+
completedUnits += samples;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
for (let sample = 1; sample <= samples; sample++) {
|
|
375
|
+
if (config.signal?.aborted) {
|
|
376
|
+
throw this.calibrationAbortError(runs);
|
|
377
|
+
}
|
|
378
|
+
emit({
|
|
379
|
+
phase: 'sampling',
|
|
380
|
+
...baseProgress,
|
|
381
|
+
sizeIndex,
|
|
382
|
+
size,
|
|
383
|
+
sample,
|
|
384
|
+
sampleCount: samples,
|
|
385
|
+
overallPercent: overallPercent(),
|
|
386
|
+
});
|
|
387
|
+
try {
|
|
388
|
+
const result = await this.runCalibrationGeneration({
|
|
389
|
+
prompt,
|
|
390
|
+
size,
|
|
391
|
+
steps,
|
|
392
|
+
cfgScale: config.cfgScale,
|
|
393
|
+
seed,
|
|
394
|
+
sampler,
|
|
395
|
+
combo,
|
|
396
|
+
onGenerationProgress: (pct) => emit({
|
|
397
|
+
phase: 'sampling',
|
|
398
|
+
...baseProgress,
|
|
399
|
+
sizeIndex,
|
|
400
|
+
size,
|
|
401
|
+
sample,
|
|
402
|
+
sampleCount: samples,
|
|
403
|
+
generationPercent: pct,
|
|
404
|
+
overallPercent: overallPercent(pct / 100),
|
|
405
|
+
}),
|
|
406
|
+
});
|
|
407
|
+
samplesMs.push(result.timeTaken);
|
|
408
|
+
snapshots.push(this.snapshotStageMs());
|
|
409
|
+
resolved = this.lastResolvedOptimizations
|
|
410
|
+
? { ...this.lastResolvedOptimizations }
|
|
411
|
+
: undefined;
|
|
412
|
+
completedUnits++;
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (config.signal?.aborted) {
|
|
416
|
+
throw this.calibrationAbortError(runs);
|
|
417
|
+
}
|
|
418
|
+
failure = this.classifyCalibrationFailure(error);
|
|
419
|
+
completedUnits += samples - sample + 1;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const run = { size, combo, status: failure ? failure.status : 'ok' };
|
|
425
|
+
if (resolved) {
|
|
426
|
+
run.resolved = resolved;
|
|
427
|
+
}
|
|
428
|
+
if (samplesMs.length > 0) {
|
|
429
|
+
run.samplesMs = samplesMs;
|
|
430
|
+
}
|
|
431
|
+
if (failure) {
|
|
432
|
+
run.error = failure.message;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
const median = medianOf(samplesMs);
|
|
436
|
+
run.timeTakenMs = median;
|
|
437
|
+
let bestIdx = 0;
|
|
438
|
+
for (let i = 1; i < samplesMs.length; i++) {
|
|
439
|
+
if (Math.abs(samplesMs[i] - median) < Math.abs(samplesMs[bestIdx] - median)) {
|
|
440
|
+
bestIdx = i;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const stage = snapshots[bestIdx];
|
|
444
|
+
if (stage &&
|
|
445
|
+
(stage.loadMs !== undefined ||
|
|
446
|
+
stage.diffusionMs !== undefined ||
|
|
447
|
+
stage.decodeMs !== undefined)) {
|
|
448
|
+
run.stageMs = stage;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
runs.push(run);
|
|
452
|
+
void this.logManager
|
|
453
|
+
?.write(`Calibration run: ${combo.label ?? JSON.stringify(combo)} @ ${size.width}x${size.height} → ${run.status}${run.timeTakenMs !== undefined ? ` (${Math.round(run.timeTakenMs)} ms)` : ''}${run.error ? ` — ${run.error.split('\n')[0]}` : ''}`, run.status === 'ok' ? 'info' : 'warn')
|
|
454
|
+
.catch(() => void 0);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const recommended = pickRecommended(runs, defaults.tieTolerancePct);
|
|
458
|
+
const machine = {};
|
|
459
|
+
try {
|
|
460
|
+
const gpu = await this.systemInfo.getGPUInfo();
|
|
461
|
+
machine.gpuType = gpu.type;
|
|
462
|
+
machine.gpuName = gpu.name;
|
|
463
|
+
machine.vramBytes = gpu.vram;
|
|
464
|
+
machine.vramAvailableBytes = gpu.vramAvailable;
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
}
|
|
468
|
+
const report = {
|
|
469
|
+
machine,
|
|
470
|
+
modelId: config.modelId,
|
|
471
|
+
steps,
|
|
472
|
+
sampler,
|
|
473
|
+
samples,
|
|
474
|
+
runs,
|
|
475
|
+
recommended,
|
|
476
|
+
};
|
|
477
|
+
if (skippedCombos.length > 0) {
|
|
478
|
+
report.skippedCombos = skippedCombos;
|
|
479
|
+
}
|
|
480
|
+
succeeded = true;
|
|
481
|
+
return report;
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
config.signal?.removeEventListener('abort', abortListener);
|
|
485
|
+
this._config = savedConfig;
|
|
486
|
+
this.currentModelInfo = savedModelInfo;
|
|
487
|
+
this.binaryPath = savedBinaryPath;
|
|
488
|
+
this.calibrating = false;
|
|
489
|
+
if (this.orchestrator) {
|
|
490
|
+
emitFn?.({
|
|
491
|
+
phase: 'restoring-llm',
|
|
492
|
+
comboIndex: Math.max(0, comboCountForProgress - 1),
|
|
493
|
+
comboCount: comboCountForProgress,
|
|
494
|
+
sizeIndex: Math.max(0, sizes.length - 1),
|
|
495
|
+
sizeCount: sizes.length,
|
|
496
|
+
overallPercent: succeeded ? 100 : lastOverallPercent,
|
|
497
|
+
});
|
|
498
|
+
await this.orchestrator.reloadLLM();
|
|
499
|
+
}
|
|
500
|
+
if (succeeded) {
|
|
501
|
+
emitFn?.({
|
|
502
|
+
phase: 'done',
|
|
503
|
+
comboIndex: Math.max(0, comboCountForProgress - 1),
|
|
504
|
+
comboCount: comboCountForProgress,
|
|
505
|
+
sizeIndex: Math.max(0, sizes.length - 1),
|
|
506
|
+
sizeCount: sizes.length,
|
|
507
|
+
overallPercent: 100,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
isCalibrating() {
|
|
513
|
+
return this.calibrating;
|
|
514
|
+
}
|
|
515
|
+
calibrationAbortError(runs) {
|
|
516
|
+
return new ServerError('Calibration aborted', {
|
|
517
|
+
code: 'CALIBRATION_ABORTED',
|
|
518
|
+
runs: [...runs],
|
|
519
|
+
suggestion: 'Partial results are available in error.details.runs',
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
async runCalibrationGeneration(params) {
|
|
523
|
+
const genConfig = {
|
|
524
|
+
prompt: params.prompt,
|
|
525
|
+
width: params.size.width,
|
|
526
|
+
height: params.size.height,
|
|
527
|
+
steps: params.steps,
|
|
528
|
+
seed: params.seed,
|
|
529
|
+
sampler: params.sampler,
|
|
530
|
+
onProgress: (_currentStep, _totalSteps, _stage, percentage) => {
|
|
531
|
+
if (percentage !== undefined) {
|
|
532
|
+
params.onGenerationProgress(percentage);
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
if (params.cfgScale !== undefined) {
|
|
537
|
+
genConfig.cfgScale = params.cfgScale;
|
|
538
|
+
}
|
|
539
|
+
return this.executeImageGeneration(genConfig, params.combo);
|
|
540
|
+
}
|
|
541
|
+
snapshotStageMs() {
|
|
542
|
+
const stage = {};
|
|
543
|
+
if (this.loadStartTime && this.loadEndTime) {
|
|
544
|
+
stage.loadMs = this.loadEndTime - this.loadStartTime;
|
|
545
|
+
}
|
|
546
|
+
if (this.diffusionStartTime && this.diffusionEndTime) {
|
|
547
|
+
stage.diffusionMs = this.diffusionEndTime - this.diffusionStartTime;
|
|
548
|
+
}
|
|
549
|
+
if (this.vaeStartTime && this.vaeEndTime) {
|
|
550
|
+
stage.decodeMs = this.vaeEndTime - this.vaeStartTime;
|
|
551
|
+
}
|
|
552
|
+
return stage;
|
|
553
|
+
}
|
|
554
|
+
classifyCalibrationFailure(error) {
|
|
555
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
556
|
+
let stderr = '';
|
|
557
|
+
if (error instanceof GenaiElectronError && error.details && typeof error.details === 'object') {
|
|
558
|
+
const detailStderr = error.details.stderr;
|
|
559
|
+
if (typeof detailStderr === 'string') {
|
|
560
|
+
stderr = detailStderr;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const text = `${message}\n${stderr}`;
|
|
564
|
+
const isOom = DIFFUSION_CALIBRATION_DEFAULTS.oomPatterns.some((pattern) => pattern.test(text));
|
|
565
|
+
return { status: isOom ? 'oom' : 'error', message };
|
|
566
|
+
}
|
|
176
567
|
async generateImage(config) {
|
|
177
568
|
if (this._status !== 'running') {
|
|
178
569
|
throw new ServerError('Server is not running', {
|
|
@@ -447,7 +838,7 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
447
838
|
req.on('error', reject);
|
|
448
839
|
});
|
|
449
840
|
}
|
|
450
|
-
async executeImageGeneration(config) {
|
|
841
|
+
async executeImageGeneration(config, flagOverrides) {
|
|
451
842
|
const startTime = Date.now();
|
|
452
843
|
if (!this.currentModelInfo) {
|
|
453
844
|
throw new ServerError('Model information not available', {
|
|
@@ -459,7 +850,7 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
459
850
|
seed: config.seed === undefined || config.seed < 0 ? this.generateRandomSeed() : config.seed,
|
|
460
851
|
};
|
|
461
852
|
this.initializeProgressTracking(normalizedConfig);
|
|
462
|
-
const optimizations = await this.computeDiffusionOptimizations();
|
|
853
|
+
const optimizations = await this.computeDiffusionOptimizations(flagOverrides);
|
|
463
854
|
const args = this.buildDiffusionArgs(normalizedConfig, this.currentModelInfo, optimizations);
|
|
464
855
|
const outputPath = getTempPath(`sd-output-${Date.now()}.png`);
|
|
465
856
|
args.push('-o', outputPath);
|
|
@@ -574,7 +965,7 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
574
965
|
}
|
|
575
966
|
return images;
|
|
576
967
|
}
|
|
577
|
-
async computeDiffusionOptimizations() {
|
|
968
|
+
async computeDiffusionOptimizations(flagOverrides) {
|
|
578
969
|
const serverConfig = this._config;
|
|
579
970
|
const modelSize = this.currentModelInfo?.size ?? 0;
|
|
580
971
|
const modelFootprint = modelSize * DIFFUSION_VRAM_THRESHOLDS.modelOverheadMultiplier;
|
|
@@ -590,15 +981,10 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
590
981
|
}
|
|
591
982
|
else {
|
|
592
983
|
const headroom = gpu.vram - modelFootprint;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
autoVaeOnCpu = isCuda ? false : headroom < DIFFUSION_VRAM_THRESHOLDS.vaeOnCpuHeadroomBytes;
|
|
598
|
-
autoOffloadToCpu = isCuda ? false : modelFootprint > gpu.vram * 0.85;
|
|
599
|
-
if (!isCuda &&
|
|
600
|
-
gpu.vramAvailable !== undefined &&
|
|
601
|
-
gpu.vramAvailable - modelFootprint < 2 * 1024 ** 3) {
|
|
984
|
+
autoClipOnCpu = headroom < DIFFUSION_VRAM_THRESHOLDS.clipOnCpuHeadroomBytes;
|
|
985
|
+
autoVaeOnCpu = headroom < DIFFUSION_VRAM_THRESHOLDS.vaeOnCpuHeadroomBytes;
|
|
986
|
+
autoOffloadToCpu = modelFootprint > gpu.vram * 0.85;
|
|
987
|
+
if (gpu.vramAvailable !== undefined && gpu.vramAvailable - modelFootprint < 2 * 1024 ** 3) {
|
|
602
988
|
autoClipOnCpu = true;
|
|
603
989
|
}
|
|
604
990
|
}
|
|
@@ -610,25 +996,17 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
610
996
|
}
|
|
611
997
|
const hasLLMComponent = !!this.currentModelInfo?.components?.llm;
|
|
612
998
|
const autoDiffusionFlashAttention = hasLLMComponent;
|
|
613
|
-
const clipOnCpu = serverConfig.clipOnCpu ?? autoClipOnCpu;
|
|
614
|
-
const vaeOnCpu = serverConfig.vaeOnCpu ?? autoVaeOnCpu;
|
|
615
|
-
const offloadToCpu = serverConfig.offloadToCpu ?? autoOffloadToCpu;
|
|
616
|
-
const diffusionFlashAttention =
|
|
999
|
+
const clipOnCpu = flagOverrides?.clipOnCpu ?? serverConfig.clipOnCpu ?? autoClipOnCpu;
|
|
1000
|
+
const vaeOnCpu = flagOverrides?.vaeOnCpu ?? serverConfig.vaeOnCpu ?? autoVaeOnCpu;
|
|
1001
|
+
const offloadToCpu = flagOverrides?.offloadToCpu ?? serverConfig.offloadToCpu ?? autoOffloadToCpu;
|
|
1002
|
+
const diffusionFlashAttention = flagOverrides?.diffusionFlashAttention ??
|
|
1003
|
+
serverConfig.diffusionFlashAttention ??
|
|
1004
|
+
autoDiffusionFlashAttention;
|
|
617
1005
|
const batchSize = serverConfig.batchSize;
|
|
1006
|
+
this.lastResolvedOptimizations = { clipOnCpu, vaeOnCpu, offloadToCpu, diffusionFlashAttention };
|
|
618
1007
|
await this.logManager?.write(`VRAM optimizations: clipOnCpu=${clipOnCpu}, vaeOnCpu=${vaeOnCpu}, offloadToCpu=${offloadToCpu}, diffusionFa=${diffusionFlashAttention}${batchSize !== undefined ? `, batchSize=${batchSize}` : ''} (auto: clip=${autoClipOnCpu}, vae=${autoVaeOnCpu}, offload=${autoOffloadToCpu}, fa=${autoDiffusionFlashAttention})`, 'info');
|
|
619
1008
|
return { clipOnCpu, vaeOnCpu, offloadToCpu, diffusionFlashAttention, batchSize };
|
|
620
1009
|
}
|
|
621
|
-
async isInstalledVariantCuda() {
|
|
622
|
-
try {
|
|
623
|
-
const variantCachePath = `${PATHS.binaries.diffusion}/.variant.json`;
|
|
624
|
-
const content = await fs.readFile(variantCachePath, 'utf-8');
|
|
625
|
-
const cache = JSON.parse(content);
|
|
626
|
-
return cache.variant === 'cuda';
|
|
627
|
-
}
|
|
628
|
-
catch {
|
|
629
|
-
return false;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
1010
|
buildDiffusionArgs(config, modelInfo, optimizations) {
|
|
633
1011
|
const args = [];
|
|
634
1012
|
if (modelInfo.components) {
|
|
@@ -736,7 +1114,7 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
736
1114
|
this.totalEstimatedTime = loadTime + diffusionTime + vaeTime;
|
|
737
1115
|
}
|
|
738
1116
|
processStdoutForProgress(data, config) {
|
|
739
|
-
if (data.includes('loading tensors from')) {
|
|
1117
|
+
if (data.includes('loading tensors from') || data.includes('loading model from')) {
|
|
740
1118
|
this.currentStage = 'loading';
|
|
741
1119
|
this.loadStartTime = Date.now();
|
|
742
1120
|
this.reportProgress(config);
|
|
@@ -768,7 +1146,7 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
768
1146
|
config.onProgress(0, 0, 'decoding', 100);
|
|
769
1147
|
}
|
|
770
1148
|
}
|
|
771
|
-
const progressMatch = data.match(/\|\s*(\d+)\/(\d+)\s
|
|
1149
|
+
const progressMatch = data.match(/\|\s*(\d+)\/(\d+)\s*-\s*[\d.]+\s*(?:it\/s|s\/it)/);
|
|
772
1150
|
if (progressMatch && progressMatch[1] && progressMatch[2]) {
|
|
773
1151
|
const current = parseInt(progressMatch[1], 10);
|
|
774
1152
|
const total = parseInt(progressMatch[2], 10);
|
|
@@ -787,6 +1165,21 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
787
1165
|
this.reportProgress(config);
|
|
788
1166
|
}
|
|
789
1167
|
}
|
|
1168
|
+
const byteMatch = data.match(/\|\s*(\d+)\/(\d+)\s*-\s*[\d.]+\s*(?:B|KB|MB|GB)\/s/);
|
|
1169
|
+
if (byteMatch && byteMatch[1] && byteMatch[2]) {
|
|
1170
|
+
const current = parseInt(byteMatch[1], 10);
|
|
1171
|
+
const total = parseInt(byteMatch[2], 10);
|
|
1172
|
+
if (this.currentStage === 'loading') {
|
|
1173
|
+
this.loadProgress = { current, total };
|
|
1174
|
+
this.reportProgress(config);
|
|
1175
|
+
}
|
|
1176
|
+
else if (this.currentStage === undefined) {
|
|
1177
|
+
this.currentStage = 'loading';
|
|
1178
|
+
this.loadStartTime = Date.now();
|
|
1179
|
+
this.loadProgress = { current, total };
|
|
1180
|
+
this.reportProgress(config);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
790
1183
|
}
|
|
791
1184
|
reportProgress(config) {
|
|
792
1185
|
if (!config.onProgress || !this.generationStartTime)
|
|
@@ -899,4 +1292,47 @@ export class DiffusionServerManager extends ServerManager {
|
|
|
899
1292
|
}
|
|
900
1293
|
}
|
|
901
1294
|
}
|
|
1295
|
+
function medianOf(values) {
|
|
1296
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1297
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1298
|
+
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
1299
|
+
}
|
|
1300
|
+
function countForcedFlags(combo) {
|
|
1301
|
+
let count = 0;
|
|
1302
|
+
if (combo.clipOnCpu !== undefined)
|
|
1303
|
+
count++;
|
|
1304
|
+
if (combo.vaeOnCpu !== undefined)
|
|
1305
|
+
count++;
|
|
1306
|
+
if (combo.offloadToCpu !== undefined)
|
|
1307
|
+
count++;
|
|
1308
|
+
if (combo.diffusionFlashAttention !== undefined)
|
|
1309
|
+
count++;
|
|
1310
|
+
return count;
|
|
1311
|
+
}
|
|
1312
|
+
export function pickRecommended(runs, tolerancePct) {
|
|
1313
|
+
const bySize = new Map();
|
|
1314
|
+
for (const run of runs) {
|
|
1315
|
+
if (run.status !== 'ok' || run.timeTakenMs === undefined) {
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
const key = `${run.size.width}x${run.size.height}`;
|
|
1319
|
+
const list = bySize.get(key);
|
|
1320
|
+
if (list) {
|
|
1321
|
+
list.push(run);
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
bySize.set(key, [run]);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
const recommended = {};
|
|
1328
|
+
for (const [key, okRuns] of bySize) {
|
|
1329
|
+
const fastest = okRuns.reduce((a, b) => (b.timeTakenMs < a.timeTakenMs ? b : a));
|
|
1330
|
+
const threshold = fastest.timeTakenMs * (1 + tolerancePct / 100);
|
|
1331
|
+
const winner = okRuns
|
|
1332
|
+
.filter((run) => run.timeTakenMs <= threshold)
|
|
1333
|
+
.sort((a, b) => countForcedFlags(a.combo) - countForcedFlags(b.combo) || a.timeTakenMs - b.timeTakenMs)[0];
|
|
1334
|
+
recommended[key] = winner.combo;
|
|
1335
|
+
}
|
|
1336
|
+
return recommended;
|
|
1337
|
+
}
|
|
902
1338
|
//# sourceMappingURL=DiffusionServerManager.js.map
|