Fix Nodes 2.0 provider policy menu
This commit is contained in:
@@ -97,6 +97,8 @@ Provider policy is stored under `workflow.extra.remote_execution` and travels wi
|
||||
- **Automatic** — considers ready SSH hosts, the selected Modal GPU, and any workflow-declared Vast pools; rejects hard incompatibilities (insufficient VRAM, provider-specific requirements), then prefers the lowest predicted incremental cost with deterministic tie-breaking.
|
||||
- **Automatically place eligible nodes** — lets the planner select remote nodes itself instead of requiring per-node toggles. Leave it off to keep manual component boundaries.
|
||||
|
||||
To change the policy from a node, right-click an eligible node and open **Remote Execution**. ComfyUI Nodes 2.0 lists **Modal only**, **Self-hosted only**, **Vast.ai only**, and **Automatic** directly below the disabled **Provider policy (current choice)** heading because its extension-menu adapter supports only one submenu level.
|
||||
|
||||
### Modal
|
||||
|
||||
Authentication is user-managed — run Modal's setup once if the current user does not already have working credentials:
|
||||
|
||||
@@ -491,6 +491,7 @@
|
||||
- Nodes 2.0 node bodies can cover legacy canvas foreground drawings, so execution-location UI needs a DOM-layer equivalent rather than relying only on `onDrawForeground`. Attach the exact runtime identity to streamed status events as well as numeric progress; otherwise loader and other non-numeric phases know the active node but cannot display its Modal task ID.
|
||||
## 2026-08-23: Vast instances need a direct-container backend
|
||||
|
||||
- ComfyUI Nodes 2.0 converts legacy extension context menus into a Vue menu model whose submenu entries are action-only leaves. A third menu level is silently discarded, so extension-owned provider and GPU selectors must put their choices directly in the first submenu and use a disabled heading to show the current selection.
|
||||
- Vast worker images must be built explicitly for `linux/amd64`. Without Docker's `--platform linux/amd64`, an Apple Silicon host selects arm64 base-image stages and rejects the intentionally x86_64 CUDA/vLLM wheels before the image can be pushed.
|
||||
- Provider-aware DAG cuts are valid only for serializable or safely reproducible edges. For shared `MODEL`, `CLIP`, `VAE`, `NOISE`, and `SAMPLER` outputs around an SSH-only LLM phase, clone a non-sampling loader dependency closure into each downstream component and rewire it before final partitioning. Fall back to hard affinity only when the producer cannot be reproduced safely; otherwise one GGUF node can pull an unrelated 61.73 GiB Comfy model onto a 52.64 GiB-RAM SSH host.
|
||||
- A raw Hugging Face repository ID is valid only for the Transformers-compatible model resolver, which requires `config.json` and safetensors. GGUF repositories must be selected through a curated profile that pins the exact weight, optional projector, tokenizer source, and `llama.cpp` backend; otherwise prompt validation fails before provider placement.
|
||||
|
||||
@@ -1191,16 +1191,48 @@ workflowGraph.nodes.push(menuNode);
|
||||
modalToggle.installModalContextMenu(MenuNode, { name: "KSampler" });
|
||||
const menuOptions = [];
|
||||
menuNode.getExtraMenuOptions(null, menuOptions);
|
||||
const modalMenu = menuOptions.find((option) => option?.content === "Modal");
|
||||
const gpuMenu = modalMenu.submenu.options.find((option) => option?.content === "GPU");
|
||||
const remoteExecutionMenu = menuOptions.find(
|
||||
(option) => option?.content === "Remote Execution",
|
||||
);
|
||||
const providerPolicyHeading = remoteExecutionMenu.submenu.options.find(
|
||||
(option) => option?.content === "Provider policy (Modal only)",
|
||||
);
|
||||
assert.equal(providerPolicyHeading.disabled, true);
|
||||
assert.equal(providerPolicyHeading.has_submenu, undefined);
|
||||
const providerPolicyHeadingIndex = remoteExecutionMenu.submenu.options.indexOf(
|
||||
providerPolicyHeading,
|
||||
);
|
||||
assert.deepEqual(
|
||||
gpuMenu.submenu.options.map((option) => option.content),
|
||||
remoteExecutionMenu.submenu.options
|
||||
.slice(providerPolicyHeadingIndex + 1, providerPolicyHeadingIndex + 5)
|
||||
.map((option) => option.content),
|
||||
["Modal only", "Self-hosted only", "Vast.ai only", "Automatic (lowest cost compatible)"],
|
||||
);
|
||||
const vastPolicyOption = remoteExecutionMenu.submenu.options.find(
|
||||
(option) => option?.content === "Vast.ai only",
|
||||
);
|
||||
assert.equal(vastPolicyOption.checked, false);
|
||||
assert.equal(vastPolicyOption.submenu, undefined);
|
||||
vastPolicyOption.callback();
|
||||
assert.equal(workflowGraph.extra.remote_execution.policy, "vast");
|
||||
assert.equal(workflowGraph.changeCount, 2);
|
||||
|
||||
const modalMenu = menuOptions.find((option) => option?.content === "Modal");
|
||||
const gpuHeading = modalMenu.submenu.options.find(
|
||||
(option) => option?.content === "GPU (B300)",
|
||||
);
|
||||
assert.equal(gpuHeading.disabled, true);
|
||||
assert.equal(gpuHeading.has_submenu, undefined);
|
||||
assert.deepEqual(
|
||||
modalMenu.submenu.options
|
||||
.filter((option) => modalToggle.MODAL_GPU_TYPES.includes(option?.content))
|
||||
.map((option) => option.content),
|
||||
modalToggle.MODAL_GPU_TYPES,
|
||||
);
|
||||
assert.equal(
|
||||
gpuMenu.submenu.options.find((option) => option.content === "B300")?.checked,
|
||||
modalMenu.submenu.options.find((option) => option?.content === "B300")?.checked,
|
||||
true,
|
||||
);
|
||||
gpuMenu.submenu.options.find((option) => option.content === "L40S").callback();
|
||||
modalMenu.submenu.options.find((option) => option?.content === "L40S").callback();
|
||||
assert.equal(workflowGraph.extra.comfy_modal.gpu, "L40S");
|
||||
assert.equal(workflowGraph.changeCount, 2);
|
||||
assert.equal(workflowGraph.changeCount, 3);
|
||||
|
||||
+18
-18
@@ -3401,22 +3401,24 @@ function installModalContextMenu(nodeType, nodeData) {
|
||||
: "Disable on Upstream Nodes";
|
||||
if (!targetOptions.some((option) => option?.content === "Remote Execution")) {
|
||||
const executionPreferences = selectedExecutionPreferences();
|
||||
const executionPolicyLabel =
|
||||
EXECUTION_POLICIES.find(([value]) => value === executionPreferences.policy)?.[1]
|
||||
?? executionPreferences.policy;
|
||||
targetOptions.push(null, {
|
||||
content: "Remote Execution",
|
||||
has_submenu: true,
|
||||
submenu: {
|
||||
options: [
|
||||
{
|
||||
content: "Provider policy",
|
||||
has_submenu: true,
|
||||
submenu: {
|
||||
options: EXECUTION_POLICIES.map(([value, label]) => ({
|
||||
content: label,
|
||||
checked: executionPreferences.policy === value,
|
||||
callback: () => setExecutionPreferences({ policy: value }),
|
||||
})),
|
||||
},
|
||||
content: `Provider policy (${executionPolicyLabel})`,
|
||||
disabled: true,
|
||||
},
|
||||
...EXECUTION_POLICIES.map(([value, label]) => ({
|
||||
content: label,
|
||||
checked: executionPreferences.policy === value,
|
||||
callback: () => setExecutionPreferences({ policy: value }),
|
||||
})),
|
||||
null,
|
||||
{
|
||||
content: "Automatically place eligible nodes",
|
||||
checked: executionPreferences.autoPlace,
|
||||
@@ -3446,16 +3448,14 @@ function installModalContextMenu(nodeType, nodeData) {
|
||||
submenu: {
|
||||
options: [
|
||||
{
|
||||
content: "GPU",
|
||||
has_submenu: true,
|
||||
submenu: {
|
||||
options: MODAL_GPU_TYPES.map((modalGpu) => ({
|
||||
content: modalGpu,
|
||||
checked: modalGpu === currentModalGpu,
|
||||
callback: () => setSelectedModalGpu(modalGpu),
|
||||
})),
|
||||
},
|
||||
content: `GPU (${currentModalGpu})`,
|
||||
disabled: true,
|
||||
},
|
||||
...MODAL_GPU_TYPES.map((modalGpu) => ({
|
||||
content: modalGpu,
|
||||
checked: modalGpu === currentModalGpu,
|
||||
callback: () => setSelectedModalGpu(modalGpu),
|
||||
})),
|
||||
null,
|
||||
{
|
||||
content: enableUpstreamMenuItemLabel,
|
||||
|
||||
Reference in New Issue
Block a user