|
|
@@ -25,6 +25,17 @@ const credentialTypes = [
|
|
|
|
|
|
const configSchema = {
|
|
|
type: 'object',
|
|
|
+ uiGroups: [
|
|
|
+ { title: 'Connection', fields: ['serverUrl', 'credentialId'] },
|
|
|
+ { title: 'Model', fields: ['modelName', 'modelType', 'whenDifferent', 'force'] },
|
|
|
+ { title: 'Components', fields: ['vae', 'clipL', 'clipG', 't5xxl', 'llm', 'taesd', 'controlnet'] },
|
|
|
+ { title: 'Loading', fields: ['flashAttn', 'diffusionFlashAttn', 'enableMmap', 'eagerLoad',
|
|
|
+ 'streamLayers', 'maxVram', 'nThreads', 'weightType'] },
|
|
|
+ { title: 'Advanced', fields: ['vaeFormat', 'prediction', 'rngType', 'samplerRngType',
|
|
|
+ 'loraApplyMode', 'vaeConvDirect', 'diffusionConvDirect',
|
|
|
+ 'taePreviewOnly', 'backend', 'paramsBackend', 'rpcServers',
|
|
|
+ 'modelArgs', 'tensorTypeRules', 'options', 'timeout'] }
|
|
|
+ ],
|
|
|
// Pressing this asks the server what it has loaded and writes it into the
|
|
|
// settings below - the model and every component it reports - so a workflow
|
|
|
// can be built from a server that is already set up the way it should be,
|
|
|
@@ -43,7 +54,24 @@ const configSchema = {
|
|
|
'loadedComponents.t5xxl': 't5xxl',
|
|
|
'loadedComponents.llm': 'llm',
|
|
|
'loadedComponents.taesd': 'taesd',
|
|
|
- 'loadedComponents.controlnet': 'controlnet'
|
|
|
+ 'loadedComponents.controlnet': 'controlnet',
|
|
|
+ 'loadOptions.flash_attn': 'flashAttn',
|
|
|
+ 'loadOptions.diffusion_flash_attn': 'diffusionFlashAttn',
|
|
|
+ 'loadOptions.enable_mmap': 'enableMmap',
|
|
|
+ 'loadOptions.eager_load': 'eagerLoad',
|
|
|
+ 'loadOptions.stream_layers': 'streamLayers',
|
|
|
+ 'loadOptions.max_vram': 'maxVram',
|
|
|
+ 'loadOptions.n_threads': 'nThreads',
|
|
|
+ 'loadOptions.vae_format': 'vaeFormat',
|
|
|
+ 'loadOptions.rng_type': 'rngType',
|
|
|
+ 'loadOptions.lora_apply_mode': 'loraApplyMode',
|
|
|
+ 'loadOptions.vae_conv_direct': 'vaeConvDirect',
|
|
|
+ 'loadOptions.diffusion_conv_direct': 'diffusionConvDirect',
|
|
|
+ 'loadOptions.tae_preview_only': 'taePreviewOnly',
|
|
|
+ 'loadOptions.backend': 'backend',
|
|
|
+ 'loadOptions.params_backend': 'paramsBackend',
|
|
|
+ 'loadOptions.rpc_servers': 'rpcServers',
|
|
|
+ 'loadOptions.model_args': 'modelArgs'
|
|
|
}
|
|
|
},
|
|
|
properties: {
|
|
|
@@ -167,8 +195,50 @@ const configSchema = {
|
|
|
needs: ['serverUrl', 'credentialId']
|
|
|
}
|
|
|
},
|
|
|
+ flashAttn: { type: 'boolean', title: 'Flash Attention', description: 'For CLIP and T5. A large speed and memory win on modern GPUs' },
|
|
|
+ diffusionFlashAttn: { type: 'boolean', title: 'Flash Attention (diffusion)', description: 'Flash attention for the diffusion model specifically' },
|
|
|
+ enableMmap: { type: 'boolean', title: 'Memory-map Weights', description: 'Recommended for large files' },
|
|
|
+ eagerLoad: { type: 'boolean', title: 'Eager Load', description: 'Move every parameter to the compute backend at load time instead of on demand' },
|
|
|
+ streamLayers: { type: 'boolean', title: 'Stream Layers', description: 'Stream diffusion layers when the model does not fit in VRAM. Pair with a VRAM budget' },
|
|
|
+ maxVram: { type: 'number', title: 'VRAM Budget (GiB)', description: 'Budget for segmented parameter offload. 0 leaves it to the server' },
|
|
|
+ nThreads: { type: 'number', title: 'CPU Threads', description: '-1 lets the server decide' },
|
|
|
+ weightType: {
|
|
|
+ type: 'string', title: 'Weight Type',
|
|
|
+ enum: ['', 'f32', 'f16', 'bf16', 'q8_0', 'q5_0', 'q5_1', 'q4_0', 'q4_1', 'q4_k', 'q5_k', 'q6_k', 'q8_k', 'q3_k', 'q2_k', 'mxfp4', 'nvfp4', 'q1_0'],
|
|
|
+ default: '', description: 'Force a quantisation. Empty takes it from the file'
|
|
|
+ },
|
|
|
+ vaeFormat: {
|
|
|
+ type: 'string', title: 'VAE Format',
|
|
|
+ enum: ['', 'auto', 'flux', 'sd3', 'flux2', 'wan'],
|
|
|
+ default: '', description: 'Override VAE format detection'
|
|
|
+ },
|
|
|
+ prediction: {
|
|
|
+ type: 'string', title: 'Prediction Type',
|
|
|
+ enum: ['', 'eps', 'v', 'edm_v', 'sd3_flow', 'flux_flow', 'flux2_flow', 'sefi_flow', 'minit2i_flow'],
|
|
|
+ default: '', description: 'Override the prediction type. Empty is detected'
|
|
|
+ },
|
|
|
+ rngType: {
|
|
|
+ type: 'string', title: 'RNG', enum: ['', 'cuda', 'std_default', 'cpu'],
|
|
|
+ default: '', description: 'Affects whether a seed reproduces across backends'
|
|
|
+ },
|
|
|
+ samplerRngType: {
|
|
|
+ type: 'string', title: 'Sampler RNG', enum: ['', 'cuda', 'std_default', 'cpu'],
|
|
|
+ default: '', description: 'Override the RNG for sampling only'
|
|
|
+ },
|
|
|
+ loraApplyMode: {
|
|
|
+ type: 'string', title: 'LoRA Apply Mode', enum: ['', 'auto', 'immediately', 'at_runtime'],
|
|
|
+ default: '', description: 'When a LoRA named in a prompt is applied'
|
|
|
+ },
|
|
|
+ vaeConvDirect: { type: 'boolean', title: 'Direct VAE Convolution', description: 'Use the ggml_conv2d_direct path for the VAE' },
|
|
|
+ diffusionConvDirect: { type: 'boolean', title: 'Direct Diffusion Convolution', description: 'Use the ggml_conv2d_direct path for the diffusion model' },
|
|
|
+ taePreviewOnly: { type: 'boolean', title: 'TAESD For Preview Only', description: 'Load TAESD purely to render progress previews, skipping the full VAE' },
|
|
|
+ backend: { type: 'string', title: 'Backend', description: 'Per-component placement, such as te=cpu,vae=cpu,controlnet=cpu' },
|
|
|
+ paramsBackend: { type: 'string', title: 'Parameter Backend', description: 'Global parameter placement, such as *=cpu to hold weights in system RAM' },
|
|
|
+ rpcServers: { type: 'string', title: 'RPC Servers', description: 'Comma separated RPC backend endpoints' },
|
|
|
+ modelArgs: { type: 'string', title: 'Model Args', description: 'Architecture-specific key=value knobs, comma separated' },
|
|
|
+ tensorTypeRules: { type: 'string', title: 'Tensor Type Rules', description: 'Per-tensor weight overrides using regex, such as ^vae\\.=f16' },
|
|
|
options: {
|
|
|
- type: 'object', title: 'Load Options',
|
|
|
+ type: 'object', title: 'Other Load Options',
|
|
|
description: 'Extra load options passed through, such as flash_attn, enable_mmap, weight_type, stream_layers or max_vram'
|
|
|
},
|
|
|
whenDifferent: {
|
|
|
@@ -201,7 +271,8 @@ const outputSchema = {
|
|
|
modelType: { type: 'string' },
|
|
|
architecture: { type: 'string', description: 'Architecture the server detected, which decides generation defaults' },
|
|
|
loaded: { type: 'boolean', description: 'True when this node performed a load' },
|
|
|
- alreadyLoaded: { type: 'boolean', description: 'True when the right model was already in place and nothing was done' },
|
|
|
+ alreadyLoaded: { type: 'boolean', description: 'True when the right model, with the right settings, was already in place' },
|
|
|
+ reloadedFor: { type: 'array', description: 'Settings that differed on an otherwise-correct model, when that is why it was reloaded' },
|
|
|
previousModel: { type: 'string', description: 'What was loaded before, when this node swapped it' },
|
|
|
loadedComponents: { type: 'object' },
|
|
|
elapsedMs: { type: 'number' }
|
|
|
@@ -300,6 +371,81 @@ function putIfSet(target, key, value) {
|
|
|
target[key] = value;
|
|
|
}
|
|
|
|
|
|
+const LOAD_OPTIONS = [
|
|
|
+ { setting: 'flashAttn', server: 'flash_attn' },
|
|
|
+ { setting: 'diffusionFlashAttn', server: 'diffusion_flash_attn' },
|
|
|
+ { setting: 'enableMmap', server: 'enable_mmap' },
|
|
|
+ { setting: 'eagerLoad', server: 'eager_load' },
|
|
|
+ { setting: 'streamLayers', server: 'stream_layers' },
|
|
|
+ { setting: 'maxVram', server: 'max_vram' },
|
|
|
+ { setting: 'nThreads', server: 'n_threads' },
|
|
|
+ { setting: 'weightType', server: 'weight_type' },
|
|
|
+ { setting: 'vaeFormat', server: 'vae_format' },
|
|
|
+ { setting: 'prediction', server: 'prediction' },
|
|
|
+ { setting: 'rngType', server: 'rng_type' },
|
|
|
+ { setting: 'samplerRngType', server: 'sampler_rng_type' },
|
|
|
+ { setting: 'loraApplyMode', server: 'lora_apply_mode' },
|
|
|
+ { setting: 'vaeConvDirect', server: 'vae_conv_direct' },
|
|
|
+ { setting: 'diffusionConvDirect', server: 'diffusion_conv_direct' },
|
|
|
+ { setting: 'taePreviewOnly', server: 'tae_preview_only' },
|
|
|
+ { setting: 'backend', server: 'backend' },
|
|
|
+ { setting: 'paramsBackend', server: 'params_backend' },
|
|
|
+ { setting: 'rpcServers', server: 'rpc_servers' },
|
|
|
+ { setting: 'modelArgs', server: 'model_args' },
|
|
|
+ { setting: 'tensorTypeRules', server: 'tensor_type_rules' },
|
|
|
+];
|
|
|
+
|
|
|
+
|
|
|
+// The options this node asks for, as the server names them. Only settings that
|
|
|
+// were actually filled in are included: an untouched setting means "whatever
|
|
|
+// the server does", not "the default", so it is neither sent nor compared.
|
|
|
+function wantedOptions(config) {
|
|
|
+ var wanted = {};
|
|
|
+ for (var i = 0; i < LOAD_OPTIONS.length; i++) {
|
|
|
+ var entry = LOAD_OPTIONS[i];
|
|
|
+ var value = config[entry.setting];
|
|
|
+ if (value === undefined || value === null || value === '') continue;
|
|
|
+ if (typeof value === 'number' && !isFinite(value)) continue;
|
|
|
+ wanted[entry.server] = value;
|
|
|
+ }
|
|
|
+ if (config.options && typeof config.options === 'object') {
|
|
|
+ var keys = Object.keys(config.options);
|
|
|
+ for (var k = 0; k < keys.length; k++) {
|
|
|
+ var extra = config.options[keys[k]];
|
|
|
+ if (extra !== undefined && extra !== null && extra !== '') {
|
|
|
+ wanted[keys[k]] = extra;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return wanted;
|
|
|
+}
|
|
|
+
|
|
|
+// Which of them the server is not currently loaded with.
|
|
|
+//
|
|
|
+// This is what makes the node able to correct a server someone else changed:
|
|
|
+// the same model loaded with streaming off is not the same thing as the model
|
|
|
+// this workflow needs, and reloading it is the whole point of saying so here.
|
|
|
+function optionsThatDiffer(wanted, current) {
|
|
|
+ var differing = [];
|
|
|
+ var keys = Object.keys(wanted);
|
|
|
+ for (var i = 0; i < keys.length; i++) {
|
|
|
+ var key = keys[i];
|
|
|
+ var have = current ? current[key] : undefined;
|
|
|
+ var want = wanted[key];
|
|
|
+ // Numbers arrive as 0 or 0.0 depending on the field, and a boolean may
|
|
|
+ // come back as a string from a form, so compare on value rather than
|
|
|
+ // on type.
|
|
|
+ var same = (typeof want === 'number' || typeof have === 'number')
|
|
|
+ ? Number(have) === Number(want)
|
|
|
+ : String(have) === String(want);
|
|
|
+ if (!same) {
|
|
|
+ differing.push(key + ': server has ' + JSON.stringify(have) +
|
|
|
+ ', this node wants ' + JSON.stringify(want));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return differing;
|
|
|
+}
|
|
|
+
|
|
|
async function execute(config, input, context) {
|
|
|
const server = normalizeServer(config.serverUrl);
|
|
|
const timeout = config.timeout > 0 ? config.timeout : 300000;
|
|
|
@@ -318,24 +464,40 @@ async function execute(config, input, context) {
|
|
|
const current = health.model_name || '';
|
|
|
const sameModel = current === modelName;
|
|
|
|
|
|
- if (sameModel && config.force !== true) {
|
|
|
- smartbotic.log.info('SD.cpp: ' + modelName + ' is already loaded, nothing to do');
|
|
|
+ // The same model loaded with different settings is not the model this
|
|
|
+ // workflow asked for. The server swaps models between queue items, so what
|
|
|
+ // is loaded now may have been put there by something else entirely.
|
|
|
+ const wanted = wantedOptions(config);
|
|
|
+ const differing = optionsThatDiffer(wanted, health.load_options || {});
|
|
|
+
|
|
|
+ if (sameModel && differing.length === 0 && config.force !== true) {
|
|
|
+ smartbotic.log.info('SD.cpp: ' + modelName + ' is already loaded with the wanted settings');
|
|
|
return {
|
|
|
modelName: current,
|
|
|
modelType: health.model_type || '',
|
|
|
architecture: health.model_architecture || '',
|
|
|
loaded: false,
|
|
|
alreadyLoaded: true,
|
|
|
+ reloadedFor: [],
|
|
|
previousModel: '',
|
|
|
loadedComponents: health.loaded_components || {},
|
|
|
elapsedMs: Date.now() - startedAt
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- if (!sameModel && (config.whenDifferent || 'load') === 'fail') {
|
|
|
- throw new Error('SD.cpp: this workflow expects "' + modelName + '" to be loaded, but ' +
|
|
|
- (current ? 'the server has "' + current + '"' : 'no model is loaded') +
|
|
|
- '. Set When A Different Model Is Loaded to "load" to swap it automatically');
|
|
|
+ if ((config.whenDifferent || 'load') === 'fail' && (!sameModel || differing.length > 0)) {
|
|
|
+ if (!sameModel) {
|
|
|
+ throw new Error('SD.cpp: this workflow expects "' + modelName + '" to be loaded, but ' +
|
|
|
+ (current ? 'the server has "' + current + '"' : 'no model is loaded') +
|
|
|
+ '. Set When A Different Model Is Loaded to "load" to swap it automatically');
|
|
|
+ }
|
|
|
+ throw new Error('SD.cpp: "' + modelName + '" is loaded, but not with the settings this ' +
|
|
|
+ 'workflow needs - ' + differing.join('; ') +
|
|
|
+ '. Set When A Different Model Is Loaded to "load" to reload it');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (sameModel && differing.length > 0) {
|
|
|
+ smartbotic.log.info('SD.cpp: reloading ' + modelName + ' because ' + differing.join('; '));
|
|
|
}
|
|
|
|
|
|
const credential = readCredential(config.credentialId);
|
|
|
@@ -350,8 +512,8 @@ async function execute(config, input, context) {
|
|
|
putIfSet(body, 'llm', config.llm);
|
|
|
putIfSet(body, 'taesd', config.taesd);
|
|
|
putIfSet(body, 'controlnet', config.controlnet);
|
|
|
- if (config.options && typeof config.options === 'object') {
|
|
|
- body.options = config.options;
|
|
|
+ if (Object.keys(wanted).length > 0) {
|
|
|
+ body.options = wanted;
|
|
|
}
|
|
|
|
|
|
smartbotic.log.info('SD.cpp: loading ' + modelName +
|
|
|
@@ -376,6 +538,9 @@ async function execute(config, input, context) {
|
|
|
architecture: after.model_architecture || '',
|
|
|
loaded: true,
|
|
|
alreadyLoaded: false,
|
|
|
+ // Empty when the model itself changed; otherwise the settings that
|
|
|
+ // forced a reload of a model that was already there.
|
|
|
+ reloadedFor: sameModel ? differing : [],
|
|
|
previousModel: sameModel ? '' : current,
|
|
|
loadedComponents: loaded.loaded_components || after.loaded_components || {},
|
|
|
elapsedMs: Date.now() - startedAt
|