feat(dynamic_ramcache): 支持新版ComfyUI双RAM缓存阈值,新增inactive参数
重构缓存参数处理逻辑,适配ComfyUI新版`--cache-ram`双参数格式 更新中英文README文档,补充新参数的使用说明与阈值解释 优化缓存清理与切换的执行逻辑,兼容旧版ComfyUI环境 修复缓存实例创建的参数兼容问题
This commit is contained in:
@@ -9,8 +9,8 @@ A custom node for ComfyUI that dynamically manages RAM usage by intelligently co
|
||||
- **Dual Cache Management Modes**:
|
||||
- CLASSIC (No Eviction): Standard cache behavior with no automatic purging
|
||||
- RAM_PRESSURE (Auto Purge): Smart cache purging when system memory falls below the threshold
|
||||
- **RAM Cache Extreme Cleanup (🧹)**: Performs a one-time aggressive purge and automatically restores the previous mode and threshold.
|
||||
- **Configurable Memory Threshold**: Fine-tune the minimum amount of free RAM to maintain
|
||||
- **RAM Cache Extreme Cleanup (🧹)**: Performs a one-time aggressive purge and automatically restores the previous mode and thresholds.
|
||||
- **Configurable Memory Thresholds**: Supports the newer `--cache-ram active inactive` RAM threshold pair
|
||||
- **Seamless Cache Migration**: Preserves essential cache data when switching between modes
|
||||
- **Real-time Memory Monitoring**: Continuously checks system RAM and virtual memory availability
|
||||
|
||||
@@ -34,7 +34,13 @@ A custom node for ComfyUI that dynamically manages RAM usage by intelligently co
|
||||
- **cleanup_threshold**:
|
||||
- Range: 0.1 - 256.0 GB
|
||||
- Default: 2.0 GB
|
||||
- Determines the minimum amount of free RAM to maintain (in gigabytes)
|
||||
- Active cache free RAM threshold, matching the first `--cache-ram` value
|
||||
|
||||
- **inactive_threshold**:
|
||||
- Range: 0 - 256.0 GB
|
||||
- Default: 0 GB
|
||||
- Inactive cache / pinned memory threshold, matching the optional second `--cache-ram` value
|
||||
- Optional for old workflows. `0` keeps ComfyUI's current value, and older ComfyUI builds ignore it
|
||||
|
||||
### Extreme Cleanup Parameters
|
||||
|
||||
@@ -44,7 +50,7 @@ A custom node for ComfyUI that dynamically manages RAM usage by intelligently co
|
||||
- Temporary headroom used for the one-shot purge
|
||||
|
||||
- **restore behavior**:
|
||||
- Automatically restores the mode and threshold that were active before cleanup
|
||||
- Automatically restores the mode and both thresholds that were active before cleanup
|
||||
|
||||
3. Connect the node anywhere in your workflow - it will continuously monitor and manage cache based on your settings
|
||||
4. Optionally add the "RAMCacheExtremeCleanup" node at the end of a workflow to trigger a one-shot cleanup and restore the previous state
|
||||
@@ -62,7 +68,7 @@ In CLASSIC mode, the node maintains standard cache behavior without automatic pu
|
||||
|
||||
In RAM_PRESSURE mode, the node actively monitors system memory and intelligently purges cache when:
|
||||
1. Available system RAM falls below the specified cleanup_threshold
|
||||
2. The system is experiencing memory pressure
|
||||
2. Inactive cache / pinned memory exceeds the inactive_threshold behavior in newer ComfyUI builds
|
||||
|
||||
When purging, it prioritizes removing older, less recently used cache items while attempting to preserve critical data.
|
||||
|
||||
|
||||
+13
-6
@@ -8,7 +8,7 @@
|
||||
|
||||
- **动态缓存模式切换**:支持在CLASSIC(无内存回收)和RAM_PRESSURE(自动内存清理)模式之间切换
|
||||
- **智能内存管理**:自动监控和清理 RAM 缓存,当内存不足时释放不必要的缓存数据
|
||||
- **自定义清理阈值**:允许用户设置最小空闲内存保持量,灵活适应不同硬件配置
|
||||
- **自定义清理阈值**:支持新版 `--cache-ram active inactive` 两个 RAM 阈值
|
||||
- **无缝数据迁移**:在切换缓存模式时保留现有缓存数据,避免重复计算
|
||||
- **直观的节点接口**:简单易用的参数设置,适用于不同水平的用户
|
||||
- **极限清理节点**:一次性强清理(包含虚拟内存占用)并自动恢复清理前的模式与阈值
|
||||
@@ -25,7 +25,8 @@
|
||||
1. 在ComfyUI中,从`utils/dynamic_ramcache`类别中添加`DynamicRAMCacheControl`节点
|
||||
2. 配置以下参数:
|
||||
- **mode**:选择缓存模式(CLASSIC或RAM_PRESSURE)
|
||||
- **cleanup_threshold**:设置最小空闲内存保持量(GB)
|
||||
- **cleanup_threshold**:设置 active cache 空闲内存阈值(GB)
|
||||
- **inactive_threshold**:可选。设置 inactive cache / pinned memory 阈值(GB),填 `0` 或旧工作流缺少此项时沿用 ComfyUI 当前值
|
||||
3. 可以选择性地连接任意输入到`any_input`端口(节点会透传此输入)
|
||||
4. 可选:在工作流末尾添加`RAMCacheExtremeCleanup`节点,执行一次性清理并恢复之前状态
|
||||
5. 运行工作流
|
||||
@@ -37,16 +38,22 @@
|
||||
- **CLASSIC (No Eviction)**:传统缓存模式,不会自动清理缓存,可能导致内存使用持续增长
|
||||
- **RAM_PRESSURE (Auto Purge)**:自动内存清理模式,当可用内存低于设定阈值时会自动清理缓存
|
||||
|
||||
### cleanup_threshold(清理阈值)
|
||||
### cleanup_threshold(active 阈值)
|
||||
|
||||
- 类型:浮点数(0.1-256.0 GB)
|
||||
- 默认值:2.0 GB
|
||||
- 说明:系统尝试维持的最小空闲内存量。当可用内存低于此值时,RAM_PRESSURE模式会触发缓存清理
|
||||
- 说明:对应新版 `--cache-ram` 的第一个值。可用内存低于此值时,RAM_PRESSURE模式会清理 active cache
|
||||
|
||||
### inactive_threshold(inactive 阈值)
|
||||
|
||||
- 类型:浮点数(0-256.0 GB)
|
||||
- 默认值:0
|
||||
- 说明:对应新版 `--cache-ram` 的第二个值。旧版 ComfyUI 不使用该参数时会自动忽略;`0` 表示不改 ComfyUI 当前值;大于 `0` 时会更新 inactive cache / pinned memory 阈值
|
||||
|
||||
### 极限清理参数
|
||||
|
||||
- **purge_threshold**:一次性清理时使用的临时阈值(默认 256.0 GB)
|
||||
- **恢复行为**:自动恢复清理前的模式与阈值
|
||||
- **恢复行为**:自动恢复清理前的模式、active 阈值和 inactive 阈值
|
||||
|
||||
### 输出
|
||||
|
||||
@@ -59,7 +66,7 @@
|
||||
1. 检测ComfyUI的`PromptExecutor`实例
|
||||
2. 根据所选模式在`RAMPressureCache`和`HierarchicalCache`之间切换
|
||||
3. 在切换过程中保留现有的缓存数据
|
||||
4. 当处于RAM_PRESSURE模式时,监控系统内存并在需要时触发缓存清理
|
||||
4. 当处于RAM_PRESSURE模式时,按 active / inactive 两个阈值触发缓存清理
|
||||
|
||||
## 兼容性要求
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import gc
|
||||
import logging
|
||||
import time
|
||||
import gc
|
||||
import inspect
|
||||
import logging
|
||||
import time
|
||||
|
||||
caching = None
|
||||
execution = None
|
||||
@@ -44,25 +45,26 @@ class DynamicRAMCacheControl:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"mode": (["CLASSIC (No Eviction)", "RAM_PRESSURE (Auto Purge)"], {"default": "RAM_PRESSURE (Auto Purge)"}),
|
||||
"cleanup_threshold": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 256.0, "step": 0.1, "tooltip": "Minimum free RAM to maintain (GB)"}),
|
||||
},
|
||||
"optional": {
|
||||
"any_input": (any_type, {}),
|
||||
}
|
||||
}
|
||||
"required": {
|
||||
"mode": (["CLASSIC (No Eviction)", "RAM_PRESSURE (Auto Purge)"], {"default": "RAM_PRESSURE (Auto Purge)"}),
|
||||
"cleanup_threshold": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 256.0, "step": 0.1, "tooltip": "Active cache free RAM threshold (GB)"}),
|
||||
},
|
||||
"optional": {
|
||||
"inactive_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 256.0, "step": 0.1, "tooltip": "Inactive cache / pinned memory threshold (GB). 0 keeps ComfyUI's current value."}),
|
||||
"any_input": (any_type, {}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,)
|
||||
RETURN_NAMES = ("output_passthrough",)
|
||||
FUNCTION = "manage_cache"
|
||||
CATEGORY = "utils/dynamic_ramcache"
|
||||
|
||||
def manage_cache(self, mode, cleanup_threshold, any_input=None):
|
||||
if caching is not None and execution is not None:
|
||||
self._execute_cache_logic(mode, cleanup_threshold)
|
||||
else:
|
||||
logging.warning("[DynamicRAMCache] Plugin disabled: Missing internal modules.")
|
||||
def manage_cache(self, mode, cleanup_threshold, inactive_threshold=0.0, any_input=None):
|
||||
if caching is not None and execution is not None:
|
||||
self._execute_cache_logic(mode, cleanup_threshold, inactive_threshold)
|
||||
else:
|
||||
logging.warning("[DynamicRAMCache] Plugin disabled: Missing internal modules.")
|
||||
|
||||
if any_input is not None:
|
||||
return (any_input,)
|
||||
@@ -73,24 +75,24 @@ class DynamicRAMCacheControl:
|
||||
except ImportError:
|
||||
return (None,)
|
||||
|
||||
def _execute_cache_logic(self, mode, cleanup_threshold):
|
||||
|
||||
target_mode_ram = "RAM_PRESSURE" in mode
|
||||
|
||||
executor = self._find_executor()
|
||||
def _execute_cache_logic(self, mode, cleanup_threshold, inactive_threshold=0.0):
|
||||
|
||||
target_mode_ram = "RAM_PRESSURE" in mode
|
||||
|
||||
executor = self._find_executor()
|
||||
|
||||
if executor is None:
|
||||
logging.warning("[DynamicRAMCache] PromptExecutor not found.")
|
||||
return
|
||||
|
||||
if not hasattr(executor, 'cache_args'):
|
||||
executor.cache_args = {}
|
||||
|
||||
old_ram_arg = executor.cache_args.get('ram', 0)
|
||||
executor.cache_args['ram'] = cleanup_threshold
|
||||
|
||||
cache_set = self._get_cache_set(executor)
|
||||
if cache_set is None:
|
||||
old_ram_arg, old_ram_inactive_arg, active_headroom, inactive_headroom, supports_inactive = self._update_cache_args(
|
||||
executor,
|
||||
cleanup_threshold,
|
||||
inactive_threshold,
|
||||
)
|
||||
|
||||
cache_set = self._get_cache_set(executor)
|
||||
if cache_set is None:
|
||||
return
|
||||
|
||||
current_cache = cache_set.outputs
|
||||
@@ -110,23 +112,130 @@ class DynamicRAMCacheControl:
|
||||
|
||||
is_currently_ram = isinstance(current_cache, RAMPressureCacheClass)
|
||||
|
||||
if target_mode_ram and not is_currently_ram:
|
||||
self._switch_to_ram_pressure(cache_set, current_cache, caching)
|
||||
logging.info(f"[DynamicRAMCache] Switched mode: CLASSIC -> RAM_PRESSURE (Headroom: {cleanup_threshold}GB)")
|
||||
|
||||
elif not target_mode_ram and is_currently_ram:
|
||||
self._switch_to_classic(cache_set, current_cache, caching)
|
||||
logging.info(f"[DynamicRAMCache] Switched mode: RAM_PRESSURE -> CLASSIC")
|
||||
|
||||
elif target_mode_ram and is_currently_ram:
|
||||
if old_ram_arg != cleanup_threshold:
|
||||
logging.info(f"[DynamicRAMCache] Updated RAM Headroom: {old_ram_arg}GB -> {cleanup_threshold}GB")
|
||||
|
||||
if target_mode_ram and hasattr(cache_set.outputs, 'poll'):
|
||||
try:
|
||||
cache_set.outputs.poll(cleanup_threshold)
|
||||
except Exception:
|
||||
pass
|
||||
if target_mode_ram and not is_currently_ram:
|
||||
self._switch_to_ram_pressure(cache_set, current_cache, caching)
|
||||
self._set_executor_cache_type(executor, target_mode_ram)
|
||||
logging.info(self._format_headroom_log("[DynamicRAMCache] Switched mode: CLASSIC -> RAM_PRESSURE", active_headroom, inactive_headroom, supports_inactive))
|
||||
|
||||
elif not target_mode_ram and is_currently_ram:
|
||||
self._switch_to_classic(cache_set, current_cache, caching)
|
||||
self._set_executor_cache_type(executor, target_mode_ram)
|
||||
logging.info(f"[DynamicRAMCache] Switched mode: RAM_PRESSURE -> CLASSIC")
|
||||
|
||||
elif target_mode_ram and is_currently_ram:
|
||||
self._set_executor_cache_type(executor, target_mode_ram)
|
||||
if old_ram_arg != active_headroom or old_ram_inactive_arg != inactive_headroom:
|
||||
logging.info(self._format_headroom_update_log(old_ram_arg, old_ram_inactive_arg, active_headroom, inactive_headroom, supports_inactive))
|
||||
|
||||
if target_mode_ram:
|
||||
self._release_ram_cache(cache_set.outputs, active_headroom, inactive_headroom, supports_inactive)
|
||||
|
||||
def _read_threshold(self, value, default_value):
|
||||
try:
|
||||
value = float(value)
|
||||
except (TypeError, ValueError):
|
||||
value = default_value
|
||||
return value
|
||||
|
||||
def _update_cache_args(self, executor, cleanup_threshold, inactive_threshold):
|
||||
if not hasattr(executor, 'cache_args') or executor.cache_args is None:
|
||||
executor.cache_args = {}
|
||||
|
||||
cache_args = executor.cache_args
|
||||
supports_inactive = self._supports_inactive_cache_arg(executor)
|
||||
active_headroom = self._read_threshold(cleanup_threshold, 2.0)
|
||||
old_ram_arg = self._read_threshold(cache_args.get('ram'), active_headroom)
|
||||
|
||||
if supports_inactive:
|
||||
old_ram_inactive_arg = self._read_threshold(cache_args.get('ram_inactive'), old_ram_arg)
|
||||
inactive_value = self._read_threshold(inactive_threshold, 0.0)
|
||||
if inactive_value > 0:
|
||||
inactive_headroom = inactive_value
|
||||
elif old_ram_inactive_arg > 0:
|
||||
inactive_headroom = old_ram_inactive_arg
|
||||
else:
|
||||
inactive_headroom = active_headroom
|
||||
cache_args['ram_inactive'] = inactive_headroom
|
||||
else:
|
||||
old_ram_inactive_arg = None
|
||||
inactive_headroom = None
|
||||
cache_args.pop('ram_inactive', None)
|
||||
|
||||
cache_args.setdefault('lru', 0)
|
||||
cache_args['ram'] = active_headroom
|
||||
return old_ram_arg, old_ram_inactive_arg, active_headroom, inactive_headroom, supports_inactive
|
||||
|
||||
def _supports_inactive_cache_arg(self, executor):
|
||||
cache_args = getattr(executor, 'cache_args', None)
|
||||
if isinstance(cache_args, dict) and 'ram_inactive' in cache_args:
|
||||
return True
|
||||
|
||||
PromptExecutor = getattr(execution, 'PromptExecutor', None)
|
||||
execute_async = getattr(PromptExecutor, 'execute_async', None)
|
||||
if execute_async is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
return 'ram_inactive' in inspect.getsource(execute_async)
|
||||
except (OSError, TypeError):
|
||||
return False
|
||||
|
||||
def _set_executor_cache_type(self, executor, target_mode_ram):
|
||||
CacheType = getattr(execution, 'CacheType', None)
|
||||
if CacheType is None:
|
||||
return
|
||||
|
||||
if target_mode_ram:
|
||||
ram_type = getattr(CacheType, 'RAM_PRESSURE', None)
|
||||
if ram_type is not None:
|
||||
executor.cache_type = ram_type
|
||||
return
|
||||
|
||||
classic_type = getattr(CacheType, 'CLASSIC', None)
|
||||
if classic_type is not None:
|
||||
executor.cache_type = classic_type
|
||||
|
||||
def _format_headroom_log(self, prefix, active_headroom, inactive_headroom, supports_inactive):
|
||||
if supports_inactive:
|
||||
return f"{prefix} (active: {active_headroom}GB, inactive: {inactive_headroom}GB)"
|
||||
return f"{prefix} (headroom: {active_headroom}GB)"
|
||||
|
||||
def _format_headroom_update_log(self, old_ram_arg, old_ram_inactive_arg, active_headroom, inactive_headroom, supports_inactive):
|
||||
if supports_inactive:
|
||||
return f"[DynamicRAMCache] Updated RAM headroom: active {old_ram_arg}GB -> {active_headroom}GB, inactive {old_ram_inactive_arg}GB -> {inactive_headroom}GB"
|
||||
return f"[DynamicRAMCache] Updated RAM headroom: {old_ram_arg}GB -> {active_headroom}GB"
|
||||
|
||||
def _release_ram_cache(self, cache, active_headroom, inactive_headroom, supports_inactive):
|
||||
ram_release = getattr(cache, 'ram_release', None)
|
||||
if callable(ram_release):
|
||||
try:
|
||||
if supports_inactive and inactive_headroom is not None:
|
||||
ram_release(int(inactive_headroom * (1024 ** 3)))
|
||||
ram_release(int(active_headroom * (1024 ** 3)), free_active=True)
|
||||
return
|
||||
except TypeError:
|
||||
try:
|
||||
ram_release(int(active_headroom * (1024 ** 3)))
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.warning(f"[DynamicRAMCache] RAM release failed: {e}")
|
||||
|
||||
poll = getattr(cache, 'poll', None)
|
||||
if callable(poll):
|
||||
try:
|
||||
if supports_inactive:
|
||||
poll(ram=active_headroom, ram_inactive=inactive_headroom)
|
||||
else:
|
||||
poll(active_headroom)
|
||||
except TypeError:
|
||||
try:
|
||||
poll(active_headroom)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _find_executor(self):
|
||||
for obj in gc.get_objects():
|
||||
@@ -169,7 +278,7 @@ class DynamicRAMCacheControl:
|
||||
if not key_class:
|
||||
key_class = getattr(caching_mod, 'CacheKeySetInputSignature', None)
|
||||
|
||||
new_cache = caching_mod.RAMPressureCache(key_class)
|
||||
new_cache = self._create_cache(caching_mod.RAMPressureCache, key_class)
|
||||
self._migrate_cache_data(old_cache, new_cache)
|
||||
if getattr(new_cache, 'timestamps', None) is None:
|
||||
new_cache.timestamps = {}
|
||||
@@ -197,12 +306,18 @@ class DynamicRAMCacheControl:
|
||||
if not key_class:
|
||||
key_class = getattr(caching_mod, 'CacheKeySetInputSignature', None)
|
||||
|
||||
new_cache = caching_mod.HierarchicalCache(key_class)
|
||||
new_cache = self._create_cache(caching_mod.HierarchicalCache, key_class)
|
||||
self._migrate_cache_data(old_cache, new_cache)
|
||||
|
||||
self._update_cache_set(cache_set, new_cache)
|
||||
|
||||
def _migrate_cache_data(self, old_cache, new_cache):
|
||||
self._update_cache_set(cache_set, new_cache)
|
||||
|
||||
def _create_cache(self, cache_class, key_class):
|
||||
try:
|
||||
return cache_class(key_class, enable_providers=True)
|
||||
except TypeError:
|
||||
return cache_class(key_class)
|
||||
|
||||
def _migrate_cache_data(self, old_cache, new_cache):
|
||||
"""迁移缓存核心数据"""
|
||||
try:
|
||||
old_dict = getattr(old_cache, '__dict__', None)
|
||||
@@ -262,19 +377,20 @@ class RAMCacheExtremeCleanup(DynamicRAMCacheControl):
|
||||
if executor is None:
|
||||
logging.warning("[DynamicRAMCache] PromptExecutor not found.")
|
||||
else:
|
||||
if not hasattr(executor, 'cache_args'):
|
||||
executor.cache_args = {}
|
||||
old_ram_arg = executor.cache_args.get('ram', 2.0)
|
||||
cache_set = self._get_cache_set(executor)
|
||||
if cache_set is not None:
|
||||
if not hasattr(executor, 'cache_args') or executor.cache_args is None:
|
||||
executor.cache_args = {}
|
||||
old_ram_arg = self._read_threshold(executor.cache_args.get('ram'), 2.0)
|
||||
old_ram_inactive_arg = self._read_threshold(executor.cache_args.get('ram_inactive'), old_ram_arg)
|
||||
cache_set = self._get_cache_set(executor)
|
||||
if cache_set is not None:
|
||||
RAMPressureCacheClass = getattr(caching, 'RAMPressureCache', None)
|
||||
if RAMPressureCacheClass:
|
||||
is_currently_ram = isinstance(cache_set.outputs, RAMPressureCacheClass)
|
||||
old_mode = "RAM_PRESSURE (Auto Purge)" if is_currently_ram else "CLASSIC (No Eviction)"
|
||||
else:
|
||||
old_mode = "CLASSIC (No Eviction)"
|
||||
self._execute_cache_logic("RAM_PRESSURE (Auto Purge)", purge_threshold)
|
||||
self._execute_cache_logic(old_mode, old_ram_arg)
|
||||
self._execute_cache_logic("RAM_PRESSURE (Auto Purge)", purge_threshold, purge_threshold)
|
||||
self._execute_cache_logic(old_mode, old_ram_arg, old_ram_inactive_arg)
|
||||
else:
|
||||
logging.warning("[DynamicRAMCache] Plugin disabled: Missing internal modules.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user