mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-06-19 22:39:24 +08:00
Compare commits
10 Commits
088778c35d
...
a6472b1514
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6472b1514 | ||
|
|
6158cd5820 | ||
|
|
bff714dda0 | ||
|
|
fce22da313 | ||
|
|
9f9d37bd9a | ||
|
|
593be209a4 | ||
|
|
3814bf4454 | ||
|
|
d062becb33 | ||
|
|
e84a200a3c | ||
|
|
192cb8eeb9 |
@ -400,7 +400,7 @@ try:
|
||||
if args.use_split_cross_attention == False and args.use_quad_cross_attention == False:
|
||||
if aotriton_supported(arch): # AMD efficient attention implementation depends on aotriton.
|
||||
if torch_version_numeric >= (2, 7): # works on 2.6 but doesn't actually seem to improve much
|
||||
if any((a in arch) for a in ["gfx90a", "gfx942", "gfx950", "gfx1100", "gfx1101", "gfx1151"]): # TODO: more arches, TODO: gfx950
|
||||
if any((a in arch) for a in ["gfx90a", "gfx942", "gfx950", "gfx1100", "gfx1101", "gfx1150", "gfx1151"]): # TODO: more arches, TODO: gfx950
|
||||
ENABLE_PYTORCH_ATTENTION = True
|
||||
if rocm_version >= (7, 0):
|
||||
if any((a in arch) for a in ["gfx1200", "gfx1201"]):
|
||||
|
||||
18
comfy/ops.py
18
comfy/ops.py
@ -336,7 +336,10 @@ class disable_weight_init:
|
||||
class Linear(torch.nn.Linear, CastWeightBiasOp):
|
||||
|
||||
def __init__(self, in_features, out_features, bias=True, device=None, dtype=None):
|
||||
if not comfy.model_management.WINDOWS or not comfy.memory_management.aimdo_enabled:
|
||||
# don't trust subclasses that BYO state dict loader to call us.
|
||||
if (not comfy.model_management.WINDOWS
|
||||
or not comfy.memory_management.aimdo_enabled
|
||||
or type(self)._load_from_state_dict is not disable_weight_init.Linear._load_from_state_dict):
|
||||
super().__init__(in_features, out_features, bias, device, dtype)
|
||||
return
|
||||
|
||||
@ -357,7 +360,9 @@ class disable_weight_init:
|
||||
def _load_from_state_dict(self, state_dict, prefix, local_metadata,
|
||||
strict, missing_keys, unexpected_keys, error_msgs):
|
||||
|
||||
if not comfy.model_management.WINDOWS or not comfy.memory_management.aimdo_enabled:
|
||||
if (not comfy.model_management.WINDOWS
|
||||
or not comfy.memory_management.aimdo_enabled
|
||||
or type(self)._load_from_state_dict is not disable_weight_init.Linear._load_from_state_dict):
|
||||
return super()._load_from_state_dict(state_dict, prefix, local_metadata, strict,
|
||||
missing_keys, unexpected_keys, error_msgs)
|
||||
disable_weight_init._lazy_load_from_state_dict(
|
||||
@ -564,7 +569,10 @@ class disable_weight_init:
|
||||
def __init__(self, num_embeddings, embedding_dim, padding_idx=None, max_norm=None,
|
||||
norm_type=2.0, scale_grad_by_freq=False, sparse=False, _weight=None,
|
||||
_freeze=False, device=None, dtype=None):
|
||||
if not comfy.model_management.WINDOWS or not comfy.memory_management.aimdo_enabled:
|
||||
# don't trust subclasses that BYO state dict loader to call us.
|
||||
if (not comfy.model_management.WINDOWS
|
||||
or not comfy.memory_management.aimdo_enabled
|
||||
or type(self)._load_from_state_dict is not disable_weight_init.Embedding._load_from_state_dict):
|
||||
super().__init__(num_embeddings, embedding_dim, padding_idx, max_norm,
|
||||
norm_type, scale_grad_by_freq, sparse, _weight,
|
||||
_freeze, device, dtype)
|
||||
@ -590,7 +598,9 @@ class disable_weight_init:
|
||||
def _load_from_state_dict(self, state_dict, prefix, local_metadata,
|
||||
strict, missing_keys, unexpected_keys, error_msgs):
|
||||
|
||||
if not comfy.model_management.WINDOWS or not comfy.memory_management.aimdo_enabled:
|
||||
if (not comfy.model_management.WINDOWS
|
||||
or not comfy.memory_management.aimdo_enabled
|
||||
or type(self)._load_from_state_dict is not disable_weight_init.Embedding._load_from_state_dict):
|
||||
return super()._load_from_state_dict(state_dict, prefix, local_metadata, strict,
|
||||
missing_keys, unexpected_keys, error_msgs)
|
||||
disable_weight_init._lazy_load_from_state_dict(
|
||||
|
||||
@ -1459,6 +1459,7 @@ class OmniProEditVideoNode(IO.ComfyNode):
|
||||
node_id="KlingOmniProEditVideoNode",
|
||||
display_name="Kling 3.0 Omni Edit Video",
|
||||
category="api node/video/Kling",
|
||||
essentials_category="Video Generation",
|
||||
description="Edit an existing video with the latest model from Kling.",
|
||||
inputs=[
|
||||
IO.Combo.Input("model_name", options=["kling-v3-omni", "kling-video-o1"]),
|
||||
|
||||
@ -833,6 +833,7 @@ class RecraftVectorizeImageNode(IO.ComfyNode):
|
||||
node_id="RecraftVectorizeImageNode",
|
||||
display_name="Recraft Vectorize Image",
|
||||
category="api node/image/Recraft",
|
||||
essentials_category="Image Tools",
|
||||
description="Generates SVG synchronously from an input image.",
|
||||
inputs=[
|
||||
IO.Image.Input("image"),
|
||||
|
||||
@ -68,19 +68,20 @@ _FAILED_SIGNATURE = object()
|
||||
|
||||
|
||||
def _shallow_is_changed_signature(value):
|
||||
"""Sanitize execution-time `is_changed` values without deep recursion."""
|
||||
"""Sanitize execution-time `is_changed` values with a small fail-closed budget."""
|
||||
value_type = type(value)
|
||||
if value_type in _PRIMITIVE_SIGNATURE_TYPES:
|
||||
return value
|
||||
|
||||
canonical = to_hashable(value, max_nodes=64)
|
||||
if type(canonical) is Unhashable:
|
||||
return canonical
|
||||
|
||||
if value_type is list or value_type is tuple:
|
||||
try:
|
||||
items = tuple(value)
|
||||
except RuntimeError:
|
||||
return Unhashable()
|
||||
if all(type(item) in _PRIMITIVE_SIGNATURE_TYPES for item in items):
|
||||
container_tag = "is_changed_list" if value_type is list else "is_changed_tuple"
|
||||
return (container_tag, items)
|
||||
return Unhashable()
|
||||
container_tag = "is_changed_list" if value_type is list else "is_changed_tuple"
|
||||
return (container_tag, canonical[1])
|
||||
|
||||
return canonical
|
||||
|
||||
|
||||
def _primitive_signature_sort_key(obj):
|
||||
@ -274,7 +275,8 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS):
|
||||
snapshots = {}
|
||||
sort_memo = {}
|
||||
processed = 0
|
||||
stack = [(obj, False)]
|
||||
# Keep traversal state separate from container snapshots/results.
|
||||
work_stack = [(obj, False)]
|
||||
|
||||
def resolve_value(value):
|
||||
"""Resolve a child value from the completed memo table when available."""
|
||||
@ -308,8 +310,8 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS):
|
||||
|
||||
return (container_tag, tuple(value for _, value in ordered_items))
|
||||
|
||||
while stack:
|
||||
current, expanded = stack.pop()
|
||||
while work_stack:
|
||||
current, expanded = work_stack.pop()
|
||||
current_type = type(current)
|
||||
|
||||
if current_type in _PRIMITIVE_SIGNATURE_TYPES or current_type is Unhashable:
|
||||
@ -387,7 +389,7 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS):
|
||||
return Unhashable()
|
||||
|
||||
active.add(current_id)
|
||||
stack.append((current, True))
|
||||
work_stack.append((current, True))
|
||||
if current_type is dict:
|
||||
try:
|
||||
items = list(current.items())
|
||||
@ -397,8 +399,8 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS):
|
||||
active.discard(current_id)
|
||||
continue
|
||||
for key, value in reversed(items):
|
||||
stack.append((value, False))
|
||||
stack.append((key, False))
|
||||
work_stack.append((value, False))
|
||||
work_stack.append((key, False))
|
||||
else:
|
||||
try:
|
||||
items = list(current)
|
||||
@ -408,7 +410,7 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS):
|
||||
active.discard(current_id)
|
||||
continue
|
||||
for item in reversed(items):
|
||||
stack.append((item, False))
|
||||
work_stack.append((item, False))
|
||||
|
||||
return memo.get(id(obj), Unhashable())
|
||||
|
||||
@ -460,18 +462,18 @@ class CacheKeySetInputSignature(CacheKeySet):
|
||||
signature.append(await self.get_immediate_node_signature(dynprompt, node_id, order_mapping))
|
||||
for ancestor_id in ancestors:
|
||||
signature.append(await self.get_immediate_node_signature(dynprompt, ancestor_id, order_mapping))
|
||||
return _signature_to_hashable(signature)
|
||||
return tuple(signature)
|
||||
|
||||
async def get_immediate_node_signature(self, dynprompt, node_id, ancestor_order_mapping):
|
||||
"""Build the immediate cache-signature fragment for a node.
|
||||
|
||||
Link inputs are reduced to ancestor references here, while non-link
|
||||
values are appended as-is. Full canonicalization happens later in
|
||||
`get_node_signature()` via `_signature_to_hashable()`.
|
||||
Link inputs are reduced to ancestor references here. Non-link values
|
||||
are canonicalized or failed closed before being appended so the final
|
||||
node signature is assembled from already-hashable fragments.
|
||||
"""
|
||||
if not dynprompt.has_node(node_id):
|
||||
# This node doesn't exist -- we can't cache it.
|
||||
return [float("NaN")]
|
||||
return (float("NaN"),)
|
||||
node = dynprompt.get_node(node_id)
|
||||
class_type = node["class_type"]
|
||||
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
||||
@ -485,8 +487,8 @@ class CacheKeySetInputSignature(CacheKeySet):
|
||||
ancestor_index = ancestor_order_mapping[ancestor_id]
|
||||
signature.append((key,("ANCESTOR", ancestor_index, ancestor_socket)))
|
||||
else:
|
||||
signature.append((key, inputs[key]))
|
||||
return signature
|
||||
signature.append((key, to_hashable(inputs[key])))
|
||||
return tuple(signature)
|
||||
|
||||
# This function returns a list of all ancestors of the given node. The order of the list is
|
||||
# deterministic based on which specific inputs the ancestor is connected by.
|
||||
|
||||
@ -19,6 +19,7 @@ class EmptyLatentAudio(IO.ComfyNode):
|
||||
node_id="EmptyLatentAudio",
|
||||
display_name="Empty Latent Audio",
|
||||
category="latent/audio",
|
||||
essentials_category="Audio",
|
||||
inputs=[
|
||||
IO.Float.Input("seconds", default=47.6, min=1.0, max=1000.0, step=0.1),
|
||||
IO.Int.Input(
|
||||
@ -185,6 +186,7 @@ class SaveAudioMP3(IO.ComfyNode):
|
||||
search_aliases=["export mp3"],
|
||||
display_name="Save Audio (MP3)",
|
||||
category="audio",
|
||||
essentials_category="Audio",
|
||||
inputs=[
|
||||
IO.Audio.Input("audio"),
|
||||
IO.String.Input("filename_prefix", default="audio/ComfyUI"),
|
||||
|
||||
@ -14,6 +14,7 @@ class ImageCompare(IO.ComfyNode):
|
||||
display_name="Image Compare",
|
||||
description="Compares two images side by side with a slider.",
|
||||
category="image",
|
||||
essentials_category="Image Tools",
|
||||
is_experimental=True,
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
|
||||
@ -58,6 +58,7 @@ class ImageCropV2(IO.ComfyNode):
|
||||
search_aliases=["trim"],
|
||||
display_name="Image Crop",
|
||||
category="image/transform",
|
||||
essentials_category="Image Tools",
|
||||
inputs=[
|
||||
IO.Image.Input("image"),
|
||||
IO.BoundingBox.Input("crop_region", component="ImageCrop"),
|
||||
|
||||
@ -21,6 +21,7 @@ class Blend(io.ComfyNode):
|
||||
node_id="ImageBlend",
|
||||
display_name="Image Blend",
|
||||
category="image/postprocessing",
|
||||
essentials_category="Image Tools",
|
||||
inputs=[
|
||||
io.Image.Input("image1"),
|
||||
io.Image.Input("image2"),
|
||||
|
||||
@ -1 +1 @@
|
||||
comfyui_manager==4.1b4
|
||||
comfyui_manager==4.1b5
|
||||
8
nodes.py
8
nodes.py
@ -81,6 +81,7 @@ class CLIPTextEncode(ComfyNodeABC):
|
||||
|
||||
|
||||
class ConditioningCombine:
|
||||
ESSENTIALS_CATEGORY = "Image Generation"
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"conditioning_1": ("CONDITIONING", ), "conditioning_2": ("CONDITIONING", )}}
|
||||
@ -1211,9 +1212,6 @@ class GLIGENTextBoxApply:
|
||||
return (c, )
|
||||
|
||||
class EmptyLatentImage:
|
||||
def __init__(self):
|
||||
self.device = comfy.model_management.intermediate_device()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
@ -1232,7 +1230,7 @@ class EmptyLatentImage:
|
||||
SEARCH_ALIASES = ["empty", "empty latent", "new latent", "create latent", "blank latent", "blank"]
|
||||
|
||||
def generate(self, width, height, batch_size=1):
|
||||
latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device)
|
||||
latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype())
|
||||
return ({"samples": latent, "downscale_ratio_spacial": 8}, )
|
||||
|
||||
|
||||
@ -1781,6 +1779,7 @@ class LoadImage:
|
||||
return True
|
||||
|
||||
class LoadImageMask:
|
||||
ESSENTIALS_CATEGORY = "Image Tools"
|
||||
SEARCH_ALIASES = ["import mask", "alpha mask", "channel mask"]
|
||||
|
||||
_color_channels = ["alpha", "red", "green", "blue"]
|
||||
@ -1889,6 +1888,7 @@ class ImageScale:
|
||||
return (s,)
|
||||
|
||||
class ImageScaleBy:
|
||||
ESSENTIALS_CATEGORY = "Image Tools"
|
||||
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -353,11 +353,26 @@ def test_shallow_is_changed_signature_accepts_primitive_lists(caching_module):
|
||||
assert sanitized == ("is_changed_list", (1, "two", None, True))
|
||||
|
||||
|
||||
def test_shallow_is_changed_signature_fails_closed_on_nested_containers(caching_module):
|
||||
"""Nested containers from `is_changed` should be rejected immediately."""
|
||||
def test_shallow_is_changed_signature_accepts_structured_builtin_fingerprint_lists(caching_module):
|
||||
"""Structured built-in `is_changed` fingerprints should remain representable."""
|
||||
caching, _ = caching_module
|
||||
|
||||
sanitized = caching._shallow_is_changed_signature([1, ["nested"]])
|
||||
sanitized = caching._shallow_is_changed_signature([("seed", 42), {"cfg": 8}])
|
||||
|
||||
assert sanitized == (
|
||||
"is_changed_list",
|
||||
(
|
||||
("tuple", ("seed", 42)),
|
||||
("dict", (("cfg", 8),)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_shallow_is_changed_signature_fails_closed_for_opaque_payload(caching_module):
|
||||
"""Opaque `is_changed` payloads should still fail closed."""
|
||||
caching, _ = caching_module
|
||||
|
||||
sanitized = caching._shallow_is_changed_signature([_OpaqueValue()])
|
||||
|
||||
assert isinstance(sanitized, caching.Unhashable)
|
||||
|
||||
|
||||
178
tests/execution/test_caching.py
Normal file
178
tests/execution/test_caching.py
Normal file
@ -0,0 +1,178 @@
|
||||
import asyncio
|
||||
|
||||
from comfy_execution import caching
|
||||
|
||||
|
||||
class _StubDynPrompt:
|
||||
def __init__(self, nodes):
|
||||
self._nodes = nodes
|
||||
|
||||
def has_node(self, node_id):
|
||||
return node_id in self._nodes
|
||||
|
||||
def get_node(self, node_id):
|
||||
return self._nodes[node_id]
|
||||
|
||||
|
||||
class _StubIsChangedCache:
|
||||
async def get(self, node_id):
|
||||
return None
|
||||
|
||||
|
||||
class _StubNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {}}
|
||||
|
||||
|
||||
def test_get_immediate_node_signature_canonicalizes_non_link_inputs(monkeypatch):
|
||||
live_value = [1, {"nested": [2, 3]}]
|
||||
dynprompt = _StubDynPrompt(
|
||||
{
|
||||
"1": {
|
||||
"class_type": "TestCacheNode",
|
||||
"inputs": {"value": live_value},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setitem(caching.nodes.NODE_CLASS_MAPPINGS, "TestCacheNode", _StubNode)
|
||||
monkeypatch.setattr(caching, "NODE_CLASS_CONTAINS_UNIQUE_ID", {})
|
||||
|
||||
keyset = caching.CacheKeySetInputSignature(dynprompt, [], _StubIsChangedCache())
|
||||
signature = asyncio.run(keyset.get_immediate_node_signature(dynprompt, "1", {}))
|
||||
|
||||
assert signature == (
|
||||
"TestCacheNode",
|
||||
None,
|
||||
("value", ("list", (1, ("dict", (("nested", ("list", (2, 3))),))))),
|
||||
)
|
||||
|
||||
|
||||
def test_to_hashable_walks_dicts_without_rebinding_traversal_stack():
|
||||
live_value = {
|
||||
"outer": {"nested": [2, 3]},
|
||||
"items": [{"leaf": 4}],
|
||||
}
|
||||
|
||||
assert caching.to_hashable(live_value) == (
|
||||
"dict",
|
||||
(
|
||||
("items", ("list", (("dict", (("leaf", 4),)),))),
|
||||
("outer", ("dict", (("nested", ("list", (2, 3))),))),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_get_immediate_node_signature_fails_closed_for_opaque_non_link_input(monkeypatch):
|
||||
class OpaqueRuntimeValue:
|
||||
pass
|
||||
|
||||
live_value = OpaqueRuntimeValue()
|
||||
dynprompt = _StubDynPrompt(
|
||||
{
|
||||
"1": {
|
||||
"class_type": "TestCacheNode",
|
||||
"inputs": {"value": live_value},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setitem(caching.nodes.NODE_CLASS_MAPPINGS, "TestCacheNode", _StubNode)
|
||||
monkeypatch.setattr(caching, "NODE_CLASS_CONTAINS_UNIQUE_ID", {})
|
||||
|
||||
keyset = caching.CacheKeySetInputSignature(dynprompt, [], _StubIsChangedCache())
|
||||
signature = asyncio.run(keyset.get_immediate_node_signature(dynprompt, "1", {}))
|
||||
|
||||
assert signature[:2] == ("TestCacheNode", None)
|
||||
assert signature[2][0] == "value"
|
||||
assert type(signature[2][1]) is caching.Unhashable
|
||||
|
||||
|
||||
def test_get_node_signature_never_visits_raw_non_link_input(monkeypatch):
|
||||
live_value = [1, 2, 3]
|
||||
dynprompt = _StubDynPrompt(
|
||||
{
|
||||
"1": {
|
||||
"class_type": "TestCacheNode",
|
||||
"inputs": {"value": live_value},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setitem(caching.nodes.NODE_CLASS_MAPPINGS, "TestCacheNode", _StubNode)
|
||||
monkeypatch.setattr(caching, "NODE_CLASS_CONTAINS_UNIQUE_ID", {})
|
||||
monkeypatch.setattr(
|
||||
caching,
|
||||
"_signature_to_hashable",
|
||||
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("outer signature canonicalizer should not run")
|
||||
),
|
||||
)
|
||||
|
||||
keyset = caching.CacheKeySetInputSignature(dynprompt, [], _StubIsChangedCache())
|
||||
signature = asyncio.run(keyset.get_node_signature(dynprompt, "1"))
|
||||
|
||||
assert isinstance(signature, tuple)
|
||||
|
||||
|
||||
def test_get_node_signature_keeps_deep_canonicalized_input_fragment(monkeypatch):
|
||||
live_value = 1
|
||||
for _ in range(8):
|
||||
live_value = [live_value]
|
||||
expected = caching.to_hashable(live_value)
|
||||
|
||||
dynprompt = _StubDynPrompt(
|
||||
{
|
||||
"1": {
|
||||
"class_type": "TestCacheNode",
|
||||
"inputs": {"value": live_value},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setitem(caching.nodes.NODE_CLASS_MAPPINGS, "TestCacheNode", _StubNode)
|
||||
monkeypatch.setattr(caching, "NODE_CLASS_CONTAINS_UNIQUE_ID", {})
|
||||
|
||||
keyset = caching.CacheKeySetInputSignature(dynprompt, [], _StubIsChangedCache())
|
||||
signature = asyncio.run(keyset.get_node_signature(dynprompt, "1"))
|
||||
|
||||
assert isinstance(signature, tuple)
|
||||
assert signature[0][2][0] == "value"
|
||||
assert signature[0][2][1] == expected
|
||||
|
||||
|
||||
def test_get_node_signature_keeps_large_precanonicalized_fragment(monkeypatch):
|
||||
live_value = object()
|
||||
canonical_fragment = ("tuple", tuple(("list", (index, index + 1)) for index in range(256)))
|
||||
dynprompt = _StubDynPrompt(
|
||||
{
|
||||
"1": {
|
||||
"class_type": "TestCacheNode",
|
||||
"inputs": {"value": live_value},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setitem(caching.nodes.NODE_CLASS_MAPPINGS, "TestCacheNode", _StubNode)
|
||||
monkeypatch.setattr(caching, "NODE_CLASS_CONTAINS_UNIQUE_ID", {})
|
||||
monkeypatch.setattr(
|
||||
caching,
|
||||
"to_hashable",
|
||||
lambda value, max_nodes=caching._MAX_SIGNATURE_CONTAINER_VISITS: (
|
||||
canonical_fragment if value is live_value else caching.Unhashable()
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
caching,
|
||||
"_signature_to_hashable",
|
||||
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("outer signature canonicalizer should not run")
|
||||
),
|
||||
)
|
||||
|
||||
keyset = caching.CacheKeySetInputSignature(dynprompt, [], _StubIsChangedCache())
|
||||
signature = asyncio.run(keyset.get_node_signature(dynprompt, "1"))
|
||||
|
||||
assert isinstance(signature, tuple)
|
||||
assert signature[0][2] == ("value", canonical_fragment)
|
||||
Loading…
Reference in New Issue
Block a user