Files

293 lines
11 KiB
Python

from typing import TypedDict
import copy
import json
import io
import base64
import numpy as np
import torch
from PIL import Image
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
class ContainsDynamicDict(dict):
"""
A custom dictionary that dynamically returns values for keys based on a pattern.
- If a key in the passed dictionary has a value with `{"_dynamic": "number"}` in the tuple's second position,
then any other key starting with the same string and ending with a number will return that value.
- For other keys, normal dictionary lookup behavior applies.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Store prefixes associated with `_dynamic` values for efficient lookup
self._dynamic_prefixes = {
key.rstrip("0123456789"): value
for key, value in self.items()
if isinstance(value, tuple) and len(value) > 1 and value[1].get("_dynamic") == "number"
}
def __contains__(self, key):
# Check if key matches a dynamically handled prefix or exists normally
return any(key.startswith(prefix) and key[len(prefix) :].isdigit() for prefix in self._dynamic_prefixes) or super().__contains__(
key
)
def __getitem__(self, key):
# Dynamically return the value for keys matching a `prefix<number>` pattern
for prefix, value in self._dynamic_prefixes.items():
if key.startswith(prefix) and key[len(prefix) :].isdigit():
return value
# Fallback to normal dictionary behavior for other keys
return super().__getitem__(key)
class Closure(TypedDict):
body: dict
captures: list
output: list
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
# Return normal spec if not a capture or param
prefix = f"{caller_unique_id}_" if caller_unique_id is not None else ""
if spec[0] != "__capture" and spec[0] != "__param":
return [f"{prefix}{spec[0]}", spec[1]]
if spec[0] == "__param":
if spec[1] >= len(params):
raise IndexError(f"Parameter index {spec[1]} out of range for function call. Only {len(params)} parameters were provided.")
value = params[spec[1]]
else:
value = closure["captures"][spec[1]]
if isinstance(value, list):
recover_id = f"{prefix}{spec[0]}_{spec[1]}"
if recover_id not in graph:
graph[recover_id] = {
"inputs": {
# Wrap the list in a tuple so it's treated as a literal, not a link
"values": (value,),
},
"class_type": "__RecoverList__",
}
return [recover_id, 0]
else:
return value
for node_id, node_data in body.items():
inputs = node_data["inputs"]
for key in inputs.keys():
inputs[key] = transform_input(inputs[key])
node_data["override_display_id"] = node_id
graph[f"{caller_unique_id}_{node_id}"] = node_data
output = transform_input(closure["output"])
return graph, output
def create_remote_workflow_from_closure(closure, params, use_shared_memory=False):
body = copy.deepcopy(closure["body"])
graph = {}
def transform_input(spec):
if not isinstance(spec, list):
return spec
# Return normal spec if not a capture or param
if spec[0] != "__capture" and spec[0] != "__param":
return spec
# Check if we have already created a deserialize node for this spec
deserialize_node_id = f"deserialize_{spec[0]}_{spec[1]}"
if deserialize_node_id in graph:
return [deserialize_node_id, 0]
# Try to create a deserialize node
if spec[0] == "__param":
if spec[1] >= len(params):
raise IndexError(f"Parameter index {spec[1]} out of range for function call. Only {len(params)} parameters were provided.")
value = params[spec[1]]
else:
value = closure["captures"][spec[1]]
try:
serialized_value, _ = serialize(value, use_shared_memory=use_shared_memory)
graph[deserialize_node_id] = {
"inputs": {
"data": serialized_value,
},
"class_type": "__Deserialize__",
}
return [deserialize_node_id, 0]
except TypeError as e:
if spec[0] == "__capture":
raise ValueError(
f"Cannot serialize captured value at index {spec[1]}. "
"Captured values must be serializable to be used in remote execution. "
"Consider disabling capture for this function to initialize the value remotely. "
f"Error: {e}"
)
else:
raise ValueError(
f"Cannot serialize parameter at index {spec[1]}. "
"Parameters must be serializable to be used in remote execution. "
f"Error: {e}"
)
for node_id, node_data in body.items():
inputs = node_data["inputs"]
for key in inputs.keys():
inputs[key] = transform_input(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
class WhitelistEncoder(json.JSONEncoder):
def __init__(self, use_shared_memory=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self.use_shared_memory = use_shared_memory
self.shared_tensors = set()
def default(self, obj):
# 区分 set 和 tuple
if isinstance(obj, set):
return {"__type__": "set", "data": list(obj)}
if isinstance(obj, tuple):
return {"__type__": "tuple", "data": list(obj)}
if isinstance(obj, bytes):
return {"__type__": "bytes", "data": base64.b64encode(obj).decode("utf-8")}
if isinstance(obj, np.ndarray):
buffer = io.BytesIO()
np.save(buffer, obj, allow_pickle=False)
return {
"__type__": "numpy.ndarray",
"data": base64.b64encode(buffer.getvalue()).decode("utf-8"),
}
if isinstance(obj, torch.Tensor):
obj = obj.cpu().contiguous()
if self.use_shared_memory:
# 启用共享内存优化
obj.share_memory_() # 确保移动到共享内存
storage = obj.untyped_storage()
manager_handle_bytes, handle_bytes, storage_size = storage._share_filename_cpu_()
self.shared_tensors.add(obj)
return {
"__type__": "torch.Tensor_shared",
"manager_handle": manager_handle_bytes.decode("utf-8"),
"handle": handle_bytes.decode("utf-8"),
"storage_size": storage_size,
"offset": obj.storage_offset(),
"shape": list(obj.shape),
"strides": list(obj.stride()),
"dtype": str(obj.dtype),
"requires_grad": obj.requires_grad,
}
else:
# 普通序列化(回退)
buffer = io.BytesIO()
torch.save(obj, buffer) # 确保 CPU
return {
"__type__": "torch.Tensor",
"data": base64.b64encode(buffer.getvalue()).decode("utf-8"),
}
if isinstance(obj, Image.Image):
buffer = io.BytesIO()
obj.save(buffer, format="PNG")
return {
"__type__": "PIL.Image",
"data": base64.b64encode(buffer.getvalue()).decode("utf-8"),
}
raise TypeError(
f"Object of type '{type(obj).__name__}' is not serializable. "
"Only basic types, set, tuple, bytes, numpy.ndarray, "
"torch.Tensor, and PIL.Image are allowed."
)
def whitelist_decoder(dct):
"""
用于 json.load/loads 的 object_hook,用于恢复自定义类型。
"""
if "__type__" in dct:
obj_type = dct["__type__"]
data = dct.get("data")
if obj_type == "set":
return set(data)
if obj_type == "tuple":
return tuple(data)
if obj_type == "bytes":
return base64.b64decode(data)
if obj_type == "numpy.ndarray":
buffer = io.BytesIO(base64.b64decode(data))
return np.load(buffer, allow_pickle=True)
if obj_type == "torch.Tensor":
buffer = io.BytesIO(base64.b64decode(data))
return torch.load(buffer)
if obj_type == "torch.Tensor_shared":
# 重建共享 Tensor
manager_handle_bytes = dct["manager_handle"].encode("utf-8")
handle_bytes = dct["handle"].encode("utf-8")
storage_size = dct["storage_size"]
offset = dct["offset"]
shape = tuple(dct["shape"])
strides = tuple(dct["strides"])
dtype_str = dct["dtype"].split(".")[-1] # 如 'float32'
dtype = getattr(torch, dtype_str)
requires_grad = dct["requires_grad"]
# 重建存储(attach 到共享内存)
storage = torch.UntypedStorage._new_shared_filename_cpu(manager_handle_bytes, handle_bytes, storage_size)
# 创建 Tensor 视图
tensor = torch.empty(shape, dtype=dtype).set_(storage, offset, shape, strides)
tensor.requires_grad_(requires_grad)
return tensor
if obj_type == "PIL.Image":
buffer = io.BytesIO(base64.b64decode(data))
img = Image.open(buffer)
return img.copy()
return dct
def serialize(obj, use_shared_memory=False):
"""
将对象序列化为 JSON 字符串。
仅支持基本类型、set、tuple、bytes、numpy.ndarray、torch.Tensor 和 PIL.Image。
use_shared_memory: 如果为 True,且 obj 是 CPU torch.Tensor,则使用共享内存优化(传输元数据)。
"""
encoder = WhitelistEncoder(use_shared_memory=use_shared_memory)
return encoder.encode(obj), encoder.shared_tensors
def deserialize(json_str):
"""
从 JSON 字符串反序列化对象。
"""
return json.loads(json_str, object_hook=whitelist_decoder)