深入源码解析 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,其发展前景备受看好。

前两周,Comfy 推出跨平台的 ComfyUI 安装包,现在我们可以一键安装 ComfyUI 了,这次更新不仅带来了全新的桌面版应用,还对用户界面进行了全面升级,并新增了自定义按键绑定、自动资源导入等一系列实用功能,让工作流程更加流畅。

今天我们深入到 ComyUI 的源码去看一下其实现原理和过程。

ComfyUI 执行的大概流程如下:

用户界面 -> 工作流定义 -> PromptQueue
   ↓
PromptExecutor 开始执行
   ↓
验证输入 (validate_prompt)
   ↓
构建执行图
   ↓
按顺序执行节点
   ↓
缓存结果
   ↓
返回结果到界面

1. ComfyUI 的初始化与执行流程详解

ComfyUI 的一个明显的优点是有着灵活的图形用户界面,可以用于处理复杂的图像生成和处理工作流。

它具有精良的架构设计,通过模块化设计、缓存优化、资源管理以及错误处理机制,确保了系统的高效性和可靠性。

1.1 系统初始化流程

ComfyUI 的启动过程分为几个主要阶段:预启动脚本的执行、节点系统的初始化、服务器的启动与 WebSocket 的连接。

1. 启动前准备

在系统启动前,ComfyUI 会首先执行预启动脚本,确保自定义节点的环境准备就绪。这一过程允许在加载节点之前执行一些必要的自定义操作。

def execute_prestartup_script():
    # 执行自定义节点的预启动脚本
    for custom_node_path in node_paths:
        if os.path.exists(script_path):
            execute_script(script_path)

2. 节点系统初始化

节点系统是 ComfyUI 的核心组件。此阶段会加载内置节点以及用户自定义的节点,并注册到系统中供后续使用。

def init_extra_nodes():
    # 加载内置节点
    import_failed = init_builtin_extra_nodes()
    # 加载自定义节点
    init_external_custom_nodes()
  • 内置节点: 位于 comfy_extras 目录下,定义了基本的图像处理功能。
  • 自定义节点: 用户可以通过 custom_nodes 目录添加自定义节点,扩展系统的功能。

3. 服务器初始化

服务器初始化是启动 ComfyUI 的 Web 服务器的过程。它包括 WebSocket 的初始化,允许前端和后端实时通信。此外,执行队列也会在此阶段创建,用于管理节点的执行顺序和任务调度。

class PromptServer:
    def __init__(self, loop):
        # 初始化 Web 服务器
        self.app = web.Application()
        # 初始化 WebSocket
        self.sockets = dict()
        # 初始化执行队列
        self.prompt_queue = None

1.2 工作流执行流程

工作流执行是 ComfyUI 的核心功能,它包括从提交工作流到执行节点的整个过程。以下是工作流执行的几个关键步骤。

1. 工作流验证

首先,系统会对提交的工作流进行验证,确保节点的类型存在、节点连接有效,并且每个节点的输入符合要求。

def validate_prompt(prompt):
    # 1. 验证节点类型是否存在
    # 2. 验证是否有输出节点
    # 3. 验证节点输入
    return (valid, error, good_outputs, node_errors)

2. 执行准备

在验证通过后,系统会初始化执行环境。这包括创建动态的提示(DynamicPrompt),以及初始化缓存系统,以避免重复计算并提高执行效率。

def execute(self, prompt, prompt_id, extra_data={}, execute_outputs=[]):
    # 1. 初始化执行环境
    with torch.inference_mode():
        # 2. 创建动态提示
        dynamic_prompt = DynamicPrompt(prompt)
        # 3. 初始化缓存
        is_changed_cache = IsChangedCache(dynamic_prompt, self.caches.outputs)

3. 节点执行

每个节点的执行流程包括获取节点的输入数据、检查是否有缓存的数据可以复用、执行节点逻辑、并缓存执行结果。节点执行是系统的核心环节,其过程如下:

def execute(server, dynprompt, caches, current_item, extra_data, executed, prompt_id, execution_list, pending_subgraph_results):
    # 1. 获取节点信息
    unique_id = current_item
    inputs = dynprompt.get_node(unique_id)['inputs']
    class_type = dynprompt.get_node(unique_id)['class_type']
    
    # 2. 检查缓存
    if caches.outputs.get(unique_id) is not None:
        return (ExecutionResult.SUCCESS, NoneNone)
    
    # 3. 获取输入数据
    input_data_all, missing_keys = get_input_data(inputs, class_def, unique_id, caches.outputs, dynprompt, extra_data)
    
    # 4. 执行节点
    output_data, output_ui, has_subgraph = get_output_data(obj, input_data_all)
    
    # 5. 缓存结果
    caches.ui.set(unique_id, {...})
  1. 获取节点信息: 获取当前节点的输入和类型信息。
  2. 检查缓存: 如果节点的输出已经缓存,则直接返回缓存结果,避免重复执行。
  3. 获取输入数据: 从上一个节点或缓存中获取需要的输入数据。
  4. 执行节点: 调用节点的执行函数,处理输入并生成输出数据。
  5. 缓存结果: 将执行结果缓存,以便后续节点使用。

1.3 执行队列管理

ComfyUI 通过执行队列管理工作流中的节点执行顺序。每个节点的执行任务会被放入队列中,系统按顺序处理这些任务。

def prompt_worker(q, server):
    e = execution.PromptExecutor(server, lru_size=args.cache_lru)
    
    while True:
        # 1. 获取队列任务
        queue_item = q.get(timeout=timeout)
        
        # 2. 执行提示
        e.execute(item[2], prompt_id, item[3], item[4])
        
        # 3. 资源管理
        if need_gc:
            comfy.model_management.cleanup_models()
            gc.collect()
  • 获取队列任务: 从队列中取出下一个需要执行的节点任务。
  • 执行节点: 调用执行器执行当前节点。
  • 资源管理: 在必要时触发模型清理和垃圾回收,确保系统资源不被过度占用。

1.4 缓存系统

ComfyUI 的缓存系统采用层次化设计,可以缓存节点的输出、对象和 UI 相关的数据,极大地提高了执行效率。

class HierarchicalCache:
    def __init__(self):
        self.outputs = {}  # 节点输出缓存
        self.ui = {}  # UI 相关缓存
        self.objects = {}  # 节点对象缓存
  • 输出缓存(outputs): 缓存节点的执行结果,避免重复计算。
  • 对象缓存(objects): 缓存大数据对象,如模型和图像,以减少加载时间。
  • UI 缓存(ui): 缓存与前端界面相关的信息,如预览图像和执行状态。

1.5 资源管理与错误处理

为了确保系统的稳定性,ComfyUI 提供了完善的资源管理和错误处理机制。在执行工作流的过程中,系统会自动清理未使用的模型和缓存,并在必要时触发内存回收。

资源管理

资源管理包括内存清理、模型卸载以及缓存清理。系统会根据内存使用情况自动卸载不必要的模型,并定期触发垃圾回收。

# 1. 内存清理
comfy.model_management.cleanup_models()
gc.collect()

# 2. 模型卸载
comfy.model_management.unload_all_models()

# 3. 缓存清理
cache.clean_unused()

错误处理

ComfyUI 实现了详细的错误处理机制,能够捕获并处理执行过程中发生的各种异常。对于节点执行中的错误,系统会记录错误信息并通知用户。

def handle_execution_error(self, prompt_id, prompt, current_outputs, executed, error, ex):
    # 1. 处理中断异常
    if isinstance(ex, comfy.model_management.InterruptProcessingException):
        self.add_message("execution_interrupted", mes)
    # 2. 处理其他错误
    else:
        self.add_message("execution_error", mes)
  • 处理中断异常: 当执行被中断时,系统会捕获异常并记录中断信息。
  • 处理其他错误: 处理其他执行错误,并通过 UI 向用户报告错误详情。

2. 节点系统架构

节点系统是 ComfyUI 的核心系统,其节点系统架构设计精巧,支持动态节点的加载、执行和扩展。今天我们详细介绍 ComfyUI 的节点系统架构,涵盖节点定义、执行流程、缓存机制、扩展性和系统特性等方面。

2.1 节点系统的基础架构

ComfyUI 的节点系统基于 Python 模块化设计,所有节点及其行为都通过类的形式进行定义。这些节点在启动时会进行注册,允许系统灵活地加载和使用内置节点与自定义节点。

核心节点定义与注册

ComfyUI 的节点系统在 nodes.py 中定义,并通过以下映射存储所有节点类及其显示名称:

NODE_CLASS_MAPPINGS = {}  # 存储所有节点类的映射
NODE_DISPLAY_NAME_MAPPINGS = {}  # 节点显示名称映射

节点通过类定义,并包含以下几个关键属性:

  • INPUT_TYPES: 输入参数的类型定义。
  • RETURN_TYPES: 返回数据的类型定义。
  • FUNCTION: 节点的具体执行函数。
  • CATEGORY: 节点的类别,用于在 UI 中分类显示。

节点类型

ComfyUI 支持两种类型的节点:

  • 内置节点: 在系统启动时加载,存储在 comfy_extras 目录下。内置节点提供了常见的图像操作、模型加载和处理功能。
  • 自定义节点: 用户可以在 custom_nodes 目录中添加自定义节点,系统在启动时自动加载这些节点。

节点加载机制

ComfyUI 提供了灵活的节点加载机制,允许动态加载内置节点和自定义节点:

def init_builtin_extra_nodes():
    extras_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras")
    extras_files = ["nodes_latent.py""nodes_hypernetwork.py"]

对于自定义节点,ComfyUI 使用动态模块导入的方式加载:

def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes") -> bool:
    module_spec = importlib.util.spec_from_file_location(module_name, module_path)
    module = importlib.util.module_from_spec(module_spec)
    if hasattr(module, "NODE_CLASS_MAPPINGS"):
        for name, node_cls in module.NODE_CLASS_MAPPINGS.items():
            NODE_CLASS_MAPPINGS[name] = node_cls

这种设计使得 ComfyUI 可以方便地扩展和加载新节点,用户可以根据需求自定义节点功能并动态加载。

2.2 节点执行系统

节点的执行逻辑由 execution.py 中的 PromptExecutor 类负责。该类管理了节点的执行流程,包括输入验证、节点函数执行、结果缓存和输出返回等。

执行流程

节点的执行流程如下:

  1. 输入验证: 系统首先验证节点的输入是否符合预定义的输入类型。
  2. 获取输入数据: 从上一个节点或缓存中获取节点的输入数据。
  3. 执行节点函数: 根据定义的 FUNCTION 执行节点逻辑。
  4. 缓存结果: 执行结果会缓存,避免重复计算。
  5. 返回输出: 将执行结果返回给下游节点或 UI。

执行器的核心代码如下:

class PromptExecutor:
    def execute(self, prompt, prompt_id, extra_data=None, execute_outputs=[]):
        # 节点执行的主要逻辑

ComfyUI 通过此执行器确保节点按顺序执行,并管理节点间的数据流动。

2.3 缓存机制

为了提高性能,减少重复计算,ComfyUI 实现了多层缓存系统,缓存节点的输出、对象和 UI 相关数据。缓存的实现类为 HierarchicalCache,并且系统支持 LRU(最近最少使用) 缓存策略。后续章节会详细讲一下缓存,这里先略过。

2.4 数据流与依赖管理

ComfyUI 的节点系统依赖于图形化的数据流管理。节点之间通过输入和输出相互连接,系统会自动分析节点间的依赖关系,确保数据流在节点间正确传递。

节点图解析与执行顺序

  1. 节点图解析: ComfyUI 解析 JSON 格式的节点图,识别节点之间的连接关系。
  2. 依赖管理: 系统自动分析节点间的依赖,确保每个节点在其依赖的节点完成后执行。
  3. 执行顺序构建: 系统基于依赖关系构建节点的执行顺序,防止循环依赖和执行死锁的发生。

2.5 错误处理与资源管理

ComfyUI 实现了完善的错误处理机制,确保节点执行过程中出现错误时,系统能够及时捕获并反馈给用户,避免系统崩溃。同时,ComfyUI 通过自动垃圾回收和内存管理功能,定期清理未使用的模型和缓存,优化系统资源使用。

常见的错误处理机制包括:

  • 输入验证失败: 如果输入数据类型不匹配或数据缺失,系统会抛出异常。
  • 执行错误: 如果节点在执行过程中遇到错误,系统会捕获并将错误信息反馈到前端 UI。

垃圾回收机制由以下代码管理:

gc_collect_interval = 10.0  # 每10秒进行一次垃圾回收

2.6 前端接口与用户交互

ComfyUI 提供了 WebSocket 和 REST API 来管理前端与后端的通信。通过这些接口,前端 UI 可以实时监控节点的执行状态,并获取节点的执行结果。

WebSocket 通信

WebSocket 负责处理前端与后端的实时通信,包括节点执行状态的推送和执行命令的接收。

@routes.get('/ws')
async def websocket_handler(request):
    # 处理前端连接
    # 发送执行状态
    # 处理节点执行

REST API

REST API 用于获取节点信息和处理其他非实时请求。例如,可以通过以下 API 获取节点的详细信息:

@routes.get("/object_info/{node_class}")
async def get_object_info_node(request):
    # 获取节点信息

2.7 系统扩展与自定义

ComfyUI 的节点系统设计非常灵活,支持用户根据需求扩展新功能。用户可以通过编写自定义节点来扩展系统的功能,而无需修改核心代码。系统支持热插拔的节点加载机制,使得自定义节点的开发和调试更加便捷。

动态类型系统

ComfyUI 支持动态类型系统,可以根据运行时情况自动调整节点的输入输出类型,确保系统的灵活性和可扩展性。

插件与自定义节点

自定义节点通过 custom_nodes 目录加载,用户可以编写自己的节点并通过 NODE_CLASS_MAPPINGS 注册到系统中。

3. 缓存系统

在 ComfyUI 项目中,缓存系统的设计主要由以下几个部分组成:

3.1 缓存类型

ComfyUI 的缓存系统主要包括三种缓存类型:

  • outputs: 用于缓存节点的输出结果,减少不必要的重复计算。
  • ui: 缓存与用户界面相关的数据,比如预览图像、状态信息等。
  • objects: 专门用于存储大型对象,如模型等大体积数据。

这三种缓存通过 CacheSet 类进行初始化和管理,具体实现如下:

class CacheSet:
    def __init__(self, lru_size=None):
        if lru_size is None or lru_size == 0:
            self.init_classic_cache() 
        else:
            self.init_lru_cache(lru_size)
        self.all = [self.outputs, self.ui, self.objects]

3.2 缓存实现方式

缓存系统可以根据不同的需求选择不同的实现方式:

Classic Cache(传统缓存)

当没有设置 LRU 缓存大小时,缓存系统会初始化为经典的层级缓存。具体实现如下:

def init_classic_cache(self):
    self.outputs = HierarchicalCache(CacheKeySetInputSignature)
    self.ui = HierarchicalCache(CacheKeySetInputSignature)
    self.objects = HierarchicalCache(CacheKeySetID)
  • outputs 和 ui 都使用 CacheKeySetInputSignature 作为缓存键,用于基于输入签名进行缓存。
  • objects 使用的是 CacheKeySetID 作为缓存键,主要是为了存储大型对象,如模型数据。

LRU Cache(最近最少使用缓存)

如果设置了 LRU 缓存大小,系统会初始化为 LRU 缓存,适用于内存较充足的情况下,以优化性能。

def init_lru_cache(self, cache_size):
    self.outputs = LRUCache(CacheKeySetInputSignature, max_size=cache_size)
    self.ui = LRUCache(CacheKeySetInputSignature, max_size=cache_size)
    self.objects = HierarchicalCache(CacheKeySetID)
  • outputs 和 ui 使用 LRU 缓存,能够根据使用频率自动淘汰最少使用的缓存项,保证内存得到最优管理。
  • objects 仍然使用 HierarchicalCache,因为模型等大型对象的加载和卸载代价较高,不适合频繁淘汰。

3.3 缓存键策略

为了确保缓存的正确性,缓存系统使用两种不同的缓存键策略:

CacheKeySetInputSignature

  • 用于 outputs 和 ui 缓存。
  • 该缓存键基于节点的输入签名,包含节点类型、输入参数及祖先节点关系等,可以确保相同的输入得到相同的输出。

CacheKeySetID

  • 用于 objects 缓存。
  • 这是一种基于节点 ID 和类型的简单缓存键,用于存储与输入无关的大型对象,如模型等。

3.4 缓存清理机制

无论是传统缓存还是 LRU 缓存,ComfyUI 都提供了缓存清理机制,避免过多的缓存占用内存资源。

经典缓存清理

经典缓存会及时释放不再需要的数据,防止内存溢出。

LRU 缓存清理

LRU 缓存通过一个 generation 计数器和 used_generation 字典记录每个缓存项的使用时间。当缓存超出预设大小时,LRU 算法会移除最久未使用的项。

3.5 扩展性和调试

ComfyUI 的缓存系统支持扩展和调试:

  • 扩展性: 缓存系统支持自定义节点和模型的缓存策略,可以根据需要调整缓存大小和键生成逻辑。
  • 调试: 提供了 recursive_debug_dump 函数,能够以递归方式输出缓存的调试信息,方便开发者进行问题排查。
def recursive_debug_dump(self):
    result = {
        "outputs": self.outputs.recursive_debug_dump(),
        "ui": self.ui.recursive_debug_dump(),
    }
    return result

3.6 缓存小结

ComfyUI 的缓存系统设计非常灵活,它通过 CacheSet 类将不同类型的数据(节点输出、UI 数据、大型对象)分离管理,并支持经典缓存和 LRU 缓存两种策略。依靠层次化的缓存结构和精确的缓存键策略,ComfyUI 能够有效地减少重复计算,并优化内存使用。

4. 使用限制

在使用 ComfyUI 时,虽然系统本身没有硬性限制节点数量,但仍然存在一些与性能、资源管理和系统稳定性相关的限制。这些限制大多与历史记录、图像分辨率、缓存、内存管理等方面有关。

4.1 历史记录限制

ComfyUI 会保存工作流中的历史记录,用于回溯和调试。系统中对历史记录的最大保存数量有如下限制:

MAXIMUM_HISTORY_SIZE = 10000
  • 限制描述: 系统最多保存 10000 条历史记录。
  • 影响: 当历史记录达到上限时,旧的记录会被移除,无法继续回溯到更早的操作。
  • 建议: 如果不需要长时间保存历史记录,定期清理历史记录可以释放内存资源,提升系统的运行效率。

4.2 图像分辨率限制

ComfyUI 中对单个图像处理的最大分辨率有限制,防止超大图像导致系统崩溃或内存不足:

MAX_RESOLUTION = 16384
  • 限制描述: 系统允许的最大图像分辨率为 16384×16384 像素。
  • 影响: 超过此分辨率的图像无法处理,可能会导致内存溢出或显存不足的情况。
  • 建议: 尽量避免处理过于高分辨率的图像,尤其是在显存较小的 GPU 上运行时。对于需要处理大图像的工作流,可以考虑将图像分割成多个较小的瓦片。

4.3 VAE 解码瓦片大小限制

在图像生成过程中,VAE 解码器对瓦片大小进行了限制,以确保解码过程的效率与内存管理:

"tile_size": ("INT", {"default"512"min"128"max"4096"step"32})
  • 限制描述: VAE 解码时,允许的瓦片大小在 128 到 4096 像素之间。
  • 影响: 如果瓦片大小设置过大,系统可能会因为内存不足而运行缓慢或崩溃;瓦片大小过小则可能影响解码效率。
  • 建议: 根据 GPU 显存大小合理调整瓦片大小,找到性能和内存占用之间的平衡点。

4.4 文件上传大小限制

在使用 ComfyUI 时,文件上传的大小受限于系统的配置,特别是通过命令行参数 max_upload_size 来控制:

max_upload_size = round(args.max_upload_size * 1024 * 1024)
  • 限制描述: 上传文件的最大尺寸由命令行参数 max_upload_size 控制,单位为 MB。
  • 影响: 超过上传文件大小限制的文件将无法上传或处理。
  • 建议: 如果需要上传较大的文件,可以通过调整命令行参数来提高上传文件的大小限制。

4.5 缓存限制

ComfyUI 使用缓存系统来优化计算效率,减少重复计算。缓存的大小和管理方式可以通过 LRU(最近最少使用)策略进行控制:

def __init__(self, lru_size=None):
    if lru_size is None or lru_size == 0:
        self.init_classic_cache() 
    else:
        self.init_lru_cache(lru_size)
  • 限制描述: 缓存的大小受 LRU 策略控制,缓存过多时,系统会淘汰最少使用的缓存项。
  • 影响: 当缓存大小设置过小,可能会频繁清除缓存,导致重复计算;缓存过大则可能占用大量内存。
  • 建议: 根据内存资源合理设置缓存大小。对于内存充足的系统,可以增加缓存大小,以减少重复计算;对于内存紧张的系统,建议定期清理缓存。

4.6 执行队列限制

节点的执行通过队列进行管理,系统按顺序执行节点,避免同时执行过多节点造成性能瓶颈:

  • 限制描述: 系统使用队列调度节点执行,包括当前运行的队列和等待执行的队列。
  • 影响: 如果节点过多,执行速度会受到队列调度的影响,可能出现等待时间过长的情况。
  • 建议: 尽量简化工作流,避免过多的节点同时排队执行。如果遇到性能瓶颈,可以将大规模的工作流分解为多个子工作流逐步执行。

4.7 Tokenizer 限制

在文本处理方面,CLIP 模型的 Tokenizer 有一个最大长度限制:

"model_max_length"77
  • 限制描述: CLIP 模型的 Tokenizer 最多支持 77 个 token。可修改配置。
  • 影响: 超过 77 个 token 的输入文本将被截断,可能会影响文本生成的精度。
  • 建议: 尽量保持输入文本简洁,避免过长的描述。如果必须使用长文本,可以通过分段输入的方式进行处理。

4.8 限制小结

虽然 ComfyUI 对节点数量没有明确的硬性限制,但在使用过程中仍然受到一些系统资源和配置的限制。这些限制大多是为了确保系统的稳定性、优化性能以及合理使用内存资源。为了避免因这些限制导致的性能瓶颈或崩溃,建议在使用时遵循以下最佳实践:

  • 保持工作流简洁: 避免不必要的节点,定期清理历史记录和缓存。
  • 监控系统资源: 注意监控内存和显存的使用情况,避免资源耗尽。
  • 分解大型工作流: 对于复杂的工作流,可以分成多个子工作流来逐步执行,减少单次执行的节点数量。
  • 调整系统配置: 根据实际需求调整文件上传大小、缓存设置等参数。

通过合理地管理工作流和系统资源,ComfyUI 可以在大型工作流中保持高效运行,避免因资源限制导致的性能问题。

模块化设计带来的无限可能

ComfyUI 的模块化节点系统不仅提升了用户的易用性,还通过灵活的扩展机制和高效的缓存管理提供了强大的自定义能力。

它的图形化工作流设计大大降低了 AI 绘图的技术门槛,使得更多用户能够轻松上手,并根据自己的需求定制不同的图像生成方案。

随着社区的不断壮大和功能的持续扩展,ComfyUI 有望成为 AI 绘图领域的重要基础设施之一,为创作与开发者提供无限的可能性。

以上。

程序员的北京折叠:生存、焦虑与抉择

引子:从《北京折叠》说起

《北京折叠》是郝景芳的一篇著名科幻小说,最早于 2012 年 12 月发表在清华大学的学生论坛水木社区的科幻版。2016 年获得第 74 届雨果奖最佳中短篇小说奖,2018 年获得第 49 届星云赏海外部门短篇小说奖项。雨果奖介绍这篇小说「构建了一个不同空间、不同阶层的北京,可像‘变形金刚般折叠起来的城市’,却又‘具有更为冷峻的现实感’」。

《北京折叠》讲述了北京这个城市被分割成了三个空间,每个空间的人们在各自的时空中生活,彼此之间几乎没有交集。第一空间的人高高在上,掌控着资源与权力;第二空间的中产阶级维持着相对体面的生活;第三空间的人则在贫困、压抑中挣扎求生。三层空间的生活轨迹几乎不会重叠,仿佛他们生活在完全不同的世界中。

作为一名程序员,这个故事让我不禁联想到我们这个行业中的「折叠北京」,在不同的公司、岗位和城市,程序员们同样被划分成了不同的「空间」。每个人的职业轨迹、生活方式和所面临的问题大相径庭,甚至无法体验到他人生活中的酸甜苦辣。

我曾在大厂呆过,在小公司也做过,自己也曾创业。在这些不同的「空间」里,我看到了程序员群体的多样性,感受到了他们各自的焦虑与困境。今天,我想借用《北京折叠》的框架,来聊聊程序员世界中的三种「空间」,它们之间的壁垒、差异,以及偶尔交错的瞬间。

1. 第一空间:大厂程序员的「黄金时代」

在程序员的世界里,第一空间无疑是那些在头部互联网大厂工作的精英们。字节跳动、阿里巴巴、腾讯、网易等巨头公司,几乎可以说是这个行业的象征。对于很多年轻程序员来说,进入大厂意味着职业生涯的「黄金时代」——高薪酬、丰厚的福利、甚至是行业内的一些光环,仿佛一切都昭示着成功与荣耀。

1.1 高压环境中的「内卷」

在大厂工作,最直观的感受就是无处不在的竞争。这种竞争不仅来源于外部市场的技术更新、产品迭代,更深刻地体现在公司内部,尤其是在同事之间。这种现象在互联网行业尤为明显,因此,很多人用「内卷」一词来概括大厂程序员们的工作环境。

1.1.1 绩效排名和末位淘汰制

大厂程序员普遍面临着严格的绩效考核制度。像字节跳动、阿里巴巴等公司,通常实行「361」类的强制考核,即在每次考核中,前20%的员工拿到最好的绩效,而后 20% 左右则面临淘汰的风险。每半年(或者一个季度)一次的绩效考核期,几乎是程序员们最为紧张的时刻,生怕自己成为「差劲」或「末位淘汰」的一员。

这种考核机制确实激励了员工不断提升自我,但也带来了巨大的心理压力和工作负担。为了在绩效评估中脱颖而出,程序员们不得不超负荷工作,甚至牺牲健康和个人生活。许多大厂的加班文化已成常态,尤其是在实行“996”工作制度的公司,程序员们的工作时长远远超出了法律规定的标准。

更为严重的是,由于绩效考核的竞争性,团队内部的合作有时变得愈发功利化。项目的成功不仅关乎团队整体的荣誉,还直接决定了每个人的绩效评定。于是,暗中较劲、互相攀比的现象时有发生,团队协作因此变得更加复杂且微妙。

1.1.2 怎样才算「成功」?

在大厂的程序员群体中,有一种不成文的共识:成功的标志不是你是否能够完成日常的任务,而是你能否写出新技术、推动新项目,甚至在团队中成为某个领域的权威。每个人都在追求「技术大咖」的头衔,渴望在某个技术社区或者公司内部的技术分享会上崭露头角。技术的不断迭代让人们时刻保持学习的心态,但这种持续的自我提升也带来了巨大的压力。

有时我会和一些在大厂的朋友聊起他们的生活,发现他们的焦虑和我在小厂时的焦虑并没有本质区别。尽管他们拿着比普通程序员高得多的工资,但他们的时间成本、精神压力和对未来的迷茫感也不比别人少。他们的生活轨迹看上去光鲜亮丽,但其实也是在一种高强度的环境中挣扎生存。

为了在考核中脱颖而出,程序员们会拼命寻找可以量化的业绩,比如开发新功能、优化系统性能、贡献开源项目等。然而,这种短期导向的行为,往往导致大量的重复劳动。不同的团队、甚至同一团队的成员,可能都在做相似的工作,因为每个人都希望自己的成果被视为「独创贡献」。

这种过度竞争导致了资源的浪费和技术的冗余。比如,不同团队可能会开发多个功能类似的工具或系统,但由于每个团队都希望展示自己的「独立成果」,这些项目往往没有被整合,造成了效率低下。这种「重复造轮子」的现象在大厂程序员中屡见不鲜,不同的部门,甚至不同的中心各有一套技术栈或管理系统的很常见。这不仅浪费了时间和资源,也让公司的整体创新能力受到抑制。

1.2 裁员潮下的生存危机

1.2.1 大厂裁员的频发性

近年来,随着互联网行业的逐渐成熟和增速放缓,国内外的大厂频繁爆出裁员的新闻。无论是由于公司战略调整,还是市场环境的变化,裁员已经成为了大厂的一种常见操作。即使是表现优异的部门,也可能因为公司调整方向而面临裁撤的命运。

大厂裁员并不仅仅针对绩效较差的员工。很多时候,裁员是为了优化成本结构,或者是公司业务重心发生了转移。某些曾经处于风口的业务部门,一旦被认为前景不妙,整个团队可能会在短时间内被解散。例如,一些大厂在短视频、智能硬件等领域的扩张速度过快,导致后期发展遇阻,一旦业务不达预期,相关团队就可能面临大规模裁员。

以字节为例,2023 年底字节跳动官宣大规模裁撤游戏项目和人员,未上线项目几乎全部关停,已上线且表现良好的游戏也要寻求剥离; 2024 年初飞书裁员超过 20%,

这种裁员的不可预测性,给大厂程序员的职业生涯带来了巨大的不确定性。即便你今天的绩效再优秀,也无法保证明天公司不会因为战略调整而决定裁掉你所在的部门。这种生存危机,成为了大厂程序员的长期困扰。

还在某大厂的兄弟说:以前,末位淘汰了还可以增补 HC,但是现在淘汰了就是淘汰了,不会有新的人补充进来,且强制 10% 的比例。这也是一种裁员的逻辑。

1.2.2 「大龄程序员」的困境

裁员的另一大受害者群体是所谓的「大龄程序员」,即那些年龄超过 35 岁、甚至 40 岁以上的技术人员。在很多大厂的文化中,年轻意味着活力和更强的工作负荷承受能力,因此,年龄较大的程序员往往被认为「性价比不高」。

当公司需要削减成本时,首先会考虑那些薪资较高的员工。而大龄程序员由于工龄长、薪资高,往往成为了裁员的首选对象。即便这些程序员有着丰富的技术经验和项目管理能力,但在日新月异的互联网行业,他们的优势往往被削弱。

同时,技术更新日新月异,大龄程序员若无法持续跟上行业的技术潮流,便可能在职业生涯中陷入困境。很多人会在 35 岁之后面临职业发展的瓶颈,不得不思考转型的可能性。

1.3 程序员的「供需失衡」

与十几年前程序员供不应求的情况不同,如今的互联网行业已经趋于饱和。随着越来越多的人涌入这个领域,市场对程序员的需求增速放缓,导致了供需之间的失衡。

在 2024 年 8 月招生季,太原理工 2024 软件工程招 60 个班,近 2000 人,冲上热搜。想象一下,在四年之后的这些学生的就业难度会像「通货膨胀」一样飞速上涨。

这种供需失衡带来了一系列问题。在初级程序员这一级,竞争会更加激烈,很多应届毕业生发现自己面临大量竞争对手,哪怕是基础岗位,也往往需要具备极高的技术能力。

企业在招聘时可以更加挑剔,倾向于选择那些工资要求低、技术基础扎实的年轻程序员,而那些经验丰富但薪资要求较高的资深程序员,反而变得不那么受欢迎。

程序员岗位已经从一个「卖方市场」彻底转变为「买方市场」

在「卖方市场」时期,企业为了吸引优秀的技术人才,往往会提供丰厚的薪资福利和极具吸引力的职业发展机会。然而,随着越来越多的程序员涌入市场,岗位供给的增速却远远赶不上需求的增长,企业开始占据更多的主动权。

在买方市场中,企业可以更加挑剔地选择应聘者,不仅要求候选人具备扎实的技术基础,还希望他们能够适应更高的工作强度和更低的薪资要求。这种局面尤其对初级程序员和应届毕业生不利。哪怕是一些基础岗位,也往往需要较高的技术门槛和项目经验,导致很多刚毕业的学生发现自己难以找到合适的工作机会。

与此同时,资深程序员的处境也不容乐观。那些拥有多年经验的程序员,虽然在技术上更为成熟,但由于薪资要求较高,企业在招聘时往往更愿意选择年轻、成本较低的程序员。这种现象让很多资深程序员陷入了「高不成低不就」的尴尬境地。他们的技术能力虽然依然强大,但在快速变化的互联网行业中,市场对他们的需求开始减少,尤其是在裁员潮和优化成本的背景下,资深程序员的议价能力逐渐被削弱。在就业市场上常常可以看到一个岗位多个人竞争的情况。

1.4 大厂程序员的「中年危机」

1.4.1 技术更新的焦虑

程序员这个职业最大的特点之一是技术更新的快速迭代。每隔几年,行业的技术栈就会发生翻天覆地的变化。从最早的C、C++到如今的云计算、人工智能和区块链,每一波技术浪潮都要求程序员持续学习新知识,适应新的工具和框架。

对于年轻程序员来说,学习新技术可能充满了乐趣和挑战性。但对于年纪较大的程序员来说,技术更新的压力往往带来了巨大的焦虑感。随着年龄增长,学习新技术的难度和精力投入都在增加,而大厂的工作环境又要求程序员始终保持对新兴技术的敏感度。这种持续的技术更新压力,让很多大龄程序员感到力不从心。

1.4.2 顶层的天花板

对于很多大厂程序员来说,最可怕的不是眼前的压力,而是那种隐隐约约的「天花板」感。你很难在大厂中看到五十岁、甚至四十岁以上的程序员,他们的去向仿佛成了一个谜题。

大家心照不宣地知道,到了某个年龄段,技术可能已经不再是你的核心竞争力,管理岗位有限,竞争者众多,如何突破这层「天花板」成了很多大厂程序员内心深处的焦虑。

面对年龄、技术更新和职业发展的瓶颈,很多大厂程序员在 30 岁之后开始考虑职业转型。然而,转型并不是一件容易的事情。大多数程序员的职业技能都围绕技术展开,一旦离开了技术岗位,很多人发现自己在其他领域缺乏竞争力。

常见的转型路径包括转向管理岗位、创业或进入教育培训行业。然而,管理岗位有限,创业风险极大,而教育培训行业本身也在经历着调整。这使得很多程序员在转型的过程中感到困惑和无助。职业发展的瓶颈使得大龄程序员的未来看起来充满了不确定性。

1.5 黄金时代的背后是无尽的焦虑

大厂程序员的生活看似光鲜,但背后却充满了无尽的压力与焦虑。高薪的代价是长期的加班和激烈的内卷;丰厚的待遇伴随着频繁的裁员和职业发展的瓶颈。尤其是大龄程序员,他们不仅面临着技术更新的焦虑,还要应对职业转型的困惑。

在这个日新月异的行业里,大厂程序员的「黄金时代」或许并不像外界看到的那样光鲜。当「中年危机」到来,如何平衡工作与生活、如何应对技术的快速变化,成为了每一个程序员都需要思考的问题。

如 will 老板所说:始终要思考的是如何在大厂活下去!,更进一步:其实更焦虑的是如何靠自己活下去

2. 第二空间:小厂程序员的迷茫与抉择

2.1 资源、团队与技术的困境

在小公司工作的程序员面临的第一个现实问题是资源的匮乏。与大厂程序员相比,小厂程序员的开发环境和资源往往十分有限。预算紧张使得小公司无法购买先进的开发工具,也没有大厂那样完善的基础设施和支持团队。很多时候,程序员需要用「土办法」去解决问题,甚至自己搭建和维护服务器、数据库等基础设施。

虽然现在云服务的使用已经很普遍了,但是能用好云服务的公司不多,甚至在常见的 CI/CD 流程都没有实施。

团队情况也是一个重要因素。小公司里,团队人员往往较少,职责分工不如大公司细致,很多程序员需要身兼数职,既要写代码,还要负责运维、测试,甚至参与产品设计和业务讨论。这种「多面手」的工作方式虽然能让个人能力得到快速锻炼,但也意味着专注度较低,无法在某一个领域深入钻研,导致技术积累不够扎实。

技术的硬门槛是另一大挑战。小公司通常专注于短期业务目标,项目进度往往比技术本身更加重要。这导致程序员在开发过程中可能会放弃对代码质量、性能优化等技术细节的追求,而更多地采用快速上线的策略。这种方式虽然能让产品迅速推向市场,但也限制了程序员的技术视野和思维,长期下去,很容易陷入技术瓶颈

2.2 平台、资源与局限

2.2.1 资源的限制

与大厂相比,小厂程序员的工作环境显得更加局促和紧张。他们没有大公司那样强大的技术团队或前沿的技术工具支持,很多时候只能依赖现有资源,甚至是开源工具来解决问题。

公司往往没有足够的预算去支持技术创新,项目的重点更多地放在如何快速满足客户需求上,而不是技术实现的完美度。因此,小厂程序员的工作更多的是一种「打补丁」的过程,解决眼前的问题,而不是从根本上提升系统的架构或性能。

由于缺少大厂的技术资源和系统流程,小厂程序员在面对复杂问题时只能依赖个人经验和有限的知识储备。这种资源的匮乏,让他们在遇到需要深入技术实现或复杂系统优化的问题时力不从心,也限制了他们的职业发展。

2.2.2 多面手的隐患

小公司经常要求程序员成为「全栈开发者」,不仅要负责前端、后端的开发,还要参与运维、测试,甚至是产品设计。这种「多面手」的角色虽然能在短时间内提升程序员的综合能力,但长期来看,专精度的不足是显而易见的。程序员往往在多个领域都有所涉及,却缺乏一个深耕的方向,导致在某些关键技术上与大厂程序员相比存在明显的差距。

这种现象尤其体现在一些高精尖的领域,比如分布式架构、性能优化、大规模数据处理等。小公司项目的局限性使得程序员鲜有机会接触这些高端技术,即便遇到相关问题,也往往是通过快速修补的方式解决,而不是深入理解和优化。多面手的广度虽然让小厂程序员具备了应对不同问题的能力,但缺乏深度的劣势在面对更高的技术挑战时显露无遗。

2.2.3 重复与瓶颈

小公司项目的重复性也是一个常见的问题。许多小公司专注于某些特定的业务场景,程序员在开发过程中,往往是在重复类似的增删改查操作。长时间在这种环境中工作,程序员容易陷入一种技术思维的局限,觉得自己的工作仅仅是完成客户需求,而忽视了技术本身的提升。这种局限让他们在面对更复杂的项目或系统时,缺乏应对的思路和方法。

在这种环境下,程序员可能会感到希望突破但找不到方向。他们渴望接触更复杂、更有挑战性的技术,但小公司的项目和资源限制了他们的视野,无法提供足够的成长空间。很多程序员在小公司工作多年后,逐渐意识到,自己的技术积累始终停留在某个水平,无法突破。

2.3 对未来的迷茫与期待

2.3.1 稳定性的假象

小厂程序员的处境,常常在稳定与成长之间徘徊。对于很多在小公司干了多年的人来说,工作内容虽然相对稳定,压力小,甚至在某些场合下还能当上小领导,但这种「舒适区」并不一定带来长久的安全感。

尽管有些程序员在小公司工作多年,积累了一定的业务经验,甚至在团队中占据了重要的角色,但这并不意味着未来的职业道路是一片坦途。小公司的抗风险能力差,经济波动或行业萎缩时,很多小公司会迅速陷入困境,甚至倒闭。对于很多 30 岁上下的程序员来说,一旦失去这份相对稳定的工作,他们可能会发现自己在技术上并没有明显优势,面临再就业的难题。

这种不稳定性让很多小厂程序员产生了焦虑感。他们担心公司倒闭后,自己所积累的业务经验和技术能力无法顺利转化到其他公司。尤其是在面对大厂的面试要求时,很多小厂程序员会发现自己的项目经验和技术广度远远不足以应付大厂的高标准。进退两难的局面让他们陷入迷茫,不知道未来的职业发展该何去何从。

2.3.2 突破的渴望与现实的差距

尽管如此,很多小厂程序员依然保持着突破现状的愿望。他们希望自己的公司能够做大做强,从而拥有更多的资源和技术成长的机会。然而,现实往往并不如人意。小公司能做到一定规模的并不多,很多公司最终还是会因为市场竞争激烈、资金不足等原因被淘汰。

因此,跳槽到中型公司或大厂历练,成为了不少小厂程序员的另一种理想选择。他们希望通过进入更大平台,接触到更多的技术挑战和行业资源,打破在小公司中「打转」的局面。但这种跳槽并不容易,尤其是对于长期习惯了小公司开发模式的程序员来说,想要进入大厂不仅需要提升技术硬实力,还需要适应大厂的工作节奏和文化。

2.4 跳槽到大厂:进阶还是冒险?

对于那些在小公司工作了多年,并且已经进入到领导层的程序员来说,最大的问题往往是:现在跳槽到大厂,值得吗?

2.4.1 跳槽的机遇

跳槽到大厂意味着能够接触到更复杂的技术栈和更具挑战性的项目。在大厂中,程序员不仅可以学习到前沿的技术(如微服务架构、Kubernetes、分布式系统等),还能够获得更为完善的职业晋升通道。大厂的技术氛围和资源整合能力,也意味着程序员能够更快地成长,跳出小公司单一业务的限制。

此外,大厂的品牌效应也不容忽视。即使是普通开发,拥有大厂背景的程序员在未来的求职市场上,无论是跳槽还是创业,都具有更高的含金量。

2.4.2 跳槽的风险

然而,跳槽到大厂并非没有风险。大厂的竞争激烈,程序员需要面对年轻一代的强大竞争压力。大厂的工作节奏快、加班文化重,许多 30 岁左右的程序员可能会发现,自己在体力和精力上难以与年轻人抗衡。

进入大厂后,之前在小公司积累的业务经验和管理经验未必能够直接转化为优势。大厂的岗位分工更加明确,很多程序员在跳槽后可能需要从普通开发做起,甚至重新适应新的工作流程和技术要求。

跳槽到大厂对于 30 岁上下的程序员来说,是一个双刃剑。如果能够抓住机会快速提升技术能力,则职业生涯将迎来新的突破;但如果无法适应大厂的节奏,则可能面临事业的再次迷茫。

2.5 技术能力和学习能力是立足之本

小厂程序员的迷茫和焦虑,归根结底源于技术成长的瓶颈和职业发展的不确定性。面对快速变化的行业环境,程序员们需要不断提升自我,不仅要在技术上有所突破,还应当具备长远的职业规划。

无论是在小公司继续发展,还是跳槽到大厂,程序员都应当意识到,技术能力和学习能力是立足于这个行业的根本。唯有不断学习和进步,才能在程序员的职业道路上走得更远、更稳。

3. 第三空间:外包与自由职业者的「生存游戏」

3.1 外包的世界

在大厂和小厂之外,还有一群程序员,他们生活在外包公司中。外包程序员的生活与大厂和小厂截然不同,他们的工作内容往往由客户决定,技术栈也不是自己可以随意选择的。一些外包程序员可能会长期为某个大厂或者知名企业提供服务,但他们并不属于这些公司,他们的身份始终是「外包」。

外包程序员的收入通常与大厂程序员有较大差距,工作内容也更加琐碎。与大厂和小厂的开发者相比,外包程序员的职业发展路径更为模糊。很多人觉得外包是一个「临时的选择」,但一旦进入外包行业,往往很难轻易跳出来

3.2 自由职业者的自由与孤独

与外包程序员类似,自由职业者也是程序员群体中的一个独特存在。他们没有固定的公司和老板,依靠接项目为生。自由职业者的生活看似自由,但实际上他们承担了巨大的生活压力:项目的来源、项目的质量、客户的付款周期,这些都直接决定了他们的收入。

我有一位朋友曾辞职做过一段时间的自由职业者,他的经历让我对这一群体有了更深的了解。他曾告诉我,自由职业的最大挑战不是技术,而是如何维持客户关系、如何接到稳定的项目。自由职业者的生活往往充满了不确定性,每天都是一次新的「生存游戏」。

4. 结语:折叠的程序员世界

程序员的世界如同《北京折叠》中的三个空间:大厂、小厂,外包与自由职业者,各自有着截然不同的生活方式与职业挑战。大厂程序员在高薪与内卷中挣扎,小厂程序员在资源匮乏和职业迷茫中徘徊,外包和自由职业者则在充满不确定性的项目中谋生。每个空间都有其独特的焦虑与困境,而这些困境往往是外界无法轻易察觉的。

然而,这些看似完全隔绝的空间并非毫无交集。在某些时刻,程序员们的职业轨迹会短暂交错:大厂的程序员可能因职业倦怠转而投身小厂,或选择成为自由职业者;小公司的程序员也可能抓住机会进入大厂,体验另一种生活。外包和自由职业者也常常通过项目合作,与大厂程序员产生联系。

折叠的背后,是程序员们面对的共同挑战:快速变化的技术浪潮、工作与生活的平衡、未来职业发展的不确定性。

无论身处哪个空间,程序员不仅要面对代码和产品,还要面对生活的选择与妥协。技术的迭代让人时刻保持危机感,职场的竞争让人不断追逐更高的目标,但归根结底,程序员们都在寻找如何掌控自己的命运,在压力与选择中找到一条适合自己的道路。

或许,正是这种多元的职业轨迹和复杂的生存环境,构成了程序员世界中的「折叠北京」。每个空间的故事,都在提醒我们:技术人的真正挑战,不仅在于掌握技术,更在于如何在折叠的世界中找到属于自己的平衡与方向

5 块钱,就可以给娃训练一个专属的 AI 模型

上周末花了 5 个小时,从零开始给娃训练了一个基于 flux1_dev 的 lora,用于生成娃的照片。

以下为详细(保姆级)的过程记录,有想法又没有自己训练过 lora 的程序员爸爸或妈妈们可以尝试一下。

可能的时间成本:2 ~ 5 小时,资金成本:5 ~ 10 块。

环境准备

因为我们会用到 GPU 机器,去类似于阿里云这种云平台租,太贵。

这里我们选择的是 audodl ,一个比阿里云便宜 60% 的可按小时租 GPU 服务器的算力平台。autodl 基于容器实现,在使用上有一些局限性,但是对于我们当下的需求来说,还是非常匹配的。

我们选择如图 1 所示的配置:

图1

4090D + 50 G 额外的空间

尽量选择空闲 GPU 多的主机或所在区,因为可能会存在主机 GPU 不够的情况。

下单付款,等待容器启动。

容器启动后,直接使用官方提供的 web 的终端,这个终端有一个好处是当你关掉浏览器以后,终端还在,有点类似于 nohup 的执行状态。

需要说明一下,autodl 的存储分为系统盘和数据盘,在 /root/autodl-tmp 下为数据盘空间。我们要下载的模型、项目等都放到数据盘下。

把 commfy UI flux 跑起来

1. 创建 comfyUI 的环境并跑起来

cd ~/autodl-tmp

git clone https://github.com/comfyanonymous/ComfyUI
cd ComfyUI
conda create -n comfyui python=3.12 -y

conda init
source ~/.bashrc

conda activate comfyui
pip install -r requirements.txt 

在 git clone 时可能会遇到克隆失败的情况,此时需要使用 autodl 的学术资源加速:https://www.autodl.com/docs/network_turbo/

另外,在 conda create 后无法直接 conda activate,需要先 conda init,然后 source ~/.bashrc 或重新启动一下终端

接下来是启动 ComfyUI

python main.py

启动是启动了,但是如何访问了,这台机器又不在我本地,难道要启一个外网端口监听,此时就需要启动自定义服务,当然我们也可以使用 ssh 的通道能力,让本地电脑能访问远程的服务,在本地命令行中执行如下的命令:

ssh -CNg -L 8188:127.0.0.1:8188 -p 49027 root@connect.westb.seetacloud.com

以上的 -p 后的参数对于不同的机器不同,请参考控制台上的登录指令,如图2:

图2

输入后会提示:root@connect.westb.seetacloud.com’s password:

当时将控制台的密码复制出来,粘贴即可,回车。此时,可能没有啥反应,嗯。这是正常的。不用管他,除非报错告诉你连接失败或者密码错误,再重新输入。

在浏览器中输入:http://localhost:8188/

就可以看到 ComfyUI 的界面了。

2. 下载并运行 flux

在 ComfyUI 的 flux1 的 Demo 界面中:https://comfyui-wiki.com/tutorial/advanced/flux1-comfyui-guide-workflow-and-examples

可以把 Flux Dev ComfyUI workflow example 那张美少女的照片下载下来,直接拖到 ComfyUI 的界面上,或者下载 json 文件加载进来。如下图所示:

图3

要把示例跑起来,我们有 4 个文件要下载,分别是:

  • 放到 models/unet 目录下的 flux1.dev fp8 模型:https://hf-mirror.com/camenduru/FLUX.1-dev/resolve/main/flux1-dev-fp8.safetensors
  • 放到 models/vae 目录下的 VAE 模型:https://hf-mirror.com/camenduru/FLUX.1-dev/resolve/main/ae.safetensors
  • 放到 models/clip 目录下的 clip 编码器:https://hf-mirror.com/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors
  • 放到 models/clip 目录下的 T5XXL 文本编码器:https://hf-mirror.com/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors

在 hf 下可能会存在下载失败的情况,这里我们选择 hf-mirror: https://hf-mirror.com/camenduru/FLUX.1-dev/tree/main

下载可能会持续个 20 分钟左右,视网络情况,早上下载可能会快一些。

接下来就比较简单了,在本地浏览器中打开,点击运行 Queue Prompt 按钮

第一次运行会慢一点,因为需要加载模型这些,后面再次执行就大概 10 秒多一点。

图4

把 lora 加到管线并跑起来

1. 下载 lora 模型在类似于 liblib.art 之类的网站下载一个你觉得不错的 lora 模型,将其放到 ComfyUI 的 models/loras 目录中。

在 ComfyUI 的界面中增加一个 lora 的节点,这里我选择的是纯粹的 lora 节点,如图 5 所示

图5

点击 Queue Prompt 按钮,等一会儿就能看到生成的图片了,如果图片不符合预期,可以调整一下 prompt 试试,如你选择的 LoRA 是一个美女明星的 LoRA,简单点的:a girl,portrait, professional portrait

2. 调整 LoRA 强度与权重在 LoRA 节点中,你也可以调整 LoRA 的权重,以影响生成图片时对 LoRA 的依赖程度。通常,权重值可以设置在 0.5 到 1.0 之间,这取决于你希望模型生成的图片与 LoRA 模型的契合度。权重越高,生成的图片越接近 LoRA 模型的风格;而权重越低,则越依赖于你输入的 prompt

调整 LoRA 节点的参数后,再次点击 Queue Prompt 按钮,等待生成的新图片。如果生成的图片依然不符合预期,可以继续调整 LoRA 权重,或者尝试修改 prompt 来更好地引导模型。

打标

在 LoRA 跑起来并生成合适的图片后,下一步就是准备训练数据集。数据集的质量对模型的效果至关重要,因此我们需要对数据集进行打标。打标的目的是为每张图片生成准确的描述标签,使模型能够理解图片的内容,以便在未来生成类似的图像时能够参考这些标签。

打标工具

选择合适的打标工具或方案可以帮助我们生成高质量的标签。以下是一些常用的打标工具或方案:

  1. Florence 2:Florence 2 是一个强大的自动打标工具,能够为图片生成高精度的标签。它的优点是标签的细节和准确性较高,适合用于需要精确描述的图片场景。
  2. JoyCaption:JoyCaption 是 WD14 的替代工具,提供了更细致的标签生成能力。它能够为每张图片生成更详细的描述,适合用于生成具备复杂场景或人物的标签。不过,它也有一定的出错概率,因此需要额外的人工检查。
  3. MiniCPM:MiniCPM 是一个轻量级的打标工具,适合小规模数据集的快速打标。它的优点是操作简便,生成速度快,缺点是生成的标签可能不够详细,适合用于简单场景的图片。
  4. WD14:WD14 是一个非常流行的打标工具,特别适合用于文生图任务。它的标签生成能力较强,能够根据图片生成与文本 prompt 相关性较高的标签。特别适用于需要高质量和一致性的图片生成任务。

打标策略

为了确保数据集的质量,打标时可以采用以下策略:

  1. 多工具结合使用:如果你的图片集包含不同类型的图片,可以使用多个打标工具来生成初步标签,然后对标签进行合并和校对。比如:使用 Florence 2 生成的标签作为基础,再用 WD14 生成补充标签。
  2. 人工校对:虽然自动打标工具能够节省大量时间,但是人工校对是必不可少的环节。检查和修正错误标签,确保每张图片的标签描述都准确无误。
  3. 标签标准化:在打标过程中,确保使用一致的标签格式和风格。比如:尽量使用标准化标签(如 “portrait”, “landscape”, “girl”, “cityscape” 等)来确保不同图片的标签一致性。
  4. 使用特定的标签:对于我们当前这个场景,对每个图片在 AI 打标的基础上增加额外的一个标签来代表娃,比如我在这里使用的是 bbdr,具体参考打标的脚本。

WD 打标

在这里我选择的是 https://huggingface.co/SmilingWolf/wd-swinv2-tagger-v3 来实现的打标,在其 web demo 的基础上做了一点小改造,具体代码见:https://github.com/phppan/wd-swinv2-tagger-v3-script/blob/main/wd_tag.py

git clone https://github.com/phppan/wd-swinv2-tagger-v3-script

cd wd-swinv2-tagger-v3-script/

conda create -n wds python=3.12 -y
conda activate wds
pip install -r requirements.txt 
mkdir data

export HF_ENDPOINT=https://hf-mirror.com

可能会存在克隆失败的情况,如:

fatal: unable to access 'https://github.com/phppan/wd-swinv2-tagger-v3-script/': GnuTLS recv error (-110): The TLS connection was non-properly terminated.

多试几次。

最后的 export HF_ENDPOINT=https://hf-mirror.com 要执行,我们从镜像网站下载模型,hf 有时候无法直接访问。

如果嫌麻烦,可以 vim ~/.bashrc,在此文件中添加后 source ~/.bashrc

上传数据集并生成打标内容

将前面准备好的 10 张照片上传到上面创建的 data 目录。执行

python wd_tag.py

在上面的打标脚本中,我将特定的标签 bbdr 加到标签中,后续在生成的时候都会带上这个标签。你也可以改成你自己的标签,赖博说这里:最好几个辅音叠加,不容易冲突

等待模型下载完成,并执行完打标工作,在 output 目录中可以看到生成和图片名称同名的 txt 文件,这就是我们打好了的标签。

开始训练模型

模型训练,这里我选择的是 ai-toolkit:https://github.com/ostris/ai-toolkit

git clone https://github.com/ostris/ai-toolkit
cd ai-toolkit/
git submodule update --init --recursive

conda create -n aitool python=3.12 -y
conda activate aitool

pip3 install torch
pip3 install -r requirements.txt

修改配置文件并执行训练

cp config/examples/train_lora_flux_24gb.yaml ./config/bbdr.yaml
vim ./config/bbdr.yaml

一些参数的修改可以参考视频:https://www.youtube.com/watch?v=HzGW_Kyermg 主要是修改:name(名字)、training_folder(训练生成的目录)、folder_path(训练集路径)

python run.py ./config/bbdr.yaml

接下来就是等待训练完成。

训练完成后,在上面参数中设置的生成目录中可以看到你想要的 lora 模型文件,如我们设置的名字是 bbdr,则生成的模型名为:bbdr.safetensors

将其复制到前面我们已经部署好了的 ComfyUI 项目的 models/loras 目录下,在界面中选择你要的 lora 模型,在 prompt 中把你自定义的标签加上,如我们这里是 bbdr,一个简单的示例:bbdr,a girl,cute,perfect body,standing,street

等一下,就能看到专属于娃的 AI 模型生成的图片了。

其它

  1. 如果在编写/调试代码、上传下载数据到实例、给他人做代码展示等不需要GPU卡场景时,可以关机后使用无卡模式开机,无卡模式开机的区别在于对于这次开机会使用0.5核;2GB内存;无GPU卡的配置,价格统一为¥0.1/小时
  2. 如果不想等训练完成,可以设置定时关机。
  3. 照片请准备同一时期的照片,不同时期的照片,特别是小朋友,可能相貌差别较大,会导致最终生成的效果不好

以上是程序员逻辑,主打一个学习和挑战。

不想整这么复杂的,可以选择在类似于 liblib 的 lora 训练平台上生成:https://www.liblib.art/pretrain 或者类似于 https://www.mimicpc.com/ 这样的平台。

强哥说:看了这么久的理论,这次终于破天荒来了篇工程的。

以上。

最后感谢赖博的指导