Repository: comfyanonymous/ComfyUI

Issue:
HyperTile node is nondeterministic across executions and messes with global randomness
The HyperTile node uses the random module and seeds the global random with its own counter variable.

Unfortunately, this counter variable is retained across executions if the HyperTile parameters don't change, and so every execution will have different results.

The effect on global random can be avoided just by using a `random.Random()` instance instead of a counter, but since ComfyUI doesn't provide any kind of after-exec function for nodes, there doesn't seem to be a way to reset it to its initial state after one prompt is executed.

I suppose you could work around this by setting having IS_CHANGED return something so that the node gets always executed, thus reinitializing randomness, but that might cause any nodes that come after the HyperTile node to needlessly re-execute.




Discussion:
This makes things deterministic but at the expense of running the node every time. Dunno if there's a better way to force it.
```
diff --git a/comfy_extras/nodes_hypertile.py b/comfy_extras/nodes_hypertile.py
index 0d7d4c9..63f45f8 100644
--- a/comfy_extras/nodes_hypertile.py
+++ b/comfy_extras/nodes_hypertile.py
@@ -4,7 +4,7 @@ import math
 from einops import rearrange
 import random
 
-def random_divisor(value: int, min_value: int, /, max_options: int = 1, counter = 0) -> int:
+def random_divisor(value: int, min_value: int, /, max_options: int = 1, random = random) -> int:
     min_value = min(min_value, value)
 
     # All big divisors of value (inclusive)
@@ -12,12 +12,18 @@ def random_divisor(value: int, min_value: int, /, max_options: int = 1, counter
 
     ns = [value // i for i in divisors[:max_options]]  # has at least 1 element
 
-    random.seed(counter)
     idx = random.randint(0, len(ns) - 1)
 
     return ns[idx]
 
 class HyperTile:
+    counter = 0
+
+    @classmethod
+    def IS_CHANGED(*args, **kwargs):
+        HyperTile.counter += 1
+        return HyperTile.counter
+
     @classmethod
     def INPUT_TYPES(s):
         return {"required": { "model": ("MODEL",),
@@ -42,7 +48,7 @@ class HyperTile:
 
         latent_tile_size = max(32, tile_size) // 8
         self.temp = None
-        self.counter = 1
+        self.rand = random.Random(1)
 
         def hypertile_in(q, k, v, extra_options):
             if q.shape[-1] in apply_to:
@@ -53,10 +59,8 @@ class HyperTile:
                 h, w = round(math.sqrt(hw * aspect_ratio)), round(math.sqrt(hw / aspect_ratio))
 
                 factor = 2**((q.shape[-1] // model_channels) - 1) if scale_depth else 1
-                nh = random_divisor(h, latent_tile_size * factor, swap_size, self.counter)
-                self.counter += 1
-                nw = random_divisor(w, latent_tile_size * factor, swap_size, self.counter)
-                self.counter += 1
+                nh = random_divisor(h, latent_tile_size * factor, swap_size, self.rand)
+                nw = random_divisor(w, latent_tile_size * factor, swap_size, self.rand)
 
                 if nh * nw > 1:
                     q = rearrange(q, "b (nh h nw w) c -> (b nh nw) (h w) c", h=h // nh, w=w // nw, nh=nh, nw=nw)
-- 
2.39.3
```
> The HyperTile node uses the random module and seeds the global random with its own counter variable.
> 
> Unfortunately, this counter variable is retained across executions if the HyperTile parameters don't change, and so every execution will have different results.
> 
> The effect on global random can be avoided just by using a `random.Random()` instance instead of a counter, but since ComfyUI doesn't provide any kind of after-exec function for nodes, there doesn't seem to be a way to reset it to its initial state after one prompt is executed.
> 
> I suppose you could work around this by setting having IS_CHANGED return something so that the node gets always executed, thus reinitializing randomness, but that might cause any nodes that come after the HyperTile node to needlessly re-execute.

I've been tinkering with hypertile in my workflow so I'm extremely interested in this, was hoping someone would come into the discussion because my knowledge is limited. 

But would this help things? 

https://github.com/comfyanonymous/ComfyUI/pull/931

The while loop I imagine, would. 

Resolving patch:
diff --git a/comfy_extras/nodes_hypertile.py b/comfy_extras/nodes_hypertile.py
--- a/comfy_extras/nodes_hypertile.py
+++ b/comfy_extras/nodes_hypertile.py
@@ -2,9 +2,10 @@
 
 import math
 from einops import rearrange
-import random
+# Use torch rng for consistency across generations
+from torch import randint
 
-def random_divisor(value: int, min_value: int, /, max_options: int = 1, counter = 0) -> int:
+def random_divisor(value: int, min_value: int, /, max_options: int = 1) -> int:
     min_value = min(min_value, value)
 
     # All big divisors of value (inclusive)
@@ -12,8 +13,7 @@ def random_divisor(value: int, min_value: int, /, max_options: int = 1, counter
 
     ns = [value // i for i in divisors[:max_options]]  # has at least 1 element
 
-    random.seed(counter)
-    idx = random.randint(0, len(ns) - 1)
+    idx = randint(low=0, high=len(ns) - 1, size=(1,)).item()
 
     return ns[idx]
 
@@ -42,7 +42,6 @@ def patch(self, model, tile_size, swap_size, max_depth, scale_depth):
 
         latent_tile_size = max(32, tile_size) // 8
         self.temp = None
-        self.counter = 1
 
         def hypertile_in(q, k, v, extra_options):
             if q.shape[-1] in apply_to:
@@ -53,10 +52,8 @@ def hypertile_in(q, k, v, extra_options):
                 h, w = round(math.sqrt(hw * aspect_ratio)), round(math.sqrt(hw / aspect_ratio))
 
                 factor = 2**((q.shape[-1] // model_channels) - 1) if scale_depth else 1
-                nh = random_divisor(h, latent_tile_size * factor, swap_size, self.counter)
-                self.counter += 1
-                nw = random_divisor(w, latent_tile_size * factor, swap_size, self.counter)
-                self.counter += 1
+                nh = random_divisor(h, latent_tile_size * factor, swap_size)
+                nw = random_divisor(w, latent_tile_size * factor, swap_size)
 
                 if nh * nw > 1:
                     q = rearrange(q, "b (nh h nw w) c -> (b nh nw) (h w) c", h=h // nh, w=w // nw, nh=nh, nw=nw)


Test patch: