diff --git a/comfy_execution/caching.py b/comfy_execution/caching.py index 0342d7ec2..ca8b15918 100644 --- a/comfy_execution/caching.py +++ b/comfy_execution/caching.py @@ -246,7 +246,10 @@ def _signature_to_hashable_impl(obj, depth=0, max_depth=_MAX_SIGNATURE_DEPTH, ac def _signature_to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS): """Build the final cache-signature representation in one fail-closed pass.""" - result = _signature_to_hashable_impl(obj, budget={"remaining": max_nodes}) + try: + result = _signature_to_hashable_impl(obj, budget={"remaining": max_nodes}) + except RuntimeError: + return Unhashable() if result is _FAILED_SIGNATURE: return Unhashable() return result[0] @@ -320,10 +323,20 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS): items = snapshots.pop(current_id, None) if items is None: items = list(current.items()) - memo[current_id] = ( - "dict", - tuple((resolve_value(k), resolve_value(v)) for k, v in items), - ) + ordered_items = [ + (_sanitized_sort_key(k, memo=sort_memo), resolve_value(k), resolve_value(v)) + for k, v in items + ] + ordered_items.sort(key=lambda item: item[0]) + for index in range(1, len(ordered_items)): + if ordered_items[index - 1][0] == ordered_items[index][0]: + memo[current_id] = Unhashable() + break + else: + memo[current_id] = ( + "dict", + tuple((key, value) for _, key, value in ordered_items), + ) elif current_type is list: items = snapshots.pop(current_id, None) if items is None: diff --git a/tests-unit/execution_test/caching_test.py b/tests-unit/execution_test/caching_test.py index c86359aa9..c96f40722 100644 --- a/tests-unit/execution_test/caching_test.py +++ b/tests-unit/execution_test/caching_test.py @@ -205,6 +205,17 @@ def test_to_hashable_handles_shared_builtin_substructures(caching_module): assert hashable[1][0][0] == "list" +def test_to_hashable_canonicalizes_dict_insertion_order(caching_module): + """Dicts with the same content should hash identically regardless of insertion order.""" + caching, _ = caching_module + + first = {"b": 2, "a": 1} + second = {"a": 1, "b": 2} + + assert caching.to_hashable(first) == ("dict", (("a", 1), ("b", 2))) + assert caching.to_hashable(first) == caching.to_hashable(second) + + @pytest.mark.parametrize( "container_factory", [ @@ -227,6 +238,19 @@ def test_to_hashable_fails_closed_on_runtimeerror(caching_module, monkeypatch, c assert isinstance(hashable, caching.Unhashable) +def test_to_hashable_fails_closed_for_ambiguous_dict_ordering(caching_module): + """Ambiguous dict key ordering should fail closed instead of using insertion order.""" + caching, _ = caching_module + ambiguous = { + _OpaqueValue(): 1, + _OpaqueValue(): 2, + } + + hashable = caching.to_hashable(ambiguous) + + assert isinstance(hashable, caching.Unhashable) + + def test_signature_to_hashable_fails_closed_for_ambiguous_dict_ordering(caching_module): """Ambiguous dict sort ties should fail closed instead of depending on input order.""" caching, _ = caching_module