11from collections import OrderedDict
22from functools import wraps
3+ from typing import Any
34
45import torch
56from diffusers import DiffusionPipeline
1213MAX_PROMPT_CACHE_SIZE = 64
1314
1415
16+ def _move_to_device (obj : Any , device ):
17+ """Recursively move tensors in a nested structure to a device."""
18+ if isinstance (obj , torch .Tensor ):
19+ # NOTE important we must detach and clone tensors before caching them
20+ return obj .detach ().clone ().to (device )
21+ if isinstance (obj , (list , tuple )):
22+ return type (obj )(_move_to_device (x , device ) for x in obj )
23+
24+ # Dict support removed as encode_prompt does not return dicts
25+ return obj
26+
27+
1528def clear_global_prompt_cache ():
1629 """Clear the global prompt embeddings cache."""
1730 GLOBAL_PROMPT_CACHE .clear ()
1831 logger .debug ("Global prompt cache cleared" )
1932
2033
21- def get_prompt_cache_total_mb () -> float :
22- """
23- Calculate the total memory usage of the prompt cache in Megabytes (MB).
24- Each element in GLOBAL_PROMPT_CACHE can be a Tensor or a tuple/list of Tensors.
25- """
26- total_bytes = 0
27-
28- def get_size (obj ):
29- if isinstance (obj , torch .Tensor ):
30- return obj .element_size () * obj .nelement ()
31- if isinstance (obj , (list , tuple )):
32- return sum (get_size (i ) for i in obj )
33- return 0
34-
35- for value in GLOBAL_PROMPT_CACHE .values ():
36- total_bytes += get_size (value )
37-
38- return total_bytes / (1024 * 1024 )
39-
40-
4134def get_prompt_cache_if_exists (cache_key ):
4235 """
4336 Retrieve cached result if it exists and move to Most Recently Used.
37+ Note: The caller is responsible for moving the result to the correct device.
4438 """
4539 if cache_key in GLOBAL_PROMPT_CACHE :
4640 GLOBAL_PROMPT_CACHE .move_to_end (cache_key )
@@ -52,14 +46,17 @@ def get_prompt_cache_if_exists(cache_key):
5246def add_prompt_cache (cache_key , result ):
5347 """
5448 Add a result to the global cache and manage its size.
49+ Automatically moves the result to CPU to save VRAM.
5550 """
56- GLOBAL_PROMPT_CACHE [cache_key ] = result
51+ # Move to CPU for storage
52+ cpu_result = _move_to_device (result , "cpu" )
53+
54+ GLOBAL_PROMPT_CACHE [cache_key ] = cpu_result
5755 if len (GLOBAL_PROMPT_CACHE ) > MAX_PROMPT_CACHE_SIZE :
5856 GLOBAL_PROMPT_CACHE .popitem (last = False ) # Remove Least Recently Used
5957
60- mb = get_prompt_cache_total_mb ()
6158 logger .info (
62- f"Prompt cached for { cache_key [0 ]} . Current cache size: { mb :.2f } MB ({ len (GLOBAL_PROMPT_CACHE )} /{ MAX_PROMPT_CACHE_SIZE } )"
59+ f"Prompt cached for { cache_key [0 ]} . Current cache size: ({ len (GLOBAL_PROMPT_CACHE )} /{ MAX_PROMPT_CACHE_SIZE } )"
6360 )
6461
6562
@@ -90,22 +87,19 @@ def enable_prompt_caching(pipeline: DiffusionPipeline) -> DiffusionPipeline:
9087 @wraps (original_encode_prompt )
9188 def wrapped_encode_prompt (* args , ** kwargs ):
9289 try :
93- # Create a cache key from identity and hashable representation of all arguments
94- # Identity ensures we don't use Flux embeddings for a Wan model, etc.
9590 cache_key = (pipeline_identity , make_hashable (args ), make_hashable (kwargs ))
9691 except (TypeError , ValueError ):
9792 logger .warning ("Failed to create hashable cache key; skipping prompt caching" )
98- # Fallback: if something isn't hashable, just compute normally
9993 return original_encode_prompt (* args , ** kwargs )
10094
10195 cached_result = get_prompt_cache_if_exists (cache_key )
10296 if cached_result is not None :
103- return cached_result
97+ # Move back to the target device (e.g. CUDA)
98+ target_device = kwargs .get ("device" ) or getattr (pipeline , "device" , torch .device ("cuda" ))
99+ return _move_to_device (cached_result , target_device )
104100
105- # Compute new results (e.g., prompt_embeds, negative_prompt_embeds)
106101 result = original_encode_prompt (* args , ** kwargs )
107102
108- # Store in global cache
109103 add_prompt_cache (cache_key , result )
110104
111105 return result
0 commit comments