Read proxy metadata from V3 hidden context
This commit is contained in:
@@ -299,7 +299,7 @@ When a component output is consumed by other remote components, Modal-Sync can k
|
||||
|
||||
Split proxies, mapped phases, and remote-to-remote component edges use component-local remote sessions. Durable bridge metadata and small serialized values are stored in a Modal `Dict`. Serialized bridge values above `COMFY_MODAL_BRIDGE_INLINE_MAX_BYTES` are stored as integrity-checked, content-addressed objects on the shared Modal Volume, with only the object reference retained in the Dict record. Bridge records do not retain producer inputs when the output is directly restorable or a literal node plan can rebuild it; linked loader plans retain only the boundary inputs used by their reduced dependency subgraph. The complete producer input signature still participates in the bridge key, so omitting recovery inputs cannot make distinct prompts collide. Durable writes loop until every byte is accepted, flush to storage, and verify the staged file's size and SHA-256 before publication. The producer commits new objects before publishing a completed invocation; a consumer whose mounted Volume snapshot is absent, truncated, or corrupt retries through the authoritative `Volume.read_file()` API without reloading the mount or disturbing memory-mapped models. Terminal-result diagnostics time worker publication, durable completion, invocation-record writes, Modal result yielding, local receipt, and stream close separately so post-compute stalls can be assigned to a specific transport boundary. `NOISE` remains deliberately excluded from generic serialization because ComfyUI represents it as a deferred strategy object rather than a realized tensor. Literal `RandomNoise` and `KSamplerSelect` bridge producers are instead rebuilt directly on the downstream worker, and fallback producer replay is rejected whenever its full dependency closure includes sampler work.
|
||||
|
||||
Each remote payload also carries a stable invocation id. The local proxy keeps volatile execution metadata outside ComfyUI's cache surface and rehydrates it using both the queued prompt id and proxy node id, so a second queued workflow cannot overwrite the active workflow's warmup or session context. Hidden prompt metadata and node ids are normalized from ComfyUI's `INPUT_IS_LIST` singleton wrappers before that lookup, keeping back-to-back queued workflows isolated. The worker records its lifecycle in a shared Modal `Dict` and replays a completed result when the local client or Modal retries the same call. An overlapping delivery waits briefly for the active attempt to publish a terminal state instead of failing immediately. If a streamed call loses its consumer, the worker cancels unfinished compute and marks that attempt failed before the retry proceeds; uncommitted Volume writes remain unpublished. Large completed results use the same content-addressed Volume store, while failed attempts remain retryable.
|
||||
Each remote payload also carries a stable invocation id. The local proxy keeps volatile execution metadata outside ComfyUI's cache surface and rehydrates it using both the queued prompt id and proxy node id, so a second queued workflow cannot overwrite the active workflow's warmup or session context. V3 proxies read hidden prompt metadata and node ids from ComfyUI's execution-specific class clone, with a list-normalizing legacy `kwargs` fallback, keeping back-to-back queued workflows isolated. The worker records its lifecycle in a shared Modal `Dict` and replays a completed result when the local client or Modal retries the same call. An overlapping delivery waits briefly for the active attempt to publish a terminal state instead of failing immediately. If a streamed call loses its consumer, the worker cancels unfinished compute and marks that attempt failed before the retry proceeds; uncommitted Volume writes remain unpublished. Large completed results use the same content-addressed Volume store, while failed attempts remain retryable.
|
||||
|
||||
After every remote payload, including the final component that clears its remote session, Modal-Sync compares the remote ComfyUI `output/` tree with its pre-execution snapshot. New or replaced regular files are bundled into the durable result and downloaded into the corresponding local `output/` subdirectory. Each downloaded filename is prefixed `remote-<app_id>-<epoch>-`, where `app_id` is the unique Modal app suffix and `epoch` is the trailing nine digits of the remote completion time. Artifact paths and SHA-256 digests are validated, symlinks and escaping paths are rejected, identical retry downloads are reused, and a differing local collision receives a numeric suffix instead of being overwritten. A terminal local `SaveVideo` whose input comes from a remote `VIDEO` is automatically executed as a remote artifact sink, preserving its configured filename and encoding while avoiding transport of the decoded frame and audio tensors. Nonterminal local video consumers retain ordinary lossless VIDEO transport.
|
||||
|
||||
|
||||
+1
-1
@@ -447,4 +447,4 @@
|
||||
- A local node between two remote sampler regions can force an ordered phase split, but losing the first worker must not replay the first sampler merely to reconstruct `RandomNoise` or `KSamplerSelect` strategy objects. Build literal `NOISE` and `SAMPLER` producers directly on the downstream worker, and evaluate the complete execute-node dependency closure before permitting any fallback producer replay; checking only a terminal node such as `ImageFromBatch` misses its expensive sampler ancestors.
|
||||
- A streamed remote method timer can conceal several distinct post-compute waits: publishing the worker result into the bounded buffer, committing durable objects, writing completed invocation metadata, suspending at the Modal generator yield, receiving the result locally, and closing the stream. Log each boundary with the same component, prompt, invocation, task, byte count, queue depth, and elapsed time before changing storage or transport policy; a single gap between output bundling and method completion cannot identify which subsystem stalled.
|
||||
- Speculative worker preparation should follow the dependency DAG, not merely the textual order of proxy nodes. Attaching only the nearest reachable component with a different worker-affinity group keeps lookahead bounded, and triggering it on the current stream's first event prevents the speculative request from racing ahead of the critical container. Warmup dedupe must include the affinity key; prompt-wide slot numbers alone collapse distinct LLM and Comfy pools into the same slot.
|
||||
- Dynamic proxies with `is_input_list=True` can receive hidden `extra_pnginfo` and `unique_id` values in singleton lists, just like ordinary inputs. Prompt-scoped payload rehydration must unwrap those hidden values before lookup; otherwise back-to-back queued prompts can select the latest prompt's dispatch group and leave the active prompt's parallel local-preview gate waiting forever.
|
||||
- V3 nodes do not receive hidden `extra_pnginfo` and `unique_id` values through execution `kwargs`; ComfyUI stores them on the execution-specific cloned class as `cls.hidden`. Dynamic proxies must read that holder first and use list-normalized `kwargs` only as a legacy/direct-call fallback. A regression must execute a class produced by `PREPARE_CLASS_CLONE`, because calling the V3 node directly with synthetic hidden keyword arguments models the wrong runtime convention and can miss cross-prompt dispatch-group deadlocks.
|
||||
|
||||
+16
-2
@@ -410,6 +410,18 @@ def _unwrap_proxy_singleton(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _pop_proxy_hidden_value(
|
||||
proxy_class: type[Any],
|
||||
kwargs: dict[str, Any],
|
||||
hidden_input: io.Hidden,
|
||||
) -> Any:
|
||||
"""Read a V3 class-clone hidden value with a legacy kwargs fallback."""
|
||||
legacy_value = kwargs.pop(hidden_input.name, None)
|
||||
hidden_holder = getattr(proxy_class, "hidden", None)
|
||||
hidden_value = getattr(hidden_holder, hidden_input.name, None)
|
||||
return legacy_value if hidden_value is None else hidden_value
|
||||
|
||||
|
||||
def _normalize_proxy_kwargs(kwargs: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Convert ComfyUI INPUT_IS_LIST proxy kwargs back into ordinary runtime values."""
|
||||
return {
|
||||
@@ -757,10 +769,12 @@ def _build_proxy_node_class(
|
||||
async def execute(cls, **kwargs: Any) -> io.NodeOutput:
|
||||
"""Forward the execution payload to the configured remote executor."""
|
||||
unique_id = _normalize_prompt_id(
|
||||
_unwrap_proxy_singleton(kwargs.pop(io.Hidden.unique_id.name, None))
|
||||
_unwrap_proxy_singleton(
|
||||
_pop_proxy_hidden_value(cls, kwargs, io.Hidden.unique_id)
|
||||
)
|
||||
)
|
||||
prompt_id = _prompt_id_from_extra_pnginfo(
|
||||
kwargs.pop(io.Hidden.extra_pnginfo.name, None)
|
||||
_pop_proxy_hidden_value(cls, kwargs, io.Hidden.extra_pnginfo)
|
||||
)
|
||||
payload = _rehydrate_proxy_payload(
|
||||
_normalize_proxy_payload(kwargs.pop(payload_input_name, None)),
|
||||
|
||||
@@ -1383,10 +1383,12 @@ def test_proxy_execution_normalizes_input_is_list_kwargs(
|
||||
}
|
||||
|
||||
|
||||
def test_cache_friendly_proxy_payload_rehydrates_list_wrapped_prompt_id_at_execution(
|
||||
@pytest.mark.parametrize("hidden_delivery", ["v3_class_clone", "legacy_kwargs"])
|
||||
def test_cache_friendly_proxy_payload_rehydrates_prompt_id_at_execution(
|
||||
modal_executor_module: Any,
|
||||
hidden_delivery: str,
|
||||
) -> None:
|
||||
"""List-wrapped hidden inputs should select the correct overlapping prompt."""
|
||||
"""V3 and legacy hidden inputs should select the correct overlapping prompt."""
|
||||
fake_nodes_module = type(
|
||||
"FakeNodesModule",
|
||||
(),
|
||||
@@ -1437,17 +1439,30 @@ def test_cache_friendly_proxy_payload_rehydrates_list_wrapped_prompt_id_at_execu
|
||||
|
||||
modal_executor_module.set_remote_executor_client_factory(lambda: FakeClient())
|
||||
try:
|
||||
result = asyncio.run(
|
||||
proxy_class.execute(
|
||||
original_node_data=[payload],
|
||||
unique_id=["node-1"],
|
||||
extra_pnginfo=[
|
||||
{
|
||||
modal_executor_module.MODAL_PROMPT_ID_EXTRA_PNGINFO_KEY:
|
||||
"prompt-1"
|
||||
hidden_metadata = {
|
||||
modal_executor_module.MODAL_PROMPT_ID_EXTRA_PNGINFO_KEY: "prompt-1"
|
||||
}
|
||||
execution_proxy_class = proxy_class
|
||||
hidden_kwargs: dict[str, Any] = {}
|
||||
if hidden_delivery == "v3_class_clone":
|
||||
execution_proxy_class = proxy_class.PREPARE_CLASS_CLONE(
|
||||
{
|
||||
"hidden_inputs": {
|
||||
modal_executor_module.io.Hidden.unique_id: "node-1",
|
||||
modal_executor_module.io.Hidden.extra_pnginfo: hidden_metadata,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
else:
|
||||
hidden_kwargs = {
|
||||
"unique_id": ["node-1"],
|
||||
"extra_pnginfo": [hidden_metadata],
|
||||
}
|
||||
result = asyncio.run(
|
||||
execution_proxy_class.execute(
|
||||
original_node_data=[payload],
|
||||
value=["payload"],
|
||||
**hidden_kwargs,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user