From 6498fdb19acf02f3986c945aba2a36f4bba02724 Mon Sep 17 00:00:00 2001 From: Jason Hoku Date: Wed, 4 Nov 2026 17:32:49 -0800 Subject: [PATCH] docs(examples): Workflow Sweep example on an SDXL API graph (#6) The answer to "non standard workflow" grids: the sweep runs the user's exact exported graph per job (reference-image conditioning, ControlNet, custom nodes...), changing only the bound inputs. uscg-workflow-sweep-sdxl.json sweeps cfg x sampler x prompt (8 jobs) on an embedded SDXL graph; README section; test_examples covers it (8 jobs, 8 distinct fingerprints). Co-Authored-By: Claude Fable 5.1 --- examples/README.md | 10 + examples/uscg-workflow-sweep-sdxl.json | 312 +++++++++++++++++++++++++ tests/test_examples.py | 17 ++ 3 files changed, 339 insertions(+) create mode 100644 examples/uscg-workflow-sweep-sdxl.json diff --git a/examples/README.md b/examples/README.md index f330f4a..3510268 100644 --- a/examples/README.md +++ b/examples/README.md @@ -82,6 +82,16 @@ Times are approximate on an RTX 5060 Ti 16 GB, not counting the first model load * **Time:** TBD * **Notes:** SLG uses the official recipe, single block 22 for the first 0-50 % of the steps. Compare the two images to see what it does. CLIP type `moss_image`. A smaller `flux2-vae_small.safetensors` VAE also exists and can be picked in the Builder instead. The 9B text encoder is large; on a 16 GB card expect model swapping. +## Workflow Sweep on your own graph (`uscg-workflow-sweep-sdxl.json`) + +| File | Folder | +|---|---| +| `sd_xl_base_1.0.safetensors` | `models/checkpoints/` | + +* **What the grid tests:** one Workflow Sweep array that wraps a complete SDXL text-to-image graph (Load Checkpoint, two CLIP Text Encode, Empty Latent 1024x1024, KSampler, VAE Decode, Save Image) and sweeps three things on it: KSampler `cfg` 5 and 7 x `sampler_name` euler and dpmpp_2m x 2 positive prompts (one shared negative). **8 images** +* **Time:** TBD +* **Notes:** unlike the other examples, the Builder does not build the pipeline here. The sweep runs your exact exported graph once per job, changing only the inputs you bound, and collects the result into the Dashboard like any other run. This is how to grid-test a workflow the three-node grid cannot express, such as reference-image conditioning, ControlNet, multi-stage or custom-node graphs. To use your own: in ComfyUI use Export (API), open the Config Builder's **Workflow Sweep** section, load that file, pick the output image, and choose the inputs to sweep. Any API-format export works, for example the Flux.2 example in this folder could be exported and swept the same way. The embedded SDXL graph here is only a stand-in so the example runs with one checkpoint. + ## Make your own * Edit the Builder: add LoRAs or a whole LoRA folder, more samplers, schedulers, step counts, cfg values, resolutions or prompts. Every list is multiplied together. diff --git a/examples/uscg-workflow-sweep-sdxl.json b/examples/uscg-workflow-sweep-sdxl.json new file mode 100644 index 0000000..c707c15 --- /dev/null +++ b/examples/uscg-workflow-sweep-sdxl.json @@ -0,0 +1,312 @@ +{ + "id": "7a1e3c52-9d4b-5f6e-8a21-4b0c6d7e9f13", + "revision": 0, + "last_node_id": 4, + "last_link_id": 2, + "nodes": [ + { + "id": 1, + "type": "UltimateConfigBuilder", + "pos": [ + 0, + 0 + ], + "size": [ + 1250, + 1500 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "session_name", + "name": "session_name", + "type": "STRING", + "widget": { + "name": "session_name" + }, + "link": null + }, + { + "localized_name": "load_session", + "name": "load_session", + "type": "COMBO", + "widget": { + "name": "load_session" + }, + "link": null + }, + { + "localized_name": "samplers", + "name": "samplers", + "type": "STRING", + "widget": { + "name": "samplers" + }, + "link": null + }, + { + "localized_name": "schedulers", + "name": "schedulers", + "type": "STRING", + "widget": { + "name": "schedulers" + }, + "link": null + }, + { + "localized_name": "steps", + "name": "steps", + "type": "STRING", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "STRING", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "lora_config", + "name": "lora_config", + "type": "STRING", + "widget": { + "name": "lora_config" + }, + "link": null + }, + { + "localized_name": "include_none", + "name": "include_none", + "type": "BOOLEAN", + "widget": { + "name": "include_none" + }, + "link": null + }, + { + "localized_name": "model", + "name": "model", + "type": "STRING", + "widget": { + "name": "model" + }, + "link": null, + "shape": 7 + } + ], + "outputs": [ + { + "localized_name": "configs_json", + "name": "configs_json", + "type": "STRING", + "links": [ + 1 + ] + } + ], + "properties": { + "cnr_id": "ultimate-auto-sampler-config-grid-testing-suite", + "Node name for S&R": "UltimateConfigBuilder" + }, + "widgets_values": [ + "example_workflow_sweep_sdxl", + "None", + "euler", + "normal", + "25", + "7", + "{\n \"session_name\": \"example_workflow_sweep_sdxl\",\n \"config_name\": \"example_workflow_sweep_sdxl\",\n \"auto_save\": false,\n \"include_none\": false,\n \"label_mode\": false,\n \"global_positive_groups\": [\n [\n \"1girl, silver hair, red scarf, standing in a snowy street at dusk, looking back at viewer, masterpiece, best quality\",\n \"1girl, short brown hair, sundress, sitting on a seaside railing, wind, golden hour, masterpiece, best quality\"\n ]\n ],\n \"global_negative\": \"lowres, worst quality, bad anatomy, bad hands, watermark\",\n \"distribution_enabled\": false,\n \"worker_urls\": [],\n \"claim_timeout\": 600,\n \"use_master_encoding\": false,\n \"config_arrays\": [\n {\n \"name\": \"SDXL workflow sweep\",\n \"kind\": \"workflow\",\n \"workflow\": {\n \"source_name\": \"sdxl_text_to_image_api.json\",\n \"api_prompt\": {\n \"4\": {\n \"inputs\": {\n \"ckpt_name\": \"sd_xl_base_1.0.safetensors\"\n },\n \"class_type\": \"CheckpointLoaderSimple\",\n \"_meta\": {\n \"title\": \"Load Checkpoint\"\n }\n },\n \"5\": {\n \"inputs\": {\n \"width\": 1024,\n \"height\": 1024,\n \"batch_size\": 1\n },\n \"class_type\": \"EmptyLatentImage\",\n \"_meta\": {\n \"title\": \"Empty Latent Image\"\n }\n },\n \"6\": {\n \"inputs\": {\n \"text\": \"1girl, silver hair, red scarf, standing in a snowy street at dusk, masterpiece, best quality\",\n \"clip\": [\n \"4\",\n 1\n ]\n },\n \"class_type\": \"CLIPTextEncode\",\n \"_meta\": {\n \"title\": \"Positive Prompt\"\n }\n },\n \"7\": {\n \"inputs\": {\n \"text\": \"lowres, worst quality, bad anatomy, watermark\",\n \"clip\": [\n \"4\",\n 1\n ]\n },\n \"class_type\": \"CLIPTextEncode\",\n \"_meta\": {\n \"title\": \"Negative Prompt\"\n }\n },\n \"3\": {\n \"inputs\": {\n \"seed\": 156680208700286,\n \"steps\": 25,\n \"cfg\": 7,\n \"sampler_name\": \"euler\",\n \"scheduler\": \"normal\",\n \"denoise\": 1,\n \"model\": [\n \"4\",\n 0\n ],\n \"positive\": [\n \"6\",\n 0\n ],\n \"negative\": [\n \"7\",\n 0\n ],\n \"latent_image\": [\n \"5\",\n 0\n ]\n },\n \"class_type\": \"KSampler\",\n \"_meta\": {\n \"title\": \"KSampler\"\n }\n },\n \"8\": {\n \"inputs\": {\n \"samples\": [\n \"3\",\n 0\n ],\n \"vae\": [\n \"4\",\n 2\n ]\n },\n \"class_type\": \"VAEDecode\",\n \"_meta\": {\n \"title\": \"VAE Decode\"\n }\n },\n \"9\": {\n \"inputs\": {\n \"filename_prefix\": \"uscg_sweep\",\n \"images\": [\n \"8\",\n 0\n ]\n },\n \"class_type\": \"SaveImage\",\n \"_meta\": {\n \"title\": \"Save Image\"\n }\n }\n },\n \"output\": {\n \"node_id\": \"8\",\n \"output_index\": 0,\n \"type\": \"IMAGE\",\n \"picked_from\": \"9\"\n },\n \"run_other_outputs\": false,\n \"bindings\": [\n {\n \"id\": \"b1\",\n \"kind\": \"literal\",\n \"targets\": [\n {\n \"node_id\": \"3\",\n \"input\": \"cfg\"\n }\n ],\n \"values\": [\n 5,\n 7\n ]\n },\n {\n \"id\": \"b2\",\n \"kind\": \"literal\",\n \"targets\": [\n {\n \"node_id\": \"3\",\n \"input\": \"sampler_name\"\n }\n ],\n \"values\": [\n \"euler\",\n \"dpmpp_2m\"\n ]\n },\n {\n \"id\": \"b3\",\n \"kind\": \"positive\",\n \"targets\": [\n {\n \"node_id\": \"6\",\n \"input\": \"text\"\n }\n ]\n },\n {\n \"id\": \"b4\",\n \"kind\": \"negative\",\n \"targets\": [\n {\n \"node_id\": \"7\",\n \"input\": \"text\"\n }\n ]\n }\n ]\n },\n \"positive_prompt_groups\": [],\n \"negative_prompt\": \"\",\n \"use_custom_prompts\": false\n }\n ],\n \"upscaling\": {\n \"enabled\": false,\n \"save_pre_upscale\": false,\n \"run_upscales_at_end\": false,\n \"hires_prompt_adjust\": false,\n \"hires_prompt_behavior\": \"append_end\",\n \"hires_prompt_text\": \"\",\n \"pipelines\": [\n {\n \"active\": true,\n \"name\": \"Pipeline 1\",\n \"steps\": [\n {\n \"active\": true,\n \"mode\": \"hires_only\",\n \"repeat\": 1,\n \"upscale_models\": [],\n \"upscale_ratios\": \"1.5\",\n \"upscale_size\": \"2.0\",\n \"hires_denoise\": \"0.3\",\n \"hires_steps\": 0,\n \"tiled_vae\": false,\n \"tile_size\": 512,\n \"tile_overlap\": 64,\n \"temporal_size\": 512,\n \"temporal_overlap\": 64,\n \"resize_method\": \"bilinear\",\n \"hires_tiled_sampling\": false,\n \"hires_tile_width\": 512,\n \"hires_tile_height\": 512,\n \"hires_mask_blur\": 8,\n \"hires_tile_padding\": 32,\n \"hires_force_uniform_tiles\": false\n }\n ]\n }\n ]\n },\n \"cooldown\": {\n \"enabled\": false,\n \"seconds\": 5,\n \"every_n\": 1,\n \"clear_vram\": false\n },\n \"image_format\": \"webp\",\n \"overwrite_existing\": false,\n \"flush_batch_every\": 1,\n \"lora_triggerwords_mode\": \"None\",\n \"save_conditioning_cache_to_file\": false,\n \"enable_model_cache\": false,\n \"vae_batch_size\": 1,\n \"add_random_seeds_to_gens\": 0\n}", + false, + "" + ] + }, + { + "id": 2, + "type": "UltimateSamplerGrid", + "pos": [ + 1300, + 0 + ], + "size": [ + 420, + 300 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "optional_model", + "name": "optional_model", + "type": "MODEL", + "shape": 7, + "link": null + }, + { + "localized_name": "optional_clip", + "name": "optional_clip", + "type": "CLIP", + "shape": 7, + "link": null + }, + { + "localized_name": "optional_vae", + "name": "optional_vae", + "type": "VAE", + "shape": 7, + "link": null + }, + { + "localized_name": "optional_positive", + "name": "optional_positive", + "type": "CONDITIONING", + "shape": 7, + "link": null + }, + { + "localized_name": "optional_negative", + "name": "optional_negative", + "type": "CONDITIONING", + "shape": 7, + "link": null + }, + { + "localized_name": "optional_latent", + "name": "optional_latent", + "type": "LATENT", + "shape": 7, + "link": null + }, + { + "localized_name": "configs_json", + "name": "configs_json", + "type": "STRING", + "widget": { + "name": "configs_json" + }, + "link": 1 + } + ], + "outputs": [ + { + "localized_name": "dashboard_html", + "name": "dashboard_html", + "type": "STRING", + "links": [ + 2 + ] + } + ], + "properties": { + "cnr_id": "ultimate-auto-sampler-config-grid-testing-suite", + "Node name for S&R": "UltimateSamplerGrid" + }, + "widgets_values": [ + "[]" + ] + }, + { + "id": 3, + "type": "UltimateGridDashboard", + "pos": [ + 1760, + 0 + ], + "size": [ + 1400, + 1000 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "dashboard_html", + "name": "dashboard_html", + "shape": 7, + "type": "STRING", + "link": 2 + }, + { + "localized_name": "session_name", + "name": "session_name", + "type": "STRING", + "widget": { + "name": "session_name" + }, + "link": null + } + ], + "outputs": [], + "properties": { + "cnr_id": "ultimate-auto-sampler-config-grid-testing-suite", + "Node name for S&R": "UltimateGridDashboard" + }, + "widgets_values": [ + "example_workflow_sweep_sdxl", + null, + "", + "" + ] + }, + { + "id": 4, + "type": "MarkdownNote", + "pos": [ + 0, + -260 + ], + "size": [ + 600, + 220 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "### Workflow Sweep example (SDXL)\nNeeds: `sd_xl_base_1.0.safetensors`\n\nThe Builder holds a Workflow Sweep array: an embedded API-format SDXL graph with cfg, sampler and prompt axes. Export any of your own workflows with 'Export (API)' and load it in the Builder's Workflow Sweep section to grid-test graphs the standard arrays cannot express. See `examples/README.md`." + ] + } + ], + "links": [ + [ + 1, + 1, + 0, + 2, + 6, + "STRING" + ], + [ + 2, + 2, + 0, + 3, + 0, + "STRING" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.6, + "offset": [ + 100, + 300 + ] + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/tests/test_examples.py b/tests/test_examples.py index 5f3798c..d41689b 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -29,6 +29,7 @@ EXPECTED_IMAGES = { "uscg-krea2-turbo.json": 2, "uscg-flux2-klein-9b.json": 4, # Advanced Sampling on + off (expand_configs grids both) "uscg-moss-image.json": 2, + "uscg-workflow-sweep-sdxl.json": 8, # Workflow Sweep: 2 cfg x 2 samplers x 2 prompts } # Every top-level key of the Builder's new-state object (web/conf_builder/conf-builder-main.js, @@ -49,6 +50,7 @@ ALLOWED_FILES = { "uscg-krea2-turbo.json": {"krea2_turbo_fp8_scaled.safetensors", "qwen3vl_4b_fp8_scaled.safetensors", "qwen_image_vae.safetensors"}, "uscg-flux2-klein-9b.json": {"flux-2-klein-9b-fp8.safetensors", "qwen_3_8b_fp8mixed.safetensors", "flux2-vae.safetensors"}, "uscg-moss-image.json": {"moss_image_sft_int8_convrot.safetensors", "qwen3.5_9b_w4a8_gptq.safetensors", "flux2-vae.safetensors"}, + "uscg-workflow-sweep-sdxl.json": {"sd_xl_base_1.0.safetensors"}, } @@ -146,6 +148,10 @@ def test_no_local_paths_and_known_filenames(path): state = _state(_load(path)) used = set() for a in state["config_arrays"]: + if a.get("kind") == "workflow": # files live inside the embedded API graph + used.update(n["inputs"]["ckpt_name"] for n in a["workflow"]["api_prompt"].values() + if n["class_type"] == "CheckpointLoaderSimple") + continue for m in a["models"]: used.add(m["path"] if isinstance(m, dict) else m) used.update(a["text_encoders"]) @@ -183,3 +189,14 @@ def test_family_settings(): assert moss[0]["clip_type"] == "moss_image" assert moss[0]["use_slg"] is True and moss[0]["slg_single_layers"] == "22" assert moss[1]["use_slg"] is False + + +def test_workflow_sweep_example(): + cfg = _cfgs("uscg-workflow-sweep-sdxl.json") + assert len(cfg) == 1 and cfg[0]["model_type"] == "workflow" + assert cfg[0]["workflow_output"] == {"node_id": "8", "output_index": 0} + import workflow_sweep as ws + jobs = ws.expand_workflow_entry(cfg[0], 0, None, None) + assert len(jobs) == 8 and len({j["wf_fingerprint"] for j in jobs}) == 8 + assert {j["wf_params"]["KSampler.cfg"] for j in jobs} == {5, 7} + assert {j["wf_params"]["KSampler.sampler_name"] for j in jobs} == {"euler", "dpmpp_2m"}