一、对比背景:为什么缓存策略值得单独比较
围绕节点式 AI 绘图工作流引擎,大家讨论最多的是节点连接、工作流编排和算力大小,真正去看缓存细节的人不多。但从学习路径的角度看,缓存机制恰恰是区分“跑通一个工作流”与“运维一套多模型环境”的分水岭。
原因在于,只要开始在一台机器上同时堆多个模型、多个 LoRA、多套工作流,缓存策略就会直接决定三件事:
- 算力去向:重复执行时是全量重算浪费算力,还是命中缓存、把显存/内存用在刀刃上;
- 稳定性:进程或容器会不会因为历史结果堆积而莫名其妙 OOM(内存溢出);
- 切换体验:工作流之间来回切换时,是“秒级热身”还是“从头再来”。
本文的对比对象非常明确:同一引擎内的四种 outputs 缓存策略——CLASSIC、LRU、RAM_PRESSURE,以及作为对照的 NONE。对比维度聚焦三点:缓存键怎么算、什么时候命中、什么时候过期(淘汰),并延伸到多模型、多 LoRA、多工作流场景下的选型依据。
二、对象定义:两路缓存 + 四种策略
2.1 两类缓存:outputs 与 objects
执行一个工作流时,引擎内部维护两类缓存,职责完全不同:
- outputs 缓存:存放节点的中间结果和 UI 输出。命中时可以直接跳过该节点的执行,这是真正省算力的地方;
- objects 缓存:存放节点对象实例(类实例),避免每次执行都重新构造和初始化节点对象,节省的是对象构建与参数校验的开销。
两类缓存由一个统一的集合类管理,示意的伪代码结构如下:
class CacheType(Enum): CLASSIC = 0 LRU = 1 NONE = 2 RAM_PRESSURE = 3class CacheSet: def __init__(self, cache_type=None, cache_args={}): if cache_type == CacheType.NONE: self.init_null_cache() # 空缓存,等价于不缓存 elif cache_type == CacheType.RAM_PRESSURE: ram_threshold = cache_args.get("ram", 16.0) self.init_ram_cache(ram_threshold) # 按内存阈值管理 elif cache_type == CacheType.LRU: lru_size = cache_args.get("lru", 0) self.init_lru_cache(lru_size) # 按容量上限管理 else: self.init_classic_cache() # 默认全量保留 self.all = [self.outputs, self.objects]
2.2 四种策略与选择优先级
从启动配置到缓存策略的映射,优先级大致如下:
配置了 LRU 容量参数 → 采用 LRU 策略否则配置了内存阈值参数 → 采用 RAM_PRESSURE 策略否则配置了关闭缓存参数 → 采用 NONE 策略都未配置 → 默认 CLASSIC 策略
这里有两点需要特别强调:
- 键的构造方式与策略无关。无论选哪种策略,outputs 都按输入签名做 key,objects 都按(节点 ID + 节点类类型)做 key。策略改变的是“留多少、留多久”,而不是“怎么识别同一次计算”。
- 策略差异只作用于 outputs。objects 基本始终采用层级缓存结构组织。也就是说,四种策略对比的是“中间结果的驻留与淘汰”,节点对象的复用机制并不随策略变化。
三、相同点分析
四种策略并非四套互不相干的系统,它们共享同一套底层逻辑:
- 目标一致:都是为了跳过重复计算、复用已构造的节点对象、加速 UI 交互反馈,只是对“资源换时间”的取舍比例不同。
- 键的构造思想一致:outputs 的键是输入签名,通常综合了节点类型、参数取值以及对上游输出结果的引用;上游一旦重算产生新的结果对象,下游节点的签名随之变化,形成级联失效。这正是“输入签名 + 变化指纹”的含义——签名不仅描述输入是什么,还隐式携带了上游是否变过的信息。
- 失效语义一致:任何策略下,只要节点参数或上游结果发生变化,该节点必然重算,这一点不受缓存策略保护。策略只决定“没变的结果要不要继续留着”。
- 不影响正确性:策略选择只影响性能与资源占用,不改变执行结果本身。真正需要权衡的是命中率与内存水位之间的平衡。
四、核心差异分析:命中与淘汰机制逐个拆解
4.1 CLASSIC:全量保留,无主动淘汰
- 命中条件:输入签名一致,且历史结果仍驻留在缓存中。
- 过期机制:基本没有主动过期,结果会一直保留,直到被新执行覆盖或由外部机制回收显存。
- 优势:重复执行同一工作流时命中率最高;局部修改参数时,只有签名变化的节点及其下游会重算,上游全部命中,调试出图参数时体验最好。
- 代价:驻留的中间结果随历史执行不断累积,内存占用没有上限。单工作流、小模型时问题不明显;一旦多工作流混跑,旧结果大量挤占显存,容器环境尤其容易触发 OOM。
4.2 LRU:按容量设上限,淘汰最久未用
- 命中条件:输入签名一致,且该结果尚未被淘汰。
- 过期机制:缓存规模超过配置上限(示例代码中该参数默认取 0,实践中应结合工作流规模显式设定)时,淘汰最久未被访问的条目。
- 优势:资源占用有了可预期的上限;多工作流轮换时,冷门工作流的结果自动让位给热点工作流,符合“最近用过的还会再用”的访问局部性假设。
- 代价:其一,容量计量与单条结果的实际内存大小并不直接挂钩,大结果与小结果在容量计数中的权重相同(具体计量单位以实现为准),容量设小容易误删仍会复用的大结果;其二,命中率高度依赖实际工作集大小,工作集大于容量时会出现明显的“缓存抖动”。
4.3 RAM_PRESSURE:按内存水位,压力驱动淘汰
- 命中条件:输入签名一致,且结果未被压力释放。
- 过期机制:当内存占用超过配置阈值(示例代码中默认 16.0,通常按 GB 量级理解,具体以实现为准,可按机器规格调整)时,按压力释放缓存内容。
- 优势:直接贴近真实资源约束,是四种策略中最“懂内存”的一个。大模型、高分辨率输出、容器内存限额严格的环境下,能有效降低 OOM 概率。
- 代价:淘汰时机由实时内存水位决定,不完全可预测,命中率的波动比 LRU 更大;同时需要配合内存监控才能判断阈值是否合理。
4.4 NONE:完全关闭,作为对照与调试基线
- 命中条件:不存在,每次执行都全量重算。
- 用途:日常生产一般不会使用,但它是排查缓存问题的利器——当怀疑“结果没更新”“缓存串了”时,用 NONE 跑一遍即可确认问题是否出在缓存命中逻辑上;也适合做性能基准测试,量化缓存实际节省了多少算力。
4.5 差异总结表
| 维度 |
CLASSIC |
LRU |
RAM_PRESSURE |
NONE |
| 启用方式 |
默认 |
配置 LRU 容量参数 |
配置内存阈值参数 |
配置关闭缓存参数 |
| 缓存键 |
输入签名 |
输入签名 |
输入签名 |
不缓存 |
| 命中条件 |
签名一致且结果仍在 |
签名一致且未被淘汰 |
签名一致且未被释放 |
永不命中 |
| 淘汰机制 |
无主动淘汰 |
超上限淘汰最久未用 |
内存超阈值压力释放 |
不适用 |
| 内存上限 |
不可控 |
受容量参数约束 |
受内存阈值约束 |
无占用 |
| 命中可预测性 |
高(但内存不可预测) |
取决于工作集大小 |
波动较大 |
完全确定 |
| 典型用途 |
单工作流日常使用 |
多工作流轮换 |
大模型/容器环境 |
调试与基准对照 |
五、典型场景选择
- 场景一:单机单人、固定工作流调试出图。工作流结构稳定、模型规模不大、显存充裕,CLASSIC 的全量保留反而是优点——改一个参数只重算下游,出图迭代最快,此时引入其他策略收益有限。
- 场景二:多工作流轮换的教学、演示环境。多个工作流来回切换,CLASSIC 下每个工作流的历史结果都驻留不走,显存很快见顶。LRU 更合适:设定容量上限后,冷门工作流的结果自动被挤出,热点工作流保持秒级热身。
- 场景三:大模型 + 多 LoRA + 容器化部署。例如在百度智能云等云平台的 GPU 容器实例上部署时,容器内存限额通常比物理机更严格,OOM 会直接导致服务重启。RAM_PRESSURE 是更稳妥的默认选择,必要时与 LRU 组合使用,用内存水位兜底、用容量上限限制驻留规模。
- 场景四:排查缓存异常。出现“改了参数结果没变”“切换工作流后输出异常”等问题时,先用 NONE 跑一遍做对照,确认问题是否出在缓存命中逻辑,再切回原策略定位具体环节。
- 多 LoRA 场景补充:LoRA 的加载与切换会改变模型的有效状态,通常表现为相关节点输入签名变化而触发重算。此时缓存策略决定的不是“会不会重算”,而是“旧配置下的历史结果还要占多久内存”——这正是 RAM_PRESSURE 与 LRU 在多 LoRA 环境下更有价值的原因。
六、选型建议
综合来看,可以按以下条件化思路判断:
- 显存充裕、工作流固定:CLASSIC 足够,命中率最高,无需额外配置;
- 多工作流频繁切换、内存持续增长:优先 LRU,把驻留规模约束在可预期范围内;
- 容器部署、内存限额严格、模型较大:优先 RAM_PRESSURE,让淘汰直接对齐真实内存压力;
- 调试与基准测试:临时切 NONE 做对照,用完即切回;
- 通用原则:先监控、后调参。上线任何策略前,先观察缓存命中率、淘汰次数和内存水位三个指标,再决定容量或阈值的具体取值,避免“拍脑袋”配置。
七、迁移与使用注意事项
- 冷启动代价:策略切换属于启动级配置,切换后首轮执行通常全量重算,属于预期行为,不要误判为“缓存坏了”;
- 参数需要估算:LRU 的容量上限应结合单节点结果的典型大小估算,RAM_PRESSURE 的阈值应为系统与其他进程预留内存余量,避免阈值贴着限额设置;
- 区分两路缓存:objects 缓存不随策略变化,排查“节点对象被复用导致状态残留”类问题时,要与 outputs 缓存问题分开定位;
- 版本升级回归:缓存实现细节可能随版本演进调整,升级后建议对核心工作流做命中率与内存占用的回归观察;
- 监控先行:持续跟踪命中率、淘汰频率、内存水位与节点执行耗时,才能量化策略调整的实际收益。
八、总结
一句话概括四种策略的取舍:CLASSIC 保命中率、LRU 保容量上限、RAM_PRESSURE 保内存水位、NONE 保确定性。它们共享同一套输入签名键构造与级联失效语义,差异只在于“留多少、留多久、何时丢”。选型的决策主线也很清晰:资源约束优先于命中率偏好——先判断自己的内存边界和多工作流程度,再决定用哪种策略去匹配。理解了这一层,“学习节点式工作流引擎”的正确路线里,就补上了缓存这块最容易被忽略、却最能拉开使用水平差距的拼图。