668 lines
24 KiB
Markdown
668 lines
24 KiB
Markdown
# 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)
|