标签归档:comfyUI

ComfyUI 的缓存架构和实现

围绕 ComfyUI,大家讨论最多的是节点、工作流、算力这些,真正去看缓存细节的人其实不多。但只要你开始在一台机器上堆多个模型、多个 LoRA、多个 workflow,缓存策略就会直接决定这几件事:

  • 你是算力浪费,还是把显存 / 内存用在刀刃上;
  • 容器会不会莫名其妙 OOM;
  • 工作流切换时,是“秒级热身”还是“从头再来”。

这篇文章只做一件事:把 ComfyUI 当前的缓存架构和实现讲清楚,重点是三类策略(CLASSIC / LRU / RAM_PRESSURE)在「键怎么算、什么时候命中、什么时候过期」上的差异,以及在多模型、多 LoRA、多 workflow 场景下应该怎么选择。

1. 整体架构:两路缓存 + 四种策略

先把整体结构捋清楚。

1.1 两类缓存:outputs 和 objects

在执行一个工作流的时候,ComfyUI 维护了两类缓存:

  • outputs
    存中间结果和 UI 输出。命中时可以直接跳过节点执行,这部分是真正省计算的地方。
  • objects
    存节点对象实例(类实例),避免每次重新构造节点对象。

这两类缓存由一个统一的集合类 CacheSet 来管理。核心结构在 execution.py 中:

class CacheType(Enum):
    CLASSIC = 0
    LRU = 1
    NONE = 2
    RAM_PRESSURE = 3

class CacheSet:
    def __init__(self, cache_type=None, cache_args={}):
        if cache_type == CacheType.NONE:
            self.init_null_cache()
        elif cache_type == CacheType.RAM_PRESSURE:
            cache_ram = cache_args.get("ram"16.0)
            self.init_ram_cache(cache_ram)
        elif cache_type == CacheType.LRU:
            cache_size = cache_args.get("lru"0)
            self.init_lru_cache(cache_size)
        else:
            self.init_classic_cache()
        self.all = [self.outputs, self.objects]

不管是哪种策略,结构都是:

  • outputs按输入签名做 key
  • objects按 (node_id, class_type) 做 key

1.2 四种策略:CLASSIC / LRU / RAM_PRESSURE / NONE

从启动参数到缓存策略的选择,大致是这样的优先级(在 main.py 中):

  • 指定 --cache-lru → 用 LRU;
  • 否则指定 --cache-ram → 用 RAM_PRESSURE;
  • 否则指定 --cache-none → 完全关闭缓存;
  • 都没指定 → 默认 CLASSIC。

CacheType 的定义前面已经贴了。不同策略只决定 outputs 的实现方式,而 objects 基本始终使用层级缓存 HierarchicalCache(CacheKeySetID),后面细讲。


2. 缓存键:输入签名 + 变化指纹

缓存是否命中,首先取决于“key 算得是否合理”。ComfyUI 的设计核心是:

  • 输出缓存(outputs):用“输入签名 + is_changed 指纹(+ 某些情况下的 node_id)”作为 key;
  • 对象缓存(objects):用 (node_id, class_type) 作为 key。

2.1 输出缓存:输入签名是怎么来的

输出键的生成由 CacheKeySetInputSignature 负责,核心逻辑在 comfy_execution/caching.py:100-126

async def get_node_signature(self, dynprompt, node_id):
    signature = []
    ancestors, order_mapping = self.get_ordered_ancestry(dynprompt, node_id)
    signature.append(await self.get_immediate_node_signature(dynprompt, node_id,
    order_mapping))
    for ancestor_id in ancestors:
        signature.append(await self.get_immediate_node_signature(dynprompt,
        ancestor_id, order_mapping))
    return to_hashable(signature)

这里有几个点:

  1. 不只看当前节点
    它会按拓扑顺序把“当前节点 + 所有祖先”的签名拼在一起。这保证了只要整个子图的拓扑和输入一致,输出就能命中缓存。

  2. immediate 签名包含什么
    get_immediate_node_signature 会把这些信息打包进去(位置见同文件 108–126):

    • class_type:节点类型;
    • is_changed 指纹(通过 fingerprint_inputs/IS_CHANGED);
    • 必要时的 node_id
    • 有序的输入值/链接。
  3. 什么时候把 node_id 也算进 key
    当节点被声明为非幂等,或者内部有 UNIQUE_ID 这种隐含输入时,会把 node_id 加进签名(见 comfy_execution/caching.py:18-23, 116),避免“看起来一样”的节点被错误复用。

最后通过 to_hashable 转成可哈希结构(tuple/frozenset 等),作为最终键值。

结果是:

  • 输入完全相同 → key 相同 → 直接命中;
  • 任意一个上游节点输入或参数变化 → 指纹变了 → key 不同 → 不会复用旧结果。

2.2 对象缓存:用 (node_id, class_type)

节点对象的键由 CacheKeySetID 构造(comfy_execution/caching.py:66-80),逻辑简单:

  • key = (node_id, class_type)

读取时:

obj = caches.objects.get(unique_id)
if obj is None:
    obj = class_def()
    caches.objects.set(unique_id, obj)

对象缓存存在的目的只有一个:同一个 workflow 执行过程中,不要重复 new 节点实例。


3. 缓存容器:Basic / Hierarchical / LRU / RAM / Null

有了 key,还需要一个合理的「容器」和「驱逐策略」。

主要类在 comfy_execution/caching.py

  • BasicCache:基础容器,提供 set_prompt / clean_unused / get / set 等;
  • HierarchicalCache:按“父节点 → 子图”构建层级缓存;
  • LRUCache:在 Basic + Hierarchical 的基础上增加代际 LRU;
  • RAMPressureCache:在 LRU 的基础上增加 RAM 压力驱逐;
  • NullCache:空实现(禁用缓存)。

核心接口统一在 BasicCache

def clean_unused(self):
    self._clean_cache()
    self._clean_subcaches()

层级相关逻辑在 HierarchicalCache,用来支持“子图单独分区缓存”。

3.1 层级缓存:怎么定位到某个节点的分区

HierarchicalCache 通过 parent 链定位子缓存,代码在 comfy_execution/caching.py:242-269

def _get_cache_for(self, node_id):
    parent_id = self.dynprompt.get_parent_node_id(node_id)
    ...
    for parent_id in reversed(hierarchy):
        cache = cache._get_subcache(parent_id)
        if cache is Nonereturn None
    return cache

def get(self, node_id):
    cache = self._get_cache_for(node_id)
    if cache is Nonereturn None
    return cache._get_immediate(node_id)

def set(self, node_id, value):
    cache = self._get_cache_for(node_id)
    assert cache is not None
    cache._set_immediate(node_id, value)

含义很直接:

  • 根节点存在当前 cache;
  • 某些节点生成子图,会在该节点下挂一个子 cache;
  • 读写时,先通过 parent 链找到对应的子 cache 再读写。

clean_unused() 里除了清除不用的 key,还会删除没用到的子 cache 分区(_clean_subcaches())。

4. 执行循环中的使用路径

缓存不是“挂在那就完事了”,它在执行循环中有比较明确的调用点。

4.1 设置 prompt + 清理

启动一次执行时(execution.py:681-685):

is_changed_cache = IsChangedCache(prompt_id, dynamic_prompt, self.caches.outputs)
for cache in self.caches.all:
    await cache.set_prompt(dynamic_prompt, prompt.keys(), is_changed_cache)
    cache.clean_unused()

这里做了三件事:

  1. 给当前 prompt 生成一个 IsChangedCache,为 key 计算提供 is_changed 结果;
  2. 对 outputs / objects 各自执行一次 set_prompt(不同策略实现不同);
  3. 紧接着执行 clean_unused(),做一次基于“当前 prompt 键集合”的清理。

4.2 节点执行前后:cache 命中与写入

在节点执行路径中:

  • 执行前:优先尝试从 outputs 命中(execution.py:686-701);
  • 执行后:将 (ui, outputs) 作为 CacheEntry 写入 outputsexecution.py:568-571)。

为了简化,这里只看抽象行为:

  • 命中 → 跳过计算,直接拿值;
  • 未命中 → 正常跑一遍,将结果塞回缓存。

4.3 每个节点之后的 RAM 轮询

如果是 RAM_PRESSURE 模式,执行完每个节点都会触发一次内存检查(execution.py:720):

self.caches.outputs.poll(ram_headroom=self.cache_args["ram"])

只有 RAMPressureCache 实现了 poll,其他模式下这个调用等同空操作。


5. CLASSIC:默认层级缓存

先看默认策略:CLASSIC。

5.1 初始化与结构

CacheSet.init_classic_cacheexecution.py:97-126):

class CacheType(Enum):
    CLASSIC = 0
    LRU = 1
    NONE = 2
    RAM_PRESSURE = 3

class CacheSet:
    def init_classic_cache(self):
        self.outputs = HierarchicalCache(CacheKeySetInputSignature)
        self.objects = HierarchicalCache(CacheKeySetID)

可以看到:

  • outputs 用层级缓存 + 输入签名做 key;
  • objects 也用层级缓存 + (node_id, class_type) 做 key;
  • 不涉及 LRU 或 RAM 驱逐。

5.2 CLASSIC 的过期机制:完全由「当前 prompt」驱动

CLASSIC 模式不做容量和时间管理,它只有两种“失效”方式:

  1. 提示切换 / 执行前清理

clean_unused() 的核心逻辑(comfy_execution/caching.py:172-195):

def clean_unused(self):
    self._clean_cache()
    self._clean_subcaches()
  • _clean_cache():把不在“当前 prompt 键集合”的项删掉;
  • _clean_subcaches():把不再需要的子缓存分区删掉。

execution.py:681-685 每次绑定新 prompt 时,都会执行这一步。结果是:

  • 换了一个新的 workflow / prompt,旧 workflow 的 outputs / objects 都会被视为“未使用”,被清理掉;
  • CLASSIC 不会跨不同 prompt 保留旧 workflow 的缓存。
  1. 键不命中(指纹失效)

is_changed 的计算在 execution.py:48-89,当节点输入更新时,指纹会变化;CacheKeySetInputSignature 在构造键时会把这个指纹带进去(comfy_execution/caching.py:115-127)。因此只要:

  • 参数 / 输入 / 上游节点的任一变化 → key 改变 → 旧值自然不命中。

5.3 CLASSIC 明确不会做的事

在 CLASSIC 下:

  • 不做 LRU 容量控制:没有 max_size,也没有代际淘汰逻辑;
  • 不做 RAM 压力驱逐:poll() 是空的,执行循环里即使调用了也什么都不干;
  • 不做 TTL:不看时间,只看 prompt 键集合。

对应的注释已经在参考内容中点得很清楚,这里就不重复堆代码了。

5.4 在频繁切换 workflow / 模型时的表现

结合上面的机制,总结一下 CLASSIC 在多 workflow 场景下的行为:

  • 执行新工作流时:
    set_prompt + clean_unused 会直接把“不在新 prompt 键集合里”的缓存项(包括对象子缓存)全部清掉;
  • 模型 / LoRA 变化:
    即便节点 ID 不变,输入签名和 is_changed 指纹不同,也会生成新键;旧条目先不命中,随后在下一次 prompt 绑定时被清空;
  • 回切旧 workflow:
    因为在上一次切换时已经把旧 workflow 相关缓存清干净了,所以基本等于重新计算。

适用场景

  • workflow 比较固定;
  • 主要想在“一次执行当中”复用中间结果,不在意跨 prompt 的持久化。

6. LRU:代际 LRU 控制 outputs 尺寸

第二种策略是 LRU,主要解决的问题是:在允许跨 prompt 复用输出的前提下,限制缓存总量,避免无限膨胀

6.1 初始化:只作用于 outputs

CacheSet.init_lru_cacheexecution.py:127-135):

def init_lru_cache(self, cache_size):
    self.outputs = LRUCache(CacheKeySetInputSignature, max_size=cache_size)
    self.objects = HierarchicalCache(CacheKeySetID)

注意几点:

  • LRU 只作用于 outputs
  • objects 仍然用 HierarchicalCache,不受 LRU 驱逐;
  • 启动方式:--cache-lru N,且 N > 0

6.2 LRU 的代际设计

LRUCache 的骨架逻辑在 comfy_execution/caching.py:299-337

class LRUCache(BasicCache):
    def __init__(self, key_class, max_size=100):
        self.max_size = max_size
        self.min_generation = 0
        self.generation = 0
        self.used_generation = {}
        self.children = {}

    async def set_prompt(...):
        self.generation += 1
        for node_id in node_ids:
            self._mark_used(node_id)

    def get(self, node_id):
        self._mark_used(node_id)
        return self._get_immediate(node_id)

    def _mark_used(self, node_id):
        cache_key = self.cache_key_set.get_data_key(node_id)
        if cache_key is not None:
            self.used_generation[cache_key] = self.generation

含义:

  • 有一个全局代数 generation,每次绑定新 prompt generation += 1
  • 每次:

    • 绑定 prompt 时,会把该 prompt 内所有节点标记为“在当前代被使用”;
    • get / set 时更新条目的 used_generation[key] 为当前代。

6.3 容量驱逐:按「最老代」逐步清理

clean_unused() 的一部分逻辑在 comfy_execution/caching.py:314-323

def clean_unused(self):
    while len(self.cache) > self.max_size and self.min_generation < self.
    generation:
        self.min_generation += 1
        to_remove = [key for key in self.cache if self.used_generation[key] < self.
        min_generation]
        for key in to_remove:
            del self.cache[key]
            del self.used_generation[key]
            if key in self.children:
                del self.children[key]
    self._clean_subcaches()

简单归纳一下:

  • 只要 len(cache) > max_size,就逐步提升 min_generation
  • 每提升一代,就删除“最近使用代 < min_generation”的条目;
  • 同步清除掉和这些 key 绑定的子缓存引用;
  • 清理完再执行 _clean_subcaches() 做层级清扫。

6.4 子图分区与代际配合

子缓存创建时会显式标记父节点和子节点“被使用”,避免刚刚生成的子图被误删。代码在 comfy_execution/caching.py:338-349

async def ensure_subcache_for(self, node_id, children_ids):
    await super()._ensure_subcache(node_id, children_ids)
    await self.cache_key_set.add_keys(children_ids)
    self._mark_used(node_id)
    cache_key = self.cache_key_set.get_data_key(node_id)
    self.children[cache_key] = []
    for child_id in children_ids:
        self._mark_used(child_id)
        self.children[cache_key].append(self.cache_key_set.get_data_key(child_id))
    return self

配合前面提到的层级结构,就形成了“按 workflow 子图分区 + LRU 按代际清理”的整体行为。

6.5 触发时机和行为总结

在 LRU 模式下:

  • 每次绑定 prompt:
    generation += 1,标记当前 prompt 的节点使用代为当前代;
  • 每次 get/set
    更新条目的 used_generation 为当前代;
  • 每次 clean_unused()

    • len(cache) > max_size 时,通过提升 min_generation 清除旧代条目;
    • 额外清理无用子缓存。

特点

  • 可以跨 prompt 保留一部分中间结果;
  • max_size 控制缓存上限;
  • 没有 RAM 压力感知:poll() 依然不做事。

适用场景

  • 希望在多 workflow 之间部分复用缓存;
  • 但机器内存有限,需要给 outputs 一个明确的容量上限;
  • 对 RAM 细粒度控制没有强需求,或使用的是物理机 / 内存足够的环境。

7. RAM_PRESSURE:按可用内存压力驱逐

第三种策略是 RAM_PRESSURE,对应类是 RAMPressureCache。它继承自 LRUCache,但不按 max_size 做驱逐,而是:

  • 通过 poll(ram_headroom),在可用内存不足时按“OOM 评分”驱逐条目。

7.1 初始化:objects 仍然是层级缓存

CacheSet.init_ram_cacheexecution.py:131-133):

def init_ram_cache(self, min_headroom):
    self.outputs = RAMPressureCache(CacheKeySetInputSignature)
    self.objects = HierarchicalCache(CacheKeySetID)

注意两个点:

  • RAM 模式下,只有 outputs 会按 RAM 压力驱逐
  • objects 不参与 RAM 驱逐,逻辑完全和 CLASSIC/LRU 下相同。

7.2 poll:可用 RAM 检测 + OOM 评分驱逐

poll 的主逻辑在 comfy_execution/caching.py:384-454

def poll(self, ram_headroom):
    def _ram_gb():
        # 优先 cgroup v2/v1,失败回退 psutil
        ...
    if _ram_gb() > ram_headroom: return
    gc.collect()
    if _ram_gb() > ram_headroom: return
    clean_list = []
    for key, (outputs, _), in self.cache.items():
        oom_score = RAM_CACHE_OLD_WORKFLOW_OOM_MULTIPLIER ** (self.generation -
        self.used_generation[key])
        ram_usage = RAM_CACHE_DEFAULT_RAM_USAGE
        def scan_list_for_ram_usage(outputs):
            nonlocal ram_usage
            ...
        scan_list_for_ram_usage(outputs)
        oom_score *= ram_usage
        bisect.insort(clean_list, (oom_score, self.timestamps[key], key))
    while _ram_gb() < ram_headroom * RAM_CACHE_HYSTERESIS and clean_list:
        _, _, key = clean_list.pop()
        del self.cache[key]
        gc.collect()

流程拆一下:

  1. 获取可用 RAM

    _ram_gb() 的实现优先读取 cgroup 的限制:

    • cgroup v2:memory.max / memory.current
    • cgroup v1:memory.limit_in_bytes / memory.usage_in_bytes
    • 都失败才回退 psutil.virtual_memory().available

    这解决了容器环境下“宿主机内存大,容器实际被限制”的常见问题。

  2. 阈值和 GC

    • 如果可用 RAM > ram_headroom,直接返回;
    • 否则先跑一次 gc.collect()
    • 再测一次 RAM,如果还是不足,进入驱逐流程。
  3. 为每个条目计算 OOM 评分

    • 初始 oom_score = RAM_CACHE_OLD_WORKFLOW_OOM_MULTIPLIER ** (generation - used_generation[key])

      • 大概意思是:越久没被用,分数指数级放大(默认倍数为 1.3,见 comfy_execution/caching.py:365);
    • 初始 ram_usage = RAM_CACHE_DEFAULT_RAM_USAGE(0.1,见 360);
    • 递归遍历 outputs 列表:

      • CPU tensor:numel * element_size * 0.5(认为 CPU 上的 tensor 价值更高,折半);
      • 自定义对象:如果实现了 get_ram_usage() 就加上它;
    • 最后 oom_score *= ram_usage,得到综合评分。

    所有条目按 (oom_score, timestamp, key) 排序,放入 clean_list

  4. 按迟滞阈值逐个删除

    • 只要 _ram_gb() < ram_headroom * RAM_CACHE_HYSTERESIS,就从 clean_list 末尾 pop 一个 key 并删除;
    • 每删一个都跑一次 gc.collect()
    • 迟滞倍数 RAM_CACHE_HYSTERESIS 默认 1.1,避免“刚删完又马上触发清理”的抖动。
  5. 访问时间戳

    访问时会更新 timestamps(comfy_execution/caching.py:376-382):

   def set(self, node_id, value):
       self.timestamps[self.cache_key_set.get_data_key(node_id)] = time.time()
       super().set(node_id, value)

   def get(self, node_id):
       self.timestamps[self.cache_key_set.get_data_key(node_id)] = time.time()
       return super().get(node_id)

在 oom_score 一样时,timestamp 起到“最近访问优先保留”的作用。

7.3 提示绑定下的行为:不清理 outputs

RAM 模式下,clean_unused() 的行为与 CLASSIC 不同(见参考说明):

  • RAM 模式:
    clean_unused() 只做子缓存分区清理,不会删掉“当前 prompt 未使用”的 outputs 条目;
  • CLASSIC 模式:
    clean_unused() 会同时删掉当前 prompt 未用到的 outputs 条目。

结果是:

  • RAM 模式可以跨多个 workflow 长时间保留中间结果;
  • 只有在 RAM 不够时才做清退。

7.4 容器环境下需要注意的点

的 AutoDL 中,用 psutil.virtual_memory().available,在容器里看到的是宿主机内存,而不是容器的限额,导致永远“不触发回收”,最后 OOM。

适用场景

  • 多 workflow / 多模型 / 多 LoRA 同时存在,且希望尽可能长时间复用输出结果;
  • 机器内存有限,但更关心“不 OOM”,而不是一个固定的 max_size
  • 特别适合容器环境(K8s / AutoDL 等),配合 --cache-ram <GB>

8. 一些落地建议

最后,用一段比较直接的建议收尾。

8.1 单模型 / workflow 稳定:用 CLASSIC 即可

特点:

  • 工作流基本不换;
  • 主要希望避免一次执行中的重复计算(比如多次 preview)。

用默认 CLASSIC:

  • 结构简单;
  • 不参与复杂的跨 prompt 保留;
  • 不需要担心 LRU 尺寸和 RAM 阈值调参。

8.2 多 workflow + 控制缓存尺寸:用 LRU

场景:

  • 有多套 workflow,在它们之间来回切;
  • 机器内存不是特别大,希望 outputs 不要无限膨胀;
  • 又希望某些常用子图能被复用。

做法:

  • 启动时加 --cache-lru N(N 先给一个相对保守的值,比如几百到几千条,看内存曲线再调);
  • LRUCache 用代际 + max_size 帮你自动做“近期常用保留、早期冷门清理”。

8.3 多模型 / 多 LoRA / 容器环境:优先 RAM_PRESSURE

场景:

  • 容器 / 云平台(K8s、AutoDL 等);
  • 有较多模型、LoRA 和 workflow 混用;
  • 内存被容器限制,容易因爆 RAM 掉进 OOM。

做法:

  • 启动时用 --cache-ram <GB> 配一个“希望保留的 RAM 余量”;

    • 比如容器给了 32GB,就设在 16–24GB 看情况;
  • RAMPressureCache

    • 最大程度保留各个 workflow 的中间结果(包括应用 LoRA 后的模型对象等);
    • 在内存不足时,根据 OOM 评分优先清旧代、大内存条目。

注意一点:即使有容器优化,如果平台本身的 cgroup 挂得不标准,_ram_gb() 的结果还是有可能偏离实际,这一点要结合平台文档确认。

8.4 完全不想折腾:直接关缓存

如果你:

  • 环境受限;
  • 或者调试阶段不想被缓存影响行为理解;

可以直接用:

  • --cache-none
    对应 CacheType.NONECacheSet.init_null_cache()NullCache,所有 get/set 都是 no-op。

代价就是:每次执行都完全重新算一遍。

9. 小结一下

把上面的内容压缩成几句话:

  • ComfyUI 有两路缓存:

    • outputs 存中间结果,真正用来省算力;
    • objects 存节点实例,只减少 Python 对象构造开销。
  • 键体系:

    • 输出:输入签名 + is_changed 指纹(+ 条件下的 node_id)
    • 对象:**(node_id, class_type)**。
  • 四种策略:

    • CLASSIC:默认层级缓存,按当前 prompt 键集合清理,不做 LRU / RAM 驱逐;
    • LRU:只对 outputs 做代际 LRU,配合 max_size 控制容量;
    • RAM_PRESSURE:在 LRU 基础上加 RAM 压力驱逐,在内存不足时按 OOM 评分清理;
    • NONE:彻底关掉缓存。
  • 在多模型 / 多 workflow / 多 LoRA 场景下:

    • 对象缓存 objects 始终是层级缓存,不参与 LRU / RAM 驱逐,在每次绑定 prompt 时按键集合清理;
    • 模型权重 / LoRA 的真实驻留由模型管理层控制;
    • 真正要关心的是:如何选择 outputs 的策略,让中间结果既能有效复用,又不会把内存打爆。

如果我们在一个复杂的工作流环境里跑 ComfyUI,建议先搞清楚自己处于哪种场景,再结合上面的策略选项,把缓存调成我们能控制的状态,而不是让它在后台「自动长草」。

以上。

在 AWS 的 EC2 上搭建换装模型 ComfyUI OOTDiffusion

项目地址:https://github.com/AuroBit/ComfyUI-OOTDiffusion

在 AWS 上购买 g4dn.xlarge ,带有一个 gpu  nvidia 显卡

初始化机器,如果没有选带 nvidia 驱动的操作系统,需要自己安装 nvidia 的显卡

sudo apt-get update #更新软件列表
sudo apt-get install g++
sudo apt-get install gcc
sudo apt-get install make ubuntu-drivers
sudo vim /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
options nouveau modeset=0
sudo update-initramfs -u #更新系统
sudo reboot # 重启
lsmod | grep nouveau
sudo apt-get remove –purge nvidia*
ubuntu-drivers devices
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt-get update
sudo apt-get install nvidia-driver-535 #此处数字要对应上面查询到的版本号
sudo apt-get install mesa-common-dev
sudo reboot # 重启
nvidia-smi

Clone 项目并安装相关依赖:

conda create -n ootd python=3.10
conda activate ootd

conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia

# Install nvcc compiler for torch cpp extensions
conda install cuda-nvcc -c nvidia

#  Clone ComfyUI
git clone https://github.com/comfyanonymous/ComfyUI.git
# Clone to custom_nodes
git clone https://github.com/AuroBit/ComfyUI-OOTDiffusion.git custom_nodes/ComfyUI-OOTDiffusion

# Install dependencies
pip install -r custom_nodes/ComfyUI-OOTDiffusion/requirements.txt

如果想外网访问,在启动的时候带上 –listen,如: python main.py –listen 0.0.0.0

AI 大时代要懂的 2 种「编程范式」

2023 年 AI 新突破导出不穷,隔两个月就会有一个爆点出来,AI 迎来了一个新的里程碑时刻。从 OpenAI 推出的 ChatGPT 到 Midjourney 发布的 V5/V6 版本,AI 在各个领域都取得了突破性的进展。随着 AI 技术的日益成熟,我们也开始思考如何更好地利用 AI 工具来提高工作效率。

到 2024 年, AI 应用、AI 配套的框架和工具如雨后春笋。

在这个 AI 大时代,有两种「编程范式」值得我们关注。为什么要打上引号呢?

因为这里所说的编程,已经不再局限于传统意义上的写代码,而是泛指利用各种工具和模型,将它们灵活组合,创造出新的应用和价值。

第一种范式是基于 ComfyUI 的编程。ComfyUI 是一个模块化的 AI 图像生成工具,它允许用户通过拖拽和连接不同的节点,轻松创建出令人惊艳的 AI 艺术作品。在 ComfyUI 上编程,你无需掌握复杂的代码知识,只需要理解每个模块的功能,并将它们以合理的方式组合在一起,就能得到理想的结果。这种直观、易用的特点,使得 ComfyUI 成为了平面设计、插画创作等领域的得力助手。

第二种范式则是基于 LangChain 的编程。与 ComfyUI 注重视觉创作不同,LangChain 的重点在于自然语言处理。通过 LangChain,开发者可以将语言模型与外部数据源相结合,快速构建功能丰富的聊天机器人、智能文档助手等应用。LangChain 提供了一系列现成的模块和接口,使得即使是非 NLP 专业的程序员,也能在短时间内上手并应用到实际项目中。从客户服务到数据分析,LangChain 正在各行各业发挥着重要作用。

相比传统的编程范式,AI 编程更加注重模块化和灵活组合。我们无需从零开始构建一个完整的系统,而是站在巨人的肩膀上,将现有的模型和工具进行拼装和优化。这种方式不仅大大降低了开发门槛,也极大地提高了开发效率。当然,AI 编程范式也并非完美无缺。对于一些需要高度定制化的场景,传统的代码编程仍然不可或缺。此外,AI 工具的使用也对开发者提出了更高的要求,需要我们对各种模型和算法有更全面的理解和把控。

接下来,简单聊一聊这两种「编程范式」,

comfyUI

ComfyUI 是一个基于 Stable Diffusion 的开源 AI 绘图工具,采用了模块化的节点式工作流设计。它通过将 Stable Diffusion 的各个组件和处理步骤抽象为独立的节点,使得用户可以通过直观的拖拽、连接操作来构建复杂的图像生成流程。

ComfyUI 解决了传统 AI 绘图工具易用性差、扩展性低的问题。其模块化设计和直观的 Web 界面大大降低了用户的使用门槛,无需深入了解底层技术细节,即可快速构建和调整工作流。同时,ComfyUI 还提供了强大的自定义节点机制,允许开发者轻松扩展新的功能和模型,使其能够适应不断发展的AI绘图领域。

ComfyUI 最初由开发者 Comfyanonymous 在 2022 年末发起,旨在提供一个简单、直观的 Stable Diffusion Web UI。早期版本实现了基本的节点类型和 Web 界面,展示了其模块化设计的优势,吸引了一批 AI 绘图爱好者的关注。

在 2023 年春夏,ComfyUI 进入了快速发展阶段。项目不断增加新的节点类型,如 ControlNet、Inpaint、Upscale等,支持更多的图像控制和后处理功能。同时,ComfyUI 引入了自定义节点机制,大大扩展了其功能和适用范围。项目也集成了更多 Stable Diffusion 衍生模型,为用户提供了更多选择。

随着用户社区的不断壮大,ComfyUI 的生态也日益丰富。社区成员积极贡献工作流、节点脚本、训练模型等资源,推动项目的发展。ComfyUI 举办了一系列社区活动,促进了用户间的交流和创作。项目代码库也迎来了更多贡献者,社区力量成为 ComfyUI 发展的重要推动力。

2023 年冬至今,ComfyUI 开始着眼于生态融合和应用拓展。项目与其他 AI 绘图工具建立了联系,支持工作流的导入导出和 API 集成。ComfyUI 也开始探索更多应用场景,如虚拟主播、游戏 mod 等,拓宽了 AI绘图的应用范围。越来越多的开发者和公司开始关注和使用 ComfyUI,其发展前景备受看好。未来,ComfyUI 将继续完善节点系统,引入更先进的 AI 技术,并加强生态建设,有望成为 AI 绘图领域的重要基础设施。

ComfyUI 中节点之间的关联是通过连接节点的输入和输出端口来实现的。每个节点都有预定义的输入和输出端口,用户可以在 UI 界面上将一个节点的输出端口连接到另一个节点的输入端口,从而建立节点之间的数据流和执行顺序。

在 ComfyUI 的后端实现中,这种节点关联是通过一个有向无环图来表示的。DAG 是一种常用的数据结构,用于描述一组节点之间的依赖关系和执行顺序。

当用户在 UI 界面上连接两个节点时,实际上是在 DAG 中添加一条边,表示数据从源节点流向目标节点。ComfyUI 会根据 DAG 的拓扑结构,确定节点的执行顺序,并在运行时将数据在节点之间传递。

ComfyUI 中节点关联有一些关键实现细节:

  1. 端口类型匹配:每个节点的输入和输出端口都有预定义的数据类型。在连接节点时,只有类型匹配的端口才能建立连接
  2. 数据传递:当一个节点执行完毕后,它会将结果数据发送到所有连接到其输出端口的节点的输入端口。
  3. 执行调度:ComfyUI 会根据 DAG 的拓扑顺序,确定节点的执行顺序。当一个节点的所有输入数据都准备好时,该节点就可以开始执行。
  4. 并行执行:无依赖关系的节点可以并行执行,提高执行效率。ComfyUI 会自动分析 DAG,找出可以并行执行的节点。
  5. 缓存优化:对于某些计算量大的节点,ComfyUI 会缓存其计算结果,避免重复计算。当节点的输入数据没有变化时,就可以直接使用缓存的结果。

ComfyUI 通过将节点组织成 DAG 的方式,实现了节点之间的关联和数据流控制。这种设计使得用户能够以可视化的方式创建复杂的图像处理工作流,同时也为并行优化和缓存优化提供了便利。

comfyUI 中核心工作都是围绕其节点,其主要节点如下。

  1. Text Prompt(文本提示)节点:提供文本描述,指导图像生成,输入是用户输入的文本提示如”1girl, brown hair, smile”;输出是编码后的文本向量(tokens)。几乎所有的绘图工作流都需要文本提示节点,它是指定图像内容的主要方式。
  2. Latent Image(潜在图像)节点:表示潜在空间中的图像,可以是随机初始化的噪音,也可以来自其他节点的输出。输入是噪音参数(如seed、尺寸等),或其他节点传递的潜在图像。输出是潜在空间中的图像表示。作为采样起点(初始噪音)或中间结果(如图像修补、图生图等)。
  3. Sampler(采样器)节点:根据条件迭代优化潜在图像,使其解码后符合要求。输入是潜在图像、文本向量、其他条件(如 ControlNet 输出等)、采样步数、采样方法等参数。输出是优化后的潜在图像。采样是图像生成的核心,不同的采样器节点可以权衡生成质量和多样性。
  4. ControlNet 节点:根据附加条件(如边缘、姿态、深度等)控制生成图像。输入是潜在图像、条件图(如 Canny 边缘图)、ControlNet 模型参数等。输出是融合条件控制的潜在图像。用于生成满足特定结构、布局或属性要求的图像,如人像、动漫线稿上色等。
  5. VAE Encode(VAE编码)节点:将 RGB 图像编码为潜在空间表示。输入是 RGB 图像,如用户上传的图片。输出是潜在空间中的图像表示。用于图生图、图像修补、图像融合等需要以图像为起点的任务。
  6. VAE Decode(VAE解码)节点:将潜在空间表示解码为 RGB 图像。输入是潜在空间中的图像表示,通常来自采样器节点。输出是 RGB 图像。用于生成最终可见的图像结果。
  7. Upscale(放大)节点:增加图像分辨率,提高细节。输入是 RGB 图像,放大方法和倍数等参数。输出是放大后的 RGB 图像。用于生成高分辨率图像,常在VAE解码后使用。
  8. Inpaint(图像修补)节点:根据 mask 和提示,对图像的指定区域进行编辑。输入是原始图像、mask 图像、修改区域的文本提示等。输出是修改后的图像。用于对生成图像进行局部编辑,如去除伪影、修改细节等。

除了以上常用节点,comfyUI 还有许多其他节点,如图像保存、剪裁、格式转换等,用于图像的后处理和输出。不同节点可以灵活组合,构建多种多样的绘图工作流,满足各类需求。

举个例子,一个常见的工作流是:文本提示节点 -> 潜在图像节点(初始噪音) -> ControlNet节点(添加结构条件) -> 采样器节点(优化潜在图像) -> VAE解码节点(生成RGB图像) -> 放大节点(提高分辨率) -> 图像保存节点(输出最终结果)。

如下图所示:

+-------------------+     +------------------+     +------------------+
|                   |     |                  |     |                  |
|  Load Model Node  |---->|  Text Encode Node|---->|  Latent Image Node |
|                   |     |                  |     |                  |
+-------------------+     +------------------+     +-------------------+
                                                             |
                                                             |
                                                             v
                                                   +-------------------+
                                                   |                   |
                                                   |  ControlNet Node  |
                                                   |                   |
                                                   +-------------------+
                                                             |
                                                             |
                                                             v
+------------------+     +------------------+     +------------------+
|                  |     |                  |     |                  |
|  Upscale Node    |<----|  VAE Decode Node |<----|  Sampler Node    |
|                  |     |                  |     |                  |
+------------------+     +------------------+     +------------------+
         |
         |
         v
+------------------+
|                  |
|  Output Image    |
|                  |
+------------------+

通过对节点的连接配置和参数调整,用户可以精细控制每个步骤,实现理想的 AI 绘图效果。同时,comfyUI 也鼓励用户开发和分享自定义节点,不断扩展其功能和应用领域。

在理解 ComfyUI 的原理时,其核心还是理解 SD 的原理,简单来讲是通过大模型、CLIP 和 VAE 编码器以及采样器的协同工作,将文本提示转换为特征马赛克,再通过 VAE 解码器还原成图像,从而实现基于文本描述生成图像的功能。

LangChain

什么是 LangChain?

LangChain 是一个开源的 Python 框架,旨在帮助开发者更容易地构建基于大语言模型(LLM)的应用。它提供了一系列工具和组件,可以方便地与各种 LLM 模型集成,如OpenAI GPT、Anthropic Claude、Google PaLM等,而无需从头开始构建或进行大量的微调。

LangChain 旨在简化和统一语言模型与外部数据和应用程序的集成过程。它为开发者提供了一套灵活的工具和组件,可以轻松地将 OpenAI、Hugging Face 等流行的语言模型与知识库、API 等数据源相结合,从而快速构建功能强大的自然语言处理应用,如聊天机器人、智能文档助手、问答系统等。

LangChain 解决了语言模型应用开发中的诸多痛点。在 LangChain 出现之前,开发者需要编写大量的胶水代码来处理不同模型和数据源之间的交互,这不仅耗时耗力,也容易引入错误。LangChain 通过提供一致的接口和预构建的组件,大大简化了这一过程。它还引入了 Prompt Engineering 的理念,允许开发者通过设计优化的提示模板来引导模型生成更准确、更符合需求的输出。

LangChain 的发展历程可以追溯到 2021 年底。最初,它只是一个简单的概念验证项目,旨在探索如何将语言模型与外部数据集成。随着 ChatGPT 等大语言模型的出现和 NLP 技术的快速发展,LangChain 的潜力开始受到关注。

2022 年,LangChain 迎来了重大更新和扩展。它引入了更多的集成选项,支持了更多种类的数据源和下游应用。同时,LangChain 的社区也在不断壮大,越来越多的开发者开始贡献代码和分享经验。

2023 年,伴随着 AI 的大爆发,LangChain 迎来了爆发式增长。它成为了开发 AI 应用的必备工具之一,在各大技术论坛和社交平台上频频被提及。LangChain 也加速了版本迭代和功能更新,引入了更多高级特性,如 Agent 和 Memory,进一步增强了其适用性和性能。

如今,LangChain 已经发展成为一个成熟而强大的 NLP 应用开发框架。它不仅帮助开发者大幅提高了开发效率,也为各行各业带来了前所未有的智能化应用。展望未来,LangChain 还将持续演进,与最新的 AI 模型和技术保持同步,为开发者和用户带来更多惊喜。

LangChain 的核心是将 LLM 与外部数据源连接,并通过 prompt engineering 技术来优化 LLM 的输入输出,从而生成更加准确、相关的结果。它的主要组件和功能如下:

  1. 模型输入输出(Model I/O):对接各种 LLM 模型的 API,提供统一的接口。支持 OpenAI、Anthropic、Hugging Face 等主流 LLM 服务商。
    • 语言模型(Language Models):支持大型语言模型(LLM)和聊天模型(ChatModel)的交互接口。
    • 提示模板(Prompt Templates):用于生成模型输入的预定义配方,支持两种主要类型是:PromptTemplate:生成字符串提示 和 ChatPromptTemplate:生成聊天消息列表提示。
    • 示例选择器(Example Selectors):提供训练、调优、测试和控制模型行为的示例输入输出。
    • 输出解析器(Output Parsers):用于将模型返回的文本结果格式化为目标对象、JSON 或数组等。
  2. 数据连接(Data Connection):在许多 LLM 应用程序中,用户特定的数据不在模型的训练集中,这可能是通过检索增强生成(RAG)实现的。RAG 的主要方法是检索外部数据,并在生成步骤中传递给 LLM。这样,LLM 就可以使用外部数据来增强生成的结果,从而提高应用程序的性能和准确性。
    • 文档加载器(Document Loaders):将不同数据源的非结构化文本加载为文档对象,并支持延迟加载。
    • 文档转换器(Document Transformers):对加载的文档进行处理,包括文本拆分、冗余过滤、元数据提取等。
    • 文本嵌入模型(Text Embedding Models):将文本转换为向量表示,用于文本检索、信息推荐、知识挖掘等。
    • 矢量存储(Vector Stores):负责存储嵌入数据并执行矢量搜索。
    • 检索器(Retrievers):从大规模文本库中检索与查询相关的文本段落,提供问答系统的额外上下文支持。
  3. 链(Chains):组件化的方式将一系列操作连接在一起形成数据处理的工作流,如数据检索、内容生成、翻译等可复用的任务执行流程。常见的链包括 LLMChain、SequentialChain、RouterChain 等。
    • 基础链(LLMChain):围绕语言模型的简单链。由提示模板和语言模型组成,用于格式化提示并返回 LLM 输出。
    • 路由链(RouterChain):可以动态选择下一条链,包括 LLMRouterChain 和 EmbeddingRouterChain。
    • 顺序链(SequentialChain):将多个链顺序连接,支持 SimpleSequentialChain 和更通用的 SequentialChain。
    • 转换链(TransformChain):在链之间添加自定义转换函数。
    • 文档链(DocumentsChain):处理多个文档输入。
  4. 记忆(Memory):为 Chains 和 Agents 提供对话状态记忆能力,用于在链之间存储和传递信息,实现上下文感知。常见的包括 ConversationBufferMemory、ChatMessageHistory 等。
  5. 代理(Agents):一种特殊的 Chain,可根据目标进行工具选择、动作规划和迭代求精。使用 LLM 作为大脑自动思考和决策,执行动作完成任务。包括 ZeroShotAgent、ReAct、Self Ask With Search 等。
  6. 回调(Callbacks):连接到 LLM 申请的各个阶段,用于日志记录、监控等。

LangChain 主要解决了以下问题:

  • 简化了与不同语言模型的交互。
  • 提供了标准化的方法来生成和管理提示。
  • 允许用户为模型提供示例输入输出,以优化模型性能。
  • 支持非结构化文本的加载、转换和处理。
  • 使文本嵌入和检索更加方便,支持向量空间中的各种运算。
  • 通过链的概念,允许将多个组件组合成复杂的工作流程。
  • 实现了对话的上下文感知能力。
  • 通过代理自动执行任务,提高了应用的智能化水平。

那如何使用呢?

使用 LangChain 的一般步骤可能包括:

  1. 根据需求选择合适的组件,如语言模型、提示模板等。
  2. 使用文档加载器加载数据,并利用文档转换器进行预处理。
  3. 配置链,将不同的组件组合起来,创建工作流程。
  4. 利用记忆组件在链之间传递上下文信息。
  5. 定义代理,使用 LLM 进行自动决策和执行。
  6. 设置回调,进行日志记录和监控。
  7. 根据具体应用场景进行调整和优化。

在使用过程中有一些注意的事项:

  1. 根据任务选择合适的 LLM:不同的 LLM 适用不同任务,并有不同的使用成本,需要根据实际情况权衡。在使用 LangChain 前,需要明确了解自己的需求和应用场景,选择和配置合适的组件和模型。
  2. 注意提示工程:LLM 的效果很大程度取决于提示的设计,需要遵循最佳实践,多进行实验和迭代。
  3. 数据的隐私和安全:在涉及用户数据时,注意数据的安全性和隐私保护,遵循相关法律法规和最佳实践。特别是在 fine-tuning 或者数据索引时,要注意数据的隐私和版权问题。
  4. 模型的公平性和伦理性:LLM 可能会放大数据中的偏见,产生有害或者不道德的内容,需要谨慎使用。
  5. 应用的可解释性:端到端的 LLM pipeline 往往是个黑盒,要考虑如何向用户解释其工作原理和局限性。
  6. 成本和效率优化:要密切关注 LLM 调用次数、向量检索等环节的耗时和费用,权衡精度和成本。对于大规模数据处理或高吞吐量的应用,需关注性能优化,可能需要并行处理、缓存机制、硬件加速等手段。

通过合理的使用和配置,LangChain 可以极大地简化复杂 AI 应用的开发流程,提高开发效率和应用性能。LangChain 的设计使得开发者可以灵活地构建和定制 AI 应用程序,以适应不同的业务需求。

LangChain 适用于构建各种 LLM 驱动的应用,比如智能对话助手、知识库问答、数据分析、文案创作等。LangChain 正在成为 LLM 应用开发领域的生产力工具,促进更多创新产品和服务的诞生。

小结

AI 编程范式正在为我们开启一个全新的创作空间。ComfyUI 让设计变得前所未有的简单,LangChain 则让智能对话唾手可得。站在时代的十字路口,拥抱 AI,学习新的编程范式,我们就能更从容地迎接未来的挑战与机遇。

但,纸上得来终觉浅,绝知此事要躬行。

在实际的工作中,这些要用起来,才能有更深刻的体会。

以上。