Ver código fonte

Merge branch 'load-options': load settings, compared and reloaded

fszontagh 1 mês atrás
pai
commit
99f682d9fb

+ 6 - 0
nodes/sdcpp/sdcpp-health.js

@@ -69,6 +69,7 @@ const outputSchema = {
         upscalerLoaded: { type: 'boolean' },
         upscalerName: { type: 'string' },
         loadedComponents: { type: 'object' },
+        loadOptions: { type: 'object', description: 'The settings the loaded model was loaded with - flash attention, streaming, VRAM budget and the rest' },
         memory: { type: 'object', description: 'Memory snapshot the server reports' },
         features: { type: 'object', description: 'Feature flags, including whether authentication is required' },
         version: { type: 'string' },
@@ -102,6 +103,7 @@ function unhealthy(server, reason, responseMs) {
         upscalerLoaded: false,
         upscalerName: '',
         loadedComponents: {},
+        loadOptions: {},
         memory: {},
         features: {},
         version: '',
@@ -191,6 +193,10 @@ async function execute(config, input, context) {
         upscalerLoaded: health.upscaler_loaded === true,
         upscalerName: health.upscaler_name || '',
         loadedComponents: health.loaded_components || {},
+        // What the model was loaded WITH, not just which model it is. A node
+        // that has to guarantee particular settings needs this to tell whether
+        // the right model is loaded the right way.
+        loadOptions: health.load_options || {},
         memory: health.memory || {},
         features: health.features || {},
         version: health.version || '',

+ 176 - 11
nodes/sdcpp/sdcpp-model-load.js

@@ -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

+ 19 - 0
tests/nodes/sdcpp-load-options-mismatch.json

@@ -0,0 +1,19 @@
+{
+  "name": "diag-load-options-mismatch",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "health", "name": "Health", "type": "sdcpp-health", "position": {"x": 0, "y": 100},
+     "config": {"serverUrl": "http://mulan:8077"}},
+    {"id": "load", "name": "Load", "type": "sdcpp-model-load", "position": {"x": 0, "y": 220},
+     "config": {"serverUrl": "http://mulan:8077",
+                "credentialId": "cred_62748b40-e9d1-4659-b175-6dbc691cc0b1",
+                "modelName": "{{$node[\"Health\"].modelName}}",
+                "streamLayers": false,
+                "whenDifferent": "fail"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "health", "targetInput": "data"},
+    {"sourceNodeId": "health", "sourceOutput": "main", "targetNodeId": "load", "targetInput": "data"}
+  ],
+  "expect": {"load": {"status": "failed", "errorContains": "not with the settings"}}
+}

+ 89 - 16
tests/nodes/sdcpp-model-load-idempotent.json

@@ -1,25 +1,98 @@
 {
   "name": "verify-sdcpp-model-load-idempotent",
   "nodes": [
-    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
-    {"id": "srv", "name": "SD.cpp Server", "type": "configurator", "position": {"x": -260, "y": 100},
-     "config": {"label": "SD.cpp Server", "settings": [
-       {"name": "serverUrl", "value": "http://mulan:8077"},
-       {"name": "credentialId", "value": "cred_62748b40-e9d1-4659-b175-6dbc691cc0b1"}
-     ]}},
-    {"id": "health", "name": "Health", "type": "sdcpp-health", "position": {"x": 0, "y": 200},
-     "config": {}},
-    {"id": "load", "name": "Load", "type": "sdcpp-model-load", "position": {"x": 0, "y": 320},
-     "config": {"modelName": "{{$node[\"Health\"].modelName}}"}}
+    {
+      "id": "n1",
+      "name": "Trigger",
+      "type": "click-trigger",
+      "position": {
+        "x": 0,
+        "y": 0
+      },
+      "config": {}
+    },
+    {
+      "id": "srv",
+      "name": "SD.cpp Server",
+      "type": "configurator",
+      "position": {
+        "x": -260,
+        "y": 100
+      },
+      "config": {
+        "label": "SD.cpp Server",
+        "settings": [
+          {
+            "name": "serverUrl",
+            "value": "http://mulan:8077"
+          },
+          {
+            "name": "credentialId",
+            "value": "cred_62748b40-e9d1-4659-b175-6dbc691cc0b1"
+          }
+        ]
+      }
+    },
+    {
+      "id": "health",
+      "name": "Health",
+      "type": "sdcpp-health",
+      "position": {
+        "x": 0,
+        "y": 200
+      },
+      "config": {}
+    },
+    {
+      "id": "load",
+      "name": "Load",
+      "type": "sdcpp-model-load",
+      "position": {
+        "x": 0,
+        "y": 320
+      },
+      "config": {
+        "modelName": "{{$node[\"Health\"].modelName}}",
+        "streamLayers": true,
+        "eagerLoad": true
+      }
+    }
   ],
   "connections": [
-    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "health", "targetInput": "data"},
-    {"sourceNodeId": "health", "sourceOutput": "main", "targetNodeId": "load", "targetInput": "data"},
-    {"sourceNodeId": "srv", "sourceOutput": "main", "targetNodeId": "health", "targetInput": "config"},
-    {"sourceNodeId": "srv", "sourceOutput": "main", "targetNodeId": "load", "targetInput": "config"}
+    {
+      "sourceNodeId": "n1",
+      "sourceOutput": "main",
+      "targetNodeId": "health",
+      "targetInput": "data"
+    },
+    {
+      "sourceNodeId": "health",
+      "sourceOutput": "main",
+      "targetNodeId": "load",
+      "targetInput": "data"
+    },
+    {
+      "sourceNodeId": "srv",
+      "sourceOutput": "main",
+      "targetNodeId": "health",
+      "targetInput": "config"
+    },
+    {
+      "sourceNodeId": "srv",
+      "sourceOutput": "main",
+      "targetNodeId": "load",
+      "targetInput": "config"
+    }
   ],
   "expectStatus": "completed",
   "expect": {
-    "load": {"status": "completed", "output": {"alreadyLoaded": true, "loaded": false}}
+    "load": {
+      "status": "completed",
+      "output": {
+        "alreadyLoaded": true,
+        "loaded": false,
+        "reloadedFor": []
+      }
+    }
   }
-}
+}