Files
2025-12-16 15:14:35 +08:00

668 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ComfyUI Functional 实现原理
本文档面向开发者,详细介绍 ComfyUI Functional 节点包的内部实现机制。如需了解如何使用这些节点,请参阅 [usage.md](./usage.md) 和 [node-reference.md](./node-reference.md)。
## 目录
1. [整体架构](#整体架构)
2. [执行钩子与工作流预处理](#执行钩子与工作流预处理)
3. [函数定义与闭包转换](#函数定义与闭包转换)
4. [函数调用与图展开](#函数调用与图展开)
5. [协程机制与高阶函数](#协程机制与高阶函数)
6. [副作用处理](#副作用处理)
7. [远程函数调用](#远程函数调用)
8. [类型系统与工具函数](#类型系统与工具函数)
9. [技术细节与限制](#技术细节与限制)
---
## 整体架构
ComfyUI Functional 通过以下几个核心机制实现函数式编程:
```
┌─────────────────────────────────────────────────────────────────┐
│ ComfyUI 执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ 1. 用户提交工作流 (Prompt) │
│ ↓ │
│ 2. hook.py 拦截执行请求 │
│ ↓ │
│ 3. transform.py 预处理工作流 │
│ - 转换 FunctionParam/FunctionEnd → CreateClosure │
│ - 预处理 Inspect 节点 │
│ ↓ │
│ 4. ComfyUI 执行转换后的工作流 │
│ - CallClosure 通过 expand 机制动态展开函数体 │
│ - CoroutineNodeBase 通过 IntermidiateCoroutine 实现迭代 │
│ ↓ │
│ 5. 返回执行结果 │
└─────────────────────────────────────────────────────────────────┘
```
### 核心文件说明
| 文件 | 职责 |
|------|------|
| `hook.py` | 钩住 ComfyUI 的 `PromptExecutor.execute` 方法,在执行前预处理工作流 |
| `transform.py` | 工作流图的静态变换算法,将函数定义转换为闭包节点 |
| `core.py` | 核心节点实现:`FunctionParam`、`FunctionEnd`、`CreateClosure`、`CallClosure`、`CoroutineNodeBase` |
| `utils.py` | 工具函数:`AnyType` 通配类型、`Closure` 数据结构、图创建与序列化 |
| `high_level.py` | 高阶函数节点:`Map`、`Fold`、`Nest` 等 |
| `side_effects.py` | 副作用节点:`Sow`、`Reap`、`Inspect`、`Sleep` |
| `logic.py` | 逻辑控制节点:`IfCondition`、`LogicalAnd`、`LogicalOr` |
| `remote.py` | 远程函数调用节点:`CallClosureRemote`、`Serialize`、`Deserialize` |
| `comfy_client.py` | ComfyUI HTTP/WebSocket 客户端,用于提交远程工作流 |
---
## 执行钩子与工作流预处理
### 钩子注入 (`hook.py`)
ComfyUI Functional 在模块加载时(`__init__.py`)自动钩住 ComfyUI 的执行器:
```python
# hook.py
def hook_comfyui_execution():
from execution import PromptExecutor
original_execute = PromptExecutor.execute
def hooked_execute(self, prompt, prompt_id, extra_data={}, execute_outputs=[]):
reset_reap_storage() # 重置 Sow/Reap 的全局存储
prompt, warnings = transform_workflow(prompt, execute_outputs)
return original_execute(self, prompt, prompt_id, extra_data, execute_outputs)
PromptExecutor.execute = hooked_execute
```
这个钩子确保每次执行工作流前:
1. 清空 `Sow`/`Reap` 的全局存储
2. 对工作流进行静态变换,将函数定义转换为闭包
---
## 函数定义与闭包转换
### 用户视角的函数定义
用户通过以下节点组合定义函数:
- **`FunctionParam`**:声明函数参数,通过 `index` 指定参数顺序
- **`FunctionEnd`**:标记函数返回值,输出一个 `CLOSURE` 类型
### 静态变换算法 (`transform.py`)
`transform_workflow` 函数将用户定义的函数子图转换为 `CreateClosure` 节点。算法流程如下:
#### 步骤 1:构建图结构
```python
class WorkflowGraph:
def __init__(self, workflow: Dict):
self.adj: Dict[str, List[str]] # 邻接表:节点 → 后继节点列表
self.rev_adj: Dict[str, List[str]] # 反向邻接表:节点 → 前驱节点列表
self.in_degree: Dict[str, int] # 入度
self.out_degree: Dict[str, int] # 出度
```
#### 步骤 2:预处理 Inspect 节点
`_preprocess_inspect_nodes` 将 `__Inspect__` 节点后直接连接的输出节点"吸收"到 Inspect 的 `body` 中,转换为 `__InspectImpl__` 节点。这样可以避免函数体内的输出节点被过早执行。
#### 步骤 3:拓扑排序并处理函数定义
按拓扑顺序遍历所有 `__FunctionEnd__` 节点,对每个函数执行 `_transform_single_function`:
```python
def _transform_single_function(end_node_id: str, workflow: Dict, graph: WorkflowGraph):
# 1. 从 FunctionEnd 反向遍历,找到完整的函数子图
subgraph_nodes = _find_function_subgraph(end_node_id, graph)
# 2. 从 FunctionParam 正向遍历,找到依赖参数的节点(函数体)
param_nodes = _find_param_nodes(subgraph_nodes, workflow, graph)
# 3. 计算捕获节点 = 子图 - 参数依赖节点
capture_nodes = subgraph_nodes - param_nodes
```
#### 函数子图的划分
```
┌─────────────────────────────────────────────────────────────────┐
│ 函数子图 │
├──────────────────────┬──────────────────────────────────────────┤
│ 捕获节点 │ 参数依赖节点(函数体) │
│ (capture_nodes) │ (param_nodes) │
├──────────────────────┼──────────────────────────────────────────┤
│ - 不依赖 FunctionParam│ - 直接或间接依赖 FunctionParam │
│ - 在函数定义时求值 │ - 每次调用时重新求值 │
│ - 作为闭包的捕获变量 │ - 序列化到闭包的 body 中 │
└──────────────────────┴──────────────────────────────────────────┘
```
#### 步骤 4:创建 CreateClosure 节点
```python
closure_node = {
"class_type": "__CreateClosure__",
"inputs": {
"body": json.dumps(param_subgraph_copy), # 函数体的节点定义
"output": json.dumps(output_edge), # 输出边的引用
"side_effects": float("NaN") if has_side_effects else 0.0,
"capture_0": [source_id, source_idx], # 捕获的边
"capture_1": [...],
...
}
}
```
函数体中的边会被重写:
- 来自 `FunctionParam` 的边 → `["__param", param_index]`
- 来自捕获节点的边 → `["__capture", capture_index]`
#### 示例:函数转换过程
原始工作流:
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ LoadCheckpt │───→│ KSampler │───→│ FunctionEnd │
│ (1) │ │ (4) │ │ (5) │
└─────────────┘ └─────────────┘ └─────────────┘
↑
┌─────────────┐
│ FunctionParam│
│ (3) │
└─────────────┘
```
转换后:
```
┌─────────────┐ ┌─────────────────────────────────────────────┐
│ LoadCheckpt │───→│ CreateClosure (5) │
│ (1) │ │ body: {KSampler 的序列化定义} │
│ (capture) │ │ capture_0: [1, 0] │
└─────────────┘ │ output: ["__param", 0] 或 节点引用 │
└─────────────────────────────────────────────┘
```
---
## 函数调用与图展开
### Closure 数据结构 (`utils.py`)
```python
class Closure(TypedDict):
body: dict # 函数体节点的序列化定义
captures: list # 捕获变量的值列表
output: list # 输出边的规格 [node_id, slot] 或 ["__param", idx]
```
### CallClosure 节点 (`core.py`)
`CallClosure` 是函数调用的入口点。它使用 ComfyUI 的 `expand` 机制动态展开函数体:
```python
class CallClosure:
def run(self, closure, unique_id, **kwargs):
# 1. 检查递归深度(unique_id 长度表示嵌套层数)
if len(unique_id) > RECURSION_LIMIT:
raise RecursionError("...")
# 2. 收集参数
params = [kwargs[f"param_{i}"] for i in range(len(kwargs))]
# 3. 从闭包创建执行图
graph, output = create_graph_from_closure(closure, params, caller_unique_id=unique_id)
# 4. 如果图为空(纯参数/捕获引用),直接返回值
if len(graph) == 0:
return (output,)
# 5. 返回展开的图
return {"result": (output,), "expand": graph}
```
### 图创建算法 (`utils.py: create_graph_from_closure`)
```python
def create_graph_from_closure(closure: Closure, params, caller_unique_id=None):
body = copy.deepcopy(closure["body"])
graph = {}
def transform_input(spec):
if not isinstance(spec, list):
return spec
prefix = f"{caller_unique_id}_" if caller_unique_id else ""
# 普通节点引用:添加前缀
if spec[0] != "__capture" and spec[0] != "__param":
return [f"{prefix}{spec[0]}", spec[1]]
# 参数引用:直接替换为参数值
if spec[0] == "__param":
value = params[spec[1]]
# 捕获引用:直接替换为捕获值
else:
value = closure["captures"][spec[1]]
# 如果值是列表,需要包装成 RecoverList 节点
if isinstance(value, list):
recover_id = f"{prefix}{spec[0]}_{spec[1]}"
graph[recover_id] = {
"inputs": {"values": (value,)}, # 元组包装防止被解析为边
"class_type": "__RecoverList__",
}
return [recover_id, 0]
else:
return value
# 转换所有节点的输入
for node_id, node_data in body.items():
for key in node_data["inputs"].keys():
node_data["inputs"][key] = transform_input(node_data["inputs"][key])
node_data["override_display_id"] = node_id # 保持原始显示 ID
graph[f"{caller_unique_id}_{node_id}"] = node_data
output = transform_input(closure["output"])
return graph, output
```
### unique_id 的作用
ComfyUI 的每个节点有唯一的 `unique_id`。在函数调用时:
- 展开的节点 ID 格式:`{caller_unique_id}_{original_node_id}`
- 嵌套调用时:`{outer_id}_{inner_id}_{node_id}`
- 通过 `unique_id` 长度检测递归深度
---
## 协程机制与高阶函数
### 设计动机
高阶函数(如 `Map`、`Fold`)需要多次调用用户提供的函数。由于 ComfyUI 的执行模型是基于图的单次遍历,无法直接实现循环。
解决方案:**Python 协程 + 图展开**
### CoroutineNodeBase (`core.py`)
所有高阶函数继承自 `CoroutineNodeBase`:
```python
class CoroutineNodeBase:
def run_coroutine(self, **kwargs):
"""子类实现:生成器函数,通过 yield 调用闭包"""
raise NotImplementedError()
yield # 使其成为生成器
def run(self, unique_id, **kwargs):
coroutine = self.run_coroutine(**kwargs)
return {
"result": ([f"{unique_id}_0", 0],),
"expand": {
f"{unique_id}_0": {
"inputs": {
"return_value": None,
"coroutine": coroutine,
},
"class_type": "__IntermidiateCoroutine__",
}
},
}
```
### IntermidiateCoroutine (`core.py`)
协程的中间状态管理器,负责驱动生成器执行:
```python
class IntermidiateCoroutine:
def run(self, return_value, coroutine, unique_id):
(base_id, index) = unique_id.rsplit("_", 1)
graph = {}
while len(graph) == 0:
try:
# 将上一次调用的结果发送给协程
(closure, params) = coroutine.send(return_value)
except StopIteration as e:
# 协程结束,返回最终结果
return (e.value,)
index = int(index) + 1
next_node_id = f"{base_id}_{index}"
if index > COROUTINE_LIMIT:
raise RecursionError("Coroutine execution limit exceeded")
# 展开闭包调用
graph, output = create_graph_from_closure(closure, params, caller_unique_id=unique_id)
if len(graph) == 0:
# 如果不需要展开图,直接使用结果继续循环
return_value = output
# 返回展开的图和下一个中间协程节点
return {
"result": ([next_node_id, 0],),
"expand": {
**graph,
next_node_id: {
"inputs": {
"return_value": output,
"coroutine": coroutine,
},
"class_type": "__IntermidiateCoroutine__",
},
},
}
```
### 执行流程示例:Map
```python
class Map(CoroutineNodeBase):
def run_coroutine(self, function, items):
results = []
for item in items:
val = yield (function, [item]) # 暂停,请求调用 function(item)
results.append(val)
return results
```
执行 `Map(f, [a, b, c])` 的流程:
```
1. Map.run() 创建协程,展开 IntermidiateCoroutine_0
2. IC_0.run(None, coroutine):
- coroutine.send(None) → yield (f, [a])
- 展开 f(a) 的图 + IC_1
3. IC_1.run(result_a, coroutine):
- coroutine.send(result_a) → yield (f, [b])
- 展开 f(b) 的图 + IC_2
4. IC_2.run(result_b, coroutine):
- coroutine.send(result_b) → yield (f, [c])
- 展开 f(c) 的图 + IC_3
5. IC_3.run(result_c, coroutine):
- coroutine.send(result_c) → StopIteration([result_a, result_b, result_c])
- 返回最终结果
```
---
## 副作用处理
### Sow/Reap 机制 (`side_effects.py`)
全局存储用于跨节点收集值:
```python
reap_storage = {} # 全局存储:{tag: [values]}
class Sow:
def run(self, signal, value, tag):
if tag not in reap_storage:
reap_storage[tag] = []
reap_storage[tag].append(value)
return (signal,) # 传递信号以保证执行顺序
class Reap:
def run(self, signal, tag):
values = reap_storage.get(tag, [])
reap_storage[tag] = [] # 清空
return (values,)
```
### 禁用缓存
副作用节点通过 `IS_CHANGED` 返回 `float("NaN")` 来禁用 ComfyUI 的缓存:
```python
@classmethod
def IS_CHANGED(cls, signal, value, tag):
return float("NaN") # NaN != NaN,总是被视为已更改
```
### Inspect 节点
`Inspect` 节点用于在函数体内查看中间值,同时不破坏惰性求值:
```python
class Inspect:
def run(self, signal, value):
return (
signal, # 第一个输出:传递信号
ExecutionBlocker("..."), # 第二个输出:阻止直接使用
)
```
`transform.py` 中的 `_preprocess_inspect_nodes` 会将连接到 `Inspect` 第二个输出的叶子节点吸收到 `InspectImpl` 的 `body` 中,在运行时动态展开。
---
## 远程函数调用
### 设计目标
远程函数调用允许在其他 ComfyUI 实例上执行函数体,实现:
- 流水线并行:将工作流的不同阶段分布到不同 GPU/机器
- 避免模型反复装卸:在专用 worker 上保持大模型常驻
### CallClosureRemote (`remote.py`)
```python
class CallClosureRemote:
async def run(self, closure, base_url, timeout, use_shared_memory, **kwargs):
params = [kwargs[f"param_{i}"] for i in range(len(kwargs))]
client = ComfyClient(server_base=base_url, timeout=timeout)
workflow = create_remote_workflow_from_closure(closure, params, use_shared_memory)
client_response = await client.run_workflow(workflow)
output = client_response.get("__output__", None)
return (deserialize(output[0]),)
```
与本地 `CallClosure` 的关键区别:
1. 不使用 `expand` 机制,而是通过 HTTP/WebSocket 提交完整工作流
2. 参数和返回值需要序列化/反序列化
3. 使用 `async` 异步执行
### 远程工作流生成 (`utils.py: create_remote_workflow_from_closure`)
```python
def create_remote_workflow_from_closure(closure, params, use_shared_memory=False):
body = copy.deepcopy(closure["body"])
graph = {}
def transform_input(spec):
if spec[0] == "__param" or spec[0] == "__capture":
# 创建 Deserialize 节点来恢复序列化的值
deserialize_node_id = f"deserialize_{spec[0]}_{spec[1]}"
value = params[spec[1]] if spec[0] == "__param" else closure["captures"][spec[1]]
serialized_value, _ = serialize(value, use_shared_memory)
graph[deserialize_node_id] = {
"inputs": {"data": serialized_value},
"class_type": "__Deserialize__",
}
return [deserialize_node_id, 0]
return spec
# 转换所有输入引用
for node_id, node_data in body.items():
for key in node_data["inputs"]:
node_data["inputs"][key] = transform_input(node_data["inputs"][key])
graph[node_id] = node_data
# 添加输出序列化节点
output = transform_input(closure["output"])
graph["__output__"] = {
"inputs": {"data": output, "use_shared_memory": use_shared_memory},
"class_type": "__Serialize__",
"_meta": {"title": "%__output__%"},
}
return graph
```
### 捕获模式与远程调用
| 方面 | 本地调用 (`capture=True`) | 远程调用 (`capture=False`) |
|------|--------------------------|---------------------------|
| 模型加载 | 在函数外加载,被捕获复用 | 必须在函数体内加载 |
| 节点 ID | 每次调用重新分配前缀 | 保持原始 ID |
| 缓存 | 无效(ID 变化) | 有效(ID 不变) |
| 序列化 | 不需要 | 参数/返回值必须可序列化 |
禁用捕获时,函数体的所有节点(包括模型加载器)都会被序列化到 `body` 中,在远程执行时直接运行。由于节点 ID 不变,远程 ComfyUI 可以正常缓存已加载的模型。
### 序列化与共享内存
参见 [类型系统与工具函数](#类型系统与工具函数) 中的序列化支持部分。
当 `use_shared_memory=True` 且两个 ComfyUI 实例在同一机器上时,PyTorch 张量通过共享内存传递:
```python
if self.use_shared_memory:
obj.share_memory_()
storage = obj.untyped_storage()
manager_handle, handle, size = storage._share_filename_cpu_()
return {
"__type__": "torch.Tensor_shared",
"manager_handle": manager_handle,
"handle": handle,
# ... 其他元数据
}
```
接收端通过 `torch.UntypedStorage._new_shared_filename_cpu()` 重建张量,避免数据复制。
---
## 类型系统与工具函数
### AnyType (`utils.py`)
ComfyUI 的类型系统要求输入输出类型匹配。`AnyType` 通过重写 `__ne__` 实现通配:
```python
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False # 与任何类型比较都返回"相等"
```
### ContainsDynamicDict (`utils.py`)
支持动态数量的输入参数(如 `param_0`, `param_1`, ...):
```python
class ContainsDynamicDict(dict):
"""
如果键以数字结尾且前缀匹配 _dynamic 模式,
则动态返回对应的类型定义。
"""
def __contains__(self, key):
return any(
key.startswith(prefix) and key[len(prefix):].isdigit()
for prefix in self._dynamic_prefixes
) or super().__contains__(key)
```
### 序列化支持 (`utils.py`)
用于远程执行的序列化/反序列化:
```python
class WhitelistEncoder(json.JSONEncoder):
"""支持序列化:set, tuple, bytes, numpy.ndarray, torch.Tensor, PIL.Image"""
def default(self, obj):
if isinstance(obj, torch.Tensor):
if self.use_shared_memory:
# 共享内存优化:只传递元数据
obj.share_memory_()
storage = obj.untyped_storage()
manager_handle, handle, size = storage._share_filename_cpu_()
return {"__type__": "torch.Tensor_shared", ...}
else:
# 普通序列化
buffer = io.BytesIO()
torch.save(obj, buffer)
return {"__type__": "torch.Tensor", "data": base64.b64encode(...)}
```
---
## 技术细节与限制
### 递归与循环限制
```python
RECURSION_LIMIT = 50 # CallClosure 的最大嵌套深度
COROUTINE_LIMIT = 100 # 单个协程的最大迭代次数
```
限制通过检查 `unique_id` 的长度或迭代计数实现。
### ComfyUI expand 机制
ComfyUI 支持节点返回 `{"result": ..., "expand": graph}` 格式来动态添加节点。这是实现函数调用和协程迭代的基础。
### 节点 ID 命名约定
- 内部节点使用双下划线前缀:`__FunctionParam__`、`__CreateClosure__` 等
- 用户可见节点无前缀:`CallClosure`、`HighLevelMap` 等
- 展开的节点 ID:`{caller_id}_{original_id}`
### 已知限制
1. **依赖 ComfyUI 内部 API**:`PromptExecutor.execute` 的钩子、`expand` 返回格式等可能在 ComfyUI 更新后失效
2. **列表类型**:必须使用 Basic Data Handling 的 `LIST` 类型,不能使用 ComfyUI 原生的 Data List
3. **缓存失效**:副作用节点会禁用缓存,影响执行性能
4. **调试困难**:展开的图在 ComfyUI UI 中不可见,需要使用 `Inspect` 节点调试
### 扩展开发
创建自定义高阶函数,继承 `CoroutineNodeBase` 并实现 `run_coroutine`:
```python
class MyHighOrderNode(CoroutineNodeBase):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"function": ("CLOSURE",),
# 其他输入...
},
"hidden": {"unique_id": "UNIQUE_ID"},
}
CATEGORY = "my_category"
RETURN_TYPES = (AnyType("*"),)
def run_coroutine(self, function, **kwargs):
# 使用 yield (closure, params) 调用函数
result = yield (function, [arg1, arg2])
# 处理结果...
return final_result
```
---
## 参考资料
- [ComfyUI 源码](https://github.com/comfyanonymous/ComfyUI)
- [Basic Data Handling](https://github.com/StableLlama/ComfyUI-basic_data_handling)
- [用户使用指南](./usage.md)
- [节点参考](./node-reference.md)