comfy-env 0.0.64__py3-none-any.whl → 0.0.66__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comfy_env/__init__.py +70 -122
- comfy_env/cli.py +78 -7
- comfy_env/config/__init__.py +19 -0
- comfy_env/config/parser.py +151 -0
- comfy_env/config/types.py +64 -0
- comfy_env/install.py +83 -361
- comfy_env/isolation/__init__.py +9 -0
- comfy_env/isolation/wrap.py +351 -0
- comfy_env/nodes.py +2 -2
- comfy_env/pixi/__init__.py +48 -0
- comfy_env/pixi/core.py +356 -0
- comfy_env/{resolver.py → pixi/resolver.py} +1 -14
- comfy_env/prestartup.py +60 -0
- comfy_env/templates/comfy-env-instructions.txt +30 -87
- comfy_env/templates/comfy-env.toml +68 -136
- comfy_env/workers/__init__.py +21 -32
- comfy_env/workers/base.py +1 -1
- comfy_env/workers/{torch_mp.py → mp.py} +47 -14
- comfy_env/workers/{venv.py → subprocess.py} +405 -441
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
- comfy_env-0.0.66.dist-info/RECORD +34 -0
- comfy_env/decorator.py +0 -700
- comfy_env/env/__init__.py +0 -47
- comfy_env/env/config.py +0 -201
- comfy_env/env/config_file.py +0 -740
- comfy_env/env/manager.py +0 -636
- comfy_env/env/security.py +0 -267
- comfy_env/ipc/__init__.py +0 -55
- comfy_env/ipc/bridge.py +0 -476
- comfy_env/ipc/protocol.py +0 -265
- comfy_env/ipc/tensor.py +0 -371
- comfy_env/ipc/torch_bridge.py +0 -401
- comfy_env/ipc/transport.py +0 -318
- comfy_env/ipc/worker.py +0 -221
- comfy_env/isolation.py +0 -310
- comfy_env/pixi.py +0 -760
- comfy_env/stub_imports.py +0 -270
- comfy_env/stubs/__init__.py +0 -1
- comfy_env/stubs/comfy/__init__.py +0 -6
- comfy_env/stubs/comfy/model_management.py +0 -58
- comfy_env/stubs/comfy/utils.py +0 -29
- comfy_env/stubs/folder_paths.py +0 -71
- comfy_env/workers/pool.py +0 -241
- comfy_env-0.0.64.dist-info/RECORD +0 -48
- /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
- /comfy_env/{env → pixi}/platform/__init__.py +0 -0
- /comfy_env/{env → pixi}/platform/base.py +0 -0
- /comfy_env/{env → pixi}/platform/darwin.py +0 -0
- /comfy_env/{env → pixi}/platform/linux.py +0 -0
- /comfy_env/{env → pixi}/platform/windows.py +0 -0
- /comfy_env/{registry.py → pixi/registry.py} +0 -0
- /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
SubprocessWorker - Cross-venv isolation using persistent subprocess + socket IPC.
|
|
3
3
|
|
|
4
4
|
This worker supports calling functions in a different Python environment:
|
|
5
|
-
- Uses subprocess
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
- ~
|
|
5
|
+
- Uses a persistent subprocess to avoid spawn overhead
|
|
6
|
+
- Socket-based IPC for commands/responses
|
|
7
|
+
- Transfers tensors via torch.save/load over socket
|
|
8
|
+
- ~50-100ms overhead per call
|
|
9
9
|
|
|
10
10
|
Use this when you need:
|
|
11
11
|
- Different PyTorch version
|
|
@@ -13,16 +13,17 @@ Use this when you need:
|
|
|
13
13
|
- Different Python version
|
|
14
14
|
|
|
15
15
|
Example:
|
|
16
|
-
worker =
|
|
16
|
+
worker = SubprocessWorker(
|
|
17
17
|
python="/path/to/other/venv/bin/python",
|
|
18
18
|
working_dir="/path/to/code",
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
# Call a
|
|
22
|
-
result = worker.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
# Call a method by module path
|
|
22
|
+
result = worker.call_method(
|
|
23
|
+
module_name="my_module",
|
|
24
|
+
class_name="MyClass",
|
|
25
|
+
method_name="process",
|
|
26
|
+
kwargs={"image": my_tensor},
|
|
26
27
|
)
|
|
27
28
|
"""
|
|
28
29
|
|
|
@@ -185,7 +186,142 @@ class SocketTransport:
|
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
# =============================================================================
|
|
188
|
-
# Serialization
|
|
189
|
+
# Shared Memory Serialization
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
from multiprocessing import shared_memory as shm
|
|
193
|
+
import numpy as np
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _to_shm(obj, registry, visited=None):
|
|
197
|
+
"""
|
|
198
|
+
Serialize object to shared memory. Returns JSON-safe metadata.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
obj: Object to serialize
|
|
202
|
+
registry: List to track SharedMemory objects for cleanup
|
|
203
|
+
visited: Dict tracking already-serialized objects (cycle detection)
|
|
204
|
+
"""
|
|
205
|
+
if visited is None:
|
|
206
|
+
visited = {}
|
|
207
|
+
|
|
208
|
+
obj_id = id(obj)
|
|
209
|
+
if obj_id in visited:
|
|
210
|
+
return visited[obj_id]
|
|
211
|
+
|
|
212
|
+
t = type(obj).__name__
|
|
213
|
+
|
|
214
|
+
# numpy array → direct shared memory
|
|
215
|
+
if t == 'ndarray':
|
|
216
|
+
arr = np.ascontiguousarray(obj)
|
|
217
|
+
block = shm.SharedMemory(create=True, size=arr.nbytes)
|
|
218
|
+
np.ndarray(arr.shape, arr.dtype, buffer=block.buf)[:] = arr
|
|
219
|
+
registry.append(block)
|
|
220
|
+
result = {"__shm_np__": block.name, "shape": list(arr.shape), "dtype": str(arr.dtype)}
|
|
221
|
+
visited[obj_id] = result
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
# torch.Tensor → convert to numpy → shared memory
|
|
225
|
+
if t == 'Tensor':
|
|
226
|
+
arr = obj.detach().cpu().numpy()
|
|
227
|
+
return _to_shm(arr, registry, visited)
|
|
228
|
+
|
|
229
|
+
# trimesh.Trimesh → vertices + faces arrays → shared memory
|
|
230
|
+
if t == 'Trimesh':
|
|
231
|
+
verts = np.ascontiguousarray(obj.vertices, dtype=np.float64)
|
|
232
|
+
faces = np.ascontiguousarray(obj.faces, dtype=np.int64)
|
|
233
|
+
|
|
234
|
+
v_block = shm.SharedMemory(create=True, size=verts.nbytes)
|
|
235
|
+
np.ndarray(verts.shape, verts.dtype, buffer=v_block.buf)[:] = verts
|
|
236
|
+
registry.append(v_block)
|
|
237
|
+
|
|
238
|
+
f_block = shm.SharedMemory(create=True, size=faces.nbytes)
|
|
239
|
+
np.ndarray(faces.shape, faces.dtype, buffer=f_block.buf)[:] = faces
|
|
240
|
+
registry.append(f_block)
|
|
241
|
+
|
|
242
|
+
result = {
|
|
243
|
+
"__shm_trimesh__": True,
|
|
244
|
+
"v_name": v_block.name, "v_shape": list(verts.shape),
|
|
245
|
+
"f_name": f_block.name, "f_shape": list(faces.shape),
|
|
246
|
+
}
|
|
247
|
+
visited[obj_id] = result
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
# Path → string
|
|
251
|
+
from pathlib import PurePath
|
|
252
|
+
if isinstance(obj, PurePath):
|
|
253
|
+
return str(obj)
|
|
254
|
+
|
|
255
|
+
# dict
|
|
256
|
+
if isinstance(obj, dict):
|
|
257
|
+
result = {k: _to_shm(v, registry, visited) for k, v in obj.items()}
|
|
258
|
+
visited[obj_id] = result
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
# list/tuple
|
|
262
|
+
if isinstance(obj, list):
|
|
263
|
+
result = [_to_shm(v, registry, visited) for v in obj]
|
|
264
|
+
visited[obj_id] = result
|
|
265
|
+
return result
|
|
266
|
+
if isinstance(obj, tuple):
|
|
267
|
+
result = [_to_shm(v, registry, visited) for v in obj] # JSON doesn't have tuples
|
|
268
|
+
visited[obj_id] = result
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
# primitives pass through
|
|
272
|
+
return obj
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _from_shm(obj, unlink=True):
|
|
276
|
+
"""Reconstruct object from shared memory metadata."""
|
|
277
|
+
if not isinstance(obj, dict):
|
|
278
|
+
if isinstance(obj, list):
|
|
279
|
+
return [_from_shm(v, unlink) for v in obj]
|
|
280
|
+
return obj
|
|
281
|
+
|
|
282
|
+
# numpy array
|
|
283
|
+
if "__shm_np__" in obj:
|
|
284
|
+
block = shm.SharedMemory(name=obj["__shm_np__"])
|
|
285
|
+
arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
|
|
286
|
+
block.close()
|
|
287
|
+
if unlink:
|
|
288
|
+
block.unlink()
|
|
289
|
+
return arr
|
|
290
|
+
|
|
291
|
+
# trimesh
|
|
292
|
+
if "__shm_trimesh__" in obj:
|
|
293
|
+
import trimesh
|
|
294
|
+
v_block = shm.SharedMemory(name=obj["v_name"])
|
|
295
|
+
verts = np.ndarray(tuple(obj["v_shape"]), dtype=np.float64, buffer=v_block.buf).copy()
|
|
296
|
+
v_block.close()
|
|
297
|
+
if unlink:
|
|
298
|
+
v_block.unlink()
|
|
299
|
+
|
|
300
|
+
f_block = shm.SharedMemory(name=obj["f_name"])
|
|
301
|
+
faces = np.ndarray(tuple(obj["f_shape"]), dtype=np.int64, buffer=f_block.buf).copy()
|
|
302
|
+
f_block.close()
|
|
303
|
+
if unlink:
|
|
304
|
+
f_block.unlink()
|
|
305
|
+
|
|
306
|
+
return trimesh.Trimesh(vertices=verts, faces=faces, process=False)
|
|
307
|
+
|
|
308
|
+
# regular dict - recurse
|
|
309
|
+
return {k: _from_shm(v, unlink) for k, v in obj.items()}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _cleanup_shm(registry):
|
|
313
|
+
"""Unlink all shared memory blocks in registry."""
|
|
314
|
+
for block in registry:
|
|
315
|
+
try:
|
|
316
|
+
block.close()
|
|
317
|
+
block.unlink()
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
registry.clear()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# =============================================================================
|
|
324
|
+
# Legacy Serialization helpers (for isolated objects)
|
|
189
325
|
# =============================================================================
|
|
190
326
|
|
|
191
327
|
|
|
@@ -204,6 +340,11 @@ def _serialize_for_ipc(obj, visited=None):
|
|
|
204
340
|
if obj_id in visited:
|
|
205
341
|
return visited[obj_id] # Return cached serialized result
|
|
206
342
|
|
|
343
|
+
# Handle Path objects - convert to string for JSON serialization
|
|
344
|
+
from pathlib import PurePath
|
|
345
|
+
if isinstance(obj, PurePath):
|
|
346
|
+
return str(obj)
|
|
347
|
+
|
|
207
348
|
# Check if this is a custom object with broken module path
|
|
208
349
|
if (hasattr(obj, '__dict__') and
|
|
209
350
|
hasattr(obj, '__class__') and
|
|
@@ -252,104 +393,6 @@ def _serialize_for_ipc(obj, visited=None):
|
|
|
252
393
|
return obj
|
|
253
394
|
|
|
254
395
|
|
|
255
|
-
# Worker script template - minimal, runs in target venv
|
|
256
|
-
_WORKER_SCRIPT = '''
|
|
257
|
-
import sys
|
|
258
|
-
import os
|
|
259
|
-
import json
|
|
260
|
-
import traceback
|
|
261
|
-
from types import SimpleNamespace
|
|
262
|
-
|
|
263
|
-
# On Windows, add DLL directories for proper library loading
|
|
264
|
-
if sys.platform == "win32" and hasattr(os, "add_dll_directory"):
|
|
265
|
-
_host_python_dir = os.environ.get("COMFYUI_HOST_PYTHON_DIR")
|
|
266
|
-
if _host_python_dir:
|
|
267
|
-
try:
|
|
268
|
-
os.add_dll_directory(_host_python_dir)
|
|
269
|
-
_dlls_dir = os.path.join(_host_python_dir, "DLLs")
|
|
270
|
-
if os.path.isdir(_dlls_dir):
|
|
271
|
-
os.add_dll_directory(_dlls_dir)
|
|
272
|
-
except Exception:
|
|
273
|
-
pass
|
|
274
|
-
_pixi_library_bin = os.environ.get("COMFYUI_PIXI_LIBRARY_BIN")
|
|
275
|
-
if _pixi_library_bin:
|
|
276
|
-
try:
|
|
277
|
-
os.add_dll_directory(_pixi_library_bin)
|
|
278
|
-
except Exception:
|
|
279
|
-
pass
|
|
280
|
-
|
|
281
|
-
def _deserialize_isolated_objects(obj):
|
|
282
|
-
"""Reconstruct objects serialized with __isolated_object__ marker."""
|
|
283
|
-
if isinstance(obj, dict):
|
|
284
|
-
if obj.get("__isolated_object__"):
|
|
285
|
-
# Reconstruct as SimpleNamespace (supports .attr access)
|
|
286
|
-
attrs = {k: _deserialize_isolated_objects(v) for k, v in obj.get("__attrs__", {}).items()}
|
|
287
|
-
ns = SimpleNamespace(**attrs)
|
|
288
|
-
ns.__class_name__ = obj.get("__class_name__", "Unknown")
|
|
289
|
-
return ns
|
|
290
|
-
return {k: _deserialize_isolated_objects(v) for k, v in obj.items()}
|
|
291
|
-
elif isinstance(obj, list):
|
|
292
|
-
return [_deserialize_isolated_objects(v) for v in obj]
|
|
293
|
-
elif isinstance(obj, tuple):
|
|
294
|
-
return tuple(_deserialize_isolated_objects(v) for v in obj)
|
|
295
|
-
return obj
|
|
296
|
-
|
|
297
|
-
def main():
|
|
298
|
-
# Read request from file
|
|
299
|
-
request_path = sys.argv[1]
|
|
300
|
-
response_path = sys.argv[2]
|
|
301
|
-
|
|
302
|
-
with open(request_path, 'r') as f:
|
|
303
|
-
request = json.load(f)
|
|
304
|
-
|
|
305
|
-
try:
|
|
306
|
-
# Setup paths
|
|
307
|
-
for p in request.get("sys_path", []):
|
|
308
|
-
if p not in sys.path:
|
|
309
|
-
sys.path.insert(0, p)
|
|
310
|
-
|
|
311
|
-
# Import torch for tensor I/O
|
|
312
|
-
import torch
|
|
313
|
-
|
|
314
|
-
# Load inputs
|
|
315
|
-
inputs_path = request.get("inputs_path")
|
|
316
|
-
if inputs_path:
|
|
317
|
-
inputs = torch.load(inputs_path, weights_only=False)
|
|
318
|
-
inputs = _deserialize_isolated_objects(inputs)
|
|
319
|
-
else:
|
|
320
|
-
inputs = {}
|
|
321
|
-
|
|
322
|
-
# Import and call function
|
|
323
|
-
module_name = request["module"]
|
|
324
|
-
func_name = request["func"]
|
|
325
|
-
|
|
326
|
-
module = __import__(module_name, fromlist=[func_name])
|
|
327
|
-
func = getattr(module, func_name)
|
|
328
|
-
|
|
329
|
-
result = func(**inputs)
|
|
330
|
-
|
|
331
|
-
# Save outputs
|
|
332
|
-
outputs_path = request.get("outputs_path")
|
|
333
|
-
if outputs_path:
|
|
334
|
-
torch.save(result, outputs_path)
|
|
335
|
-
|
|
336
|
-
response = {"status": "ok"}
|
|
337
|
-
|
|
338
|
-
except Exception as e:
|
|
339
|
-
response = {
|
|
340
|
-
"status": "error",
|
|
341
|
-
"error": str(e),
|
|
342
|
-
"traceback": traceback.format_exc(),
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
with open(response_path, 'w') as f:
|
|
346
|
-
json.dump(response, f)
|
|
347
|
-
|
|
348
|
-
if __name__ == "__main__":
|
|
349
|
-
main()
|
|
350
|
-
'''
|
|
351
|
-
|
|
352
|
-
|
|
353
396
|
def _get_shm_dir() -> Path:
|
|
354
397
|
"""Get shared memory directory for efficient tensor transfer."""
|
|
355
398
|
# Linux: /dev/shm is RAM-backed tmpfs
|
|
@@ -359,249 +402,6 @@ def _get_shm_dir() -> Path:
|
|
|
359
402
|
return Path(tempfile.gettempdir())
|
|
360
403
|
|
|
361
404
|
|
|
362
|
-
class VenvWorker(Worker):
|
|
363
|
-
"""
|
|
364
|
-
Worker using subprocess for cross-venv isolation.
|
|
365
|
-
|
|
366
|
-
This worker spawns a new Python process for each call, using
|
|
367
|
-
a different Python interpreter (from another venv). Tensors are
|
|
368
|
-
transferred via torch.save/load through shared memory.
|
|
369
|
-
|
|
370
|
-
For long-running workloads, consider using persistent mode which
|
|
371
|
-
keeps the subprocess alive between calls.
|
|
372
|
-
"""
|
|
373
|
-
|
|
374
|
-
def __init__(
|
|
375
|
-
self,
|
|
376
|
-
python: Union[str, Path],
|
|
377
|
-
working_dir: Optional[Union[str, Path]] = None,
|
|
378
|
-
sys_path: Optional[List[str]] = None,
|
|
379
|
-
env: Optional[Dict[str, str]] = None,
|
|
380
|
-
name: Optional[str] = None,
|
|
381
|
-
persistent: bool = True,
|
|
382
|
-
):
|
|
383
|
-
"""
|
|
384
|
-
Initialize the worker.
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
python: Path to Python executable in target venv.
|
|
388
|
-
working_dir: Working directory for subprocess.
|
|
389
|
-
sys_path: Additional paths to add to sys.path in subprocess.
|
|
390
|
-
env: Additional environment variables.
|
|
391
|
-
name: Optional name for logging.
|
|
392
|
-
persistent: If True, keep subprocess alive between calls (faster).
|
|
393
|
-
"""
|
|
394
|
-
self.python = Path(python)
|
|
395
|
-
self.working_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
396
|
-
self.sys_path = sys_path or []
|
|
397
|
-
self.extra_env = env or {}
|
|
398
|
-
self.name = name or f"VenvWorker({self.python.parent.parent.name})"
|
|
399
|
-
self.persistent = persistent
|
|
400
|
-
|
|
401
|
-
# Verify Python exists
|
|
402
|
-
if not self.python.exists():
|
|
403
|
-
raise FileNotFoundError(f"Python not found: {self.python}")
|
|
404
|
-
|
|
405
|
-
# Create temp directory for IPC files
|
|
406
|
-
self._temp_dir = Path(tempfile.mkdtemp(prefix='comfyui_venv_'))
|
|
407
|
-
self._shm_dir = _get_shm_dir()
|
|
408
|
-
|
|
409
|
-
# Persistent process state
|
|
410
|
-
self._process: Optional[subprocess.Popen] = None
|
|
411
|
-
self._shutdown = False
|
|
412
|
-
|
|
413
|
-
# Write worker script
|
|
414
|
-
self._worker_script = self._temp_dir / "worker.py"
|
|
415
|
-
self._worker_script.write_text(_WORKER_SCRIPT)
|
|
416
|
-
|
|
417
|
-
def call(
|
|
418
|
-
self,
|
|
419
|
-
func: Callable,
|
|
420
|
-
*args,
|
|
421
|
-
timeout: Optional[float] = None,
|
|
422
|
-
**kwargs
|
|
423
|
-
) -> Any:
|
|
424
|
-
"""
|
|
425
|
-
Execute a function - NOT SUPPORTED for VenvWorker.
|
|
426
|
-
|
|
427
|
-
VenvWorker cannot pickle arbitrary functions across venv boundaries.
|
|
428
|
-
Use call_module() instead to call functions by module path.
|
|
429
|
-
|
|
430
|
-
Raises:
|
|
431
|
-
NotImplementedError: Always.
|
|
432
|
-
"""
|
|
433
|
-
raise NotImplementedError(
|
|
434
|
-
f"{self.name}: VenvWorker cannot call arbitrary functions. "
|
|
435
|
-
f"Use call_module(module='...', func='...', **kwargs) instead."
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
def call_module(
|
|
439
|
-
self,
|
|
440
|
-
module: str,
|
|
441
|
-
func: str,
|
|
442
|
-
timeout: Optional[float] = None,
|
|
443
|
-
**kwargs
|
|
444
|
-
) -> Any:
|
|
445
|
-
"""
|
|
446
|
-
Call a function by module path in the isolated venv.
|
|
447
|
-
|
|
448
|
-
Args:
|
|
449
|
-
module: Module name (e.g., "my_package.my_module").
|
|
450
|
-
func: Function name within the module.
|
|
451
|
-
timeout: Timeout in seconds (None = 600s default).
|
|
452
|
-
**kwargs: Keyword arguments passed to the function.
|
|
453
|
-
Must be torch.save-compatible (tensors, dicts, etc.).
|
|
454
|
-
|
|
455
|
-
Returns:
|
|
456
|
-
Return value of module.func(**kwargs).
|
|
457
|
-
|
|
458
|
-
Raises:
|
|
459
|
-
WorkerError: If function raises an exception.
|
|
460
|
-
TimeoutError: If execution exceeds timeout.
|
|
461
|
-
"""
|
|
462
|
-
if self._shutdown:
|
|
463
|
-
raise RuntimeError(f"{self.name}: Worker has been shut down")
|
|
464
|
-
|
|
465
|
-
timeout = timeout or 600.0 # 10 minute default
|
|
466
|
-
|
|
467
|
-
# Create unique ID for this call
|
|
468
|
-
call_id = str(uuid.uuid4())[:8]
|
|
469
|
-
|
|
470
|
-
# Paths for IPC (use shm for tensors, temp for json)
|
|
471
|
-
inputs_path = self._shm_dir / f"comfyui_venv_{call_id}_in.pt"
|
|
472
|
-
outputs_path = self._shm_dir / f"comfyui_venv_{call_id}_out.pt"
|
|
473
|
-
request_path = self._temp_dir / f"request_{call_id}.json"
|
|
474
|
-
response_path = self._temp_dir / f"response_{call_id}.json"
|
|
475
|
-
|
|
476
|
-
try:
|
|
477
|
-
# Save inputs via torch.save (handles tensors natively)
|
|
478
|
-
# Serialize custom objects with broken __module__ paths first
|
|
479
|
-
import torch
|
|
480
|
-
if kwargs:
|
|
481
|
-
serialized_kwargs = _serialize_for_ipc(kwargs)
|
|
482
|
-
torch.save(serialized_kwargs, str(inputs_path))
|
|
483
|
-
|
|
484
|
-
# Build request
|
|
485
|
-
request = {
|
|
486
|
-
"module": module,
|
|
487
|
-
"func": func,
|
|
488
|
-
"sys_path": [str(self.working_dir)] + self.sys_path,
|
|
489
|
-
"inputs_path": str(inputs_path) if kwargs else None,
|
|
490
|
-
"outputs_path": str(outputs_path),
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
request_path.write_text(json.dumps(request))
|
|
494
|
-
|
|
495
|
-
# Build environment
|
|
496
|
-
env = os.environ.copy()
|
|
497
|
-
env.update(self.extra_env)
|
|
498
|
-
env["COMFYUI_ISOLATION_WORKER"] = "1"
|
|
499
|
-
|
|
500
|
-
# For conda/pixi environments, add lib dir to LD_LIBRARY_PATH (Linux)
|
|
501
|
-
lib_dir = self.python.parent.parent / "lib"
|
|
502
|
-
if lib_dir.is_dir():
|
|
503
|
-
existing = env.get("LD_LIBRARY_PATH", "")
|
|
504
|
-
env["LD_LIBRARY_PATH"] = f"{lib_dir}:{existing}" if existing else str(lib_dir)
|
|
505
|
-
|
|
506
|
-
# On Windows, pass host Python directory and pixi Library/bin for DLL loading
|
|
507
|
-
if sys.platform == "win32":
|
|
508
|
-
env["COMFYUI_HOST_PYTHON_DIR"] = str(Path(sys.executable).parent)
|
|
509
|
-
|
|
510
|
-
# For pixi environments with MKL, add Library/bin to PATH for DLL loading
|
|
511
|
-
# Pixi has python.exe directly in env dir, not in Scripts/
|
|
512
|
-
env_dir = self.python.parent
|
|
513
|
-
library_bin = env_dir / "Library" / "bin"
|
|
514
|
-
if library_bin.is_dir():
|
|
515
|
-
existing_path = env.get("PATH", "")
|
|
516
|
-
env["PATH"] = f"{env_dir};{library_bin};{existing_path}"
|
|
517
|
-
env["COMFYUI_PIXI_LIBRARY_BIN"] = str(library_bin)
|
|
518
|
-
# Allow duplicate OpenMP libraries (MKL's libiomp5md.dll + PyTorch's libomp.dll)
|
|
519
|
-
env["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
520
|
-
# Use UTF-8 encoding for stdout/stderr to handle Unicode symbols
|
|
521
|
-
env["PYTHONIOENCODING"] = "utf-8"
|
|
522
|
-
|
|
523
|
-
# Run subprocess
|
|
524
|
-
cmd = [
|
|
525
|
-
str(self.python),
|
|
526
|
-
str(self._worker_script),
|
|
527
|
-
str(request_path),
|
|
528
|
-
str(response_path),
|
|
529
|
-
]
|
|
530
|
-
|
|
531
|
-
process = subprocess.Popen(
|
|
532
|
-
cmd,
|
|
533
|
-
cwd=str(self.working_dir),
|
|
534
|
-
env=env,
|
|
535
|
-
stdout=subprocess.PIPE,
|
|
536
|
-
stderr=subprocess.PIPE,
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
try:
|
|
540
|
-
stdout, stderr = process.communicate(timeout=timeout)
|
|
541
|
-
except subprocess.TimeoutExpired:
|
|
542
|
-
process.kill()
|
|
543
|
-
process.wait()
|
|
544
|
-
raise TimeoutError(f"{self.name}: Call timed out after {timeout}s")
|
|
545
|
-
|
|
546
|
-
# Check for process error
|
|
547
|
-
if process.returncode != 0:
|
|
548
|
-
raise WorkerError(
|
|
549
|
-
f"Subprocess failed with code {process.returncode}",
|
|
550
|
-
traceback=stderr.decode('utf-8', errors='replace'),
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
# Read response
|
|
554
|
-
if not response_path.exists():
|
|
555
|
-
raise WorkerError(
|
|
556
|
-
f"No response file",
|
|
557
|
-
traceback=stderr.decode('utf-8', errors='replace'),
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
response = json.loads(response_path.read_text())
|
|
561
|
-
|
|
562
|
-
if response["status"] == "error":
|
|
563
|
-
raise WorkerError(
|
|
564
|
-
response.get("error", "Unknown error"),
|
|
565
|
-
traceback=response.get("traceback"),
|
|
566
|
-
)
|
|
567
|
-
|
|
568
|
-
# Load result
|
|
569
|
-
if outputs_path.exists():
|
|
570
|
-
result = torch.load(str(outputs_path), weights_only=False)
|
|
571
|
-
return result
|
|
572
|
-
else:
|
|
573
|
-
return None
|
|
574
|
-
|
|
575
|
-
finally:
|
|
576
|
-
# Cleanup IPC files
|
|
577
|
-
for path in [inputs_path, outputs_path, request_path, response_path]:
|
|
578
|
-
try:
|
|
579
|
-
if path.exists():
|
|
580
|
-
path.unlink()
|
|
581
|
-
except:
|
|
582
|
-
pass
|
|
583
|
-
|
|
584
|
-
def shutdown(self) -> None:
|
|
585
|
-
"""Shut down the worker and clean up resources."""
|
|
586
|
-
if self._shutdown:
|
|
587
|
-
return
|
|
588
|
-
|
|
589
|
-
self._shutdown = True
|
|
590
|
-
|
|
591
|
-
# Clean up temp directory
|
|
592
|
-
try:
|
|
593
|
-
shutil.rmtree(self._temp_dir, ignore_errors=True)
|
|
594
|
-
except:
|
|
595
|
-
pass
|
|
596
|
-
|
|
597
|
-
def is_alive(self) -> bool:
|
|
598
|
-
"""VenvWorker spawns fresh process per call, so always 'alive' if not shutdown."""
|
|
599
|
-
return not self._shutdown
|
|
600
|
-
|
|
601
|
-
def __repr__(self):
|
|
602
|
-
return f"<VenvWorker name={self.name!r} python={self.python}>"
|
|
603
|
-
|
|
604
|
-
|
|
605
405
|
# Persistent worker script - runs as __main__ in the venv Python subprocess
|
|
606
406
|
# Uses Unix socket (or TCP localhost) for IPC - completely separate from stdout/stderr
|
|
607
407
|
_PERSISTENT_WORKER_SCRIPT = '''
|
|
@@ -717,6 +517,99 @@ if sys.platform == "win32":
|
|
|
717
517
|
except Exception as e:
|
|
718
518
|
wlog(f"[worker] Failed to add pixi Library/bin: {e}")
|
|
719
519
|
|
|
520
|
+
# =============================================================================
|
|
521
|
+
# Shared Memory Serialization
|
|
522
|
+
# =============================================================================
|
|
523
|
+
|
|
524
|
+
from multiprocessing import shared_memory as shm
|
|
525
|
+
import numpy as np
|
|
526
|
+
|
|
527
|
+
def _to_shm(obj, registry, visited=None):
|
|
528
|
+
"""Serialize to shared memory. Returns JSON-safe metadata."""
|
|
529
|
+
if visited is None:
|
|
530
|
+
visited = {}
|
|
531
|
+
obj_id = id(obj)
|
|
532
|
+
if obj_id in visited:
|
|
533
|
+
return visited[obj_id]
|
|
534
|
+
t = type(obj).__name__
|
|
535
|
+
|
|
536
|
+
if t == 'ndarray':
|
|
537
|
+
arr = np.ascontiguousarray(obj)
|
|
538
|
+
block = shm.SharedMemory(create=True, size=arr.nbytes)
|
|
539
|
+
np.ndarray(arr.shape, arr.dtype, buffer=block.buf)[:] = arr
|
|
540
|
+
registry.append(block)
|
|
541
|
+
result = {"__shm_np__": block.name, "shape": list(arr.shape), "dtype": str(arr.dtype)}
|
|
542
|
+
visited[obj_id] = result
|
|
543
|
+
return result
|
|
544
|
+
|
|
545
|
+
if t == 'Tensor':
|
|
546
|
+
arr = obj.detach().cpu().numpy()
|
|
547
|
+
return _to_shm(arr, registry, visited)
|
|
548
|
+
|
|
549
|
+
if t == 'Trimesh':
|
|
550
|
+
verts = np.ascontiguousarray(obj.vertices, dtype=np.float64)
|
|
551
|
+
faces = np.ascontiguousarray(obj.faces, dtype=np.int64)
|
|
552
|
+
|
|
553
|
+
v_block = shm.SharedMemory(create=True, size=verts.nbytes)
|
|
554
|
+
np.ndarray(verts.shape, verts.dtype, buffer=v_block.buf)[:] = verts
|
|
555
|
+
registry.append(v_block)
|
|
556
|
+
|
|
557
|
+
f_block = shm.SharedMemory(create=True, size=faces.nbytes)
|
|
558
|
+
np.ndarray(faces.shape, faces.dtype, buffer=f_block.buf)[:] = faces
|
|
559
|
+
registry.append(f_block)
|
|
560
|
+
|
|
561
|
+
result = {
|
|
562
|
+
"__shm_trimesh__": True,
|
|
563
|
+
"v_name": v_block.name, "v_shape": list(verts.shape),
|
|
564
|
+
"f_name": f_block.name, "f_shape": list(faces.shape),
|
|
565
|
+
}
|
|
566
|
+
visited[obj_id] = result
|
|
567
|
+
return result
|
|
568
|
+
|
|
569
|
+
if isinstance(obj, dict):
|
|
570
|
+
result = {k: _to_shm(v, registry, visited) for k, v in obj.items()}
|
|
571
|
+
visited[obj_id] = result
|
|
572
|
+
return result
|
|
573
|
+
if isinstance(obj, (list, tuple)):
|
|
574
|
+
result = [_to_shm(v, registry, visited) for v in obj]
|
|
575
|
+
visited[obj_id] = result
|
|
576
|
+
return result
|
|
577
|
+
|
|
578
|
+
return obj
|
|
579
|
+
|
|
580
|
+
def _from_shm(obj):
|
|
581
|
+
"""Reconstruct from shared memory metadata. Does NOT unlink - caller handles that."""
|
|
582
|
+
if not isinstance(obj, dict):
|
|
583
|
+
if isinstance(obj, list):
|
|
584
|
+
return [_from_shm(v) for v in obj]
|
|
585
|
+
return obj
|
|
586
|
+
if "__shm_np__" in obj:
|
|
587
|
+
block = shm.SharedMemory(name=obj["__shm_np__"])
|
|
588
|
+
arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
|
|
589
|
+
block.close()
|
|
590
|
+
return arr
|
|
591
|
+
if "__shm_trimesh__" in obj:
|
|
592
|
+
import trimesh
|
|
593
|
+
v_block = shm.SharedMemory(name=obj["v_name"])
|
|
594
|
+
verts = np.ndarray(tuple(obj["v_shape"]), dtype=np.float64, buffer=v_block.buf).copy()
|
|
595
|
+
v_block.close()
|
|
596
|
+
|
|
597
|
+
f_block = shm.SharedMemory(name=obj["f_name"])
|
|
598
|
+
faces = np.ndarray(tuple(obj["f_shape"]), dtype=np.int64, buffer=f_block.buf).copy()
|
|
599
|
+
f_block.close()
|
|
600
|
+
|
|
601
|
+
return trimesh.Trimesh(vertices=verts, faces=faces, process=False)
|
|
602
|
+
return {k: _from_shm(v) for k, v in obj.items()}
|
|
603
|
+
|
|
604
|
+
def _cleanup_shm(registry):
|
|
605
|
+
for block in registry:
|
|
606
|
+
try:
|
|
607
|
+
block.close()
|
|
608
|
+
block.unlink()
|
|
609
|
+
except Exception:
|
|
610
|
+
pass
|
|
611
|
+
registry.clear()
|
|
612
|
+
|
|
720
613
|
# =============================================================================
|
|
721
614
|
# Object Reference System - keep complex objects in worker, pass refs to host
|
|
722
615
|
# =============================================================================
|
|
@@ -762,9 +655,38 @@ def _should_use_reference(obj):
|
|
|
762
655
|
# Dicts, lists, tuples - recurse into contents (don't ref the container)
|
|
763
656
|
if isinstance(obj, (dict, list, tuple)):
|
|
764
657
|
return False
|
|
765
|
-
#
|
|
658
|
+
# Trimesh - pass by value but needs special handling (see _prepare_trimesh_for_pickle)
|
|
659
|
+
if obj_type == 'Trimesh':
|
|
660
|
+
return False
|
|
661
|
+
# Everything else (custom classes) - pass by reference
|
|
766
662
|
return True
|
|
767
663
|
|
|
664
|
+
def _prepare_trimesh_for_pickle(mesh):
|
|
665
|
+
"""
|
|
666
|
+
Prepare a trimesh object for cross-Python-version pickling.
|
|
667
|
+
|
|
668
|
+
Trimesh attaches helper objects (ray tracer, proximity query) that may use
|
|
669
|
+
native extensions like embreex. These cause import errors when unpickling
|
|
670
|
+
on a system without those extensions. We strip them - they'll be recreated
|
|
671
|
+
lazily when needed.
|
|
672
|
+
|
|
673
|
+
Note: Do NOT strip _cache - trimesh needs it to function properly.
|
|
674
|
+
"""
|
|
675
|
+
# Make a copy to avoid modifying the original
|
|
676
|
+
mesh = mesh.copy()
|
|
677
|
+
|
|
678
|
+
# Remove helper objects that may have unpickleable native code references
|
|
679
|
+
# These are lazily recreated on first access anyway
|
|
680
|
+
# Do NOT remove _cache - it's needed for trimesh to work
|
|
681
|
+
for attr in ('ray', '_ray', 'permutate', 'nearest'):
|
|
682
|
+
try:
|
|
683
|
+
delattr(mesh, attr)
|
|
684
|
+
except AttributeError:
|
|
685
|
+
pass
|
|
686
|
+
|
|
687
|
+
return mesh
|
|
688
|
+
|
|
689
|
+
|
|
768
690
|
def _serialize_result(obj, visited=None):
|
|
769
691
|
"""Convert result for IPC - complex objects become references."""
|
|
770
692
|
if visited is None:
|
|
@@ -783,6 +705,11 @@ def _serialize_result(obj, visited=None):
|
|
|
783
705
|
|
|
784
706
|
visited.add(obj_id)
|
|
785
707
|
|
|
708
|
+
# Handle trimesh objects specially - strip unpickleable native extensions
|
|
709
|
+
obj_type = type(obj).__name__
|
|
710
|
+
if obj_type == 'Trimesh':
|
|
711
|
+
return _prepare_trimesh_for_pickle(obj)
|
|
712
|
+
|
|
786
713
|
if isinstance(obj, dict):
|
|
787
714
|
return {k: _serialize_result(v, visited) for k, v in obj.items()}
|
|
788
715
|
if isinstance(obj, list):
|
|
@@ -791,7 +718,6 @@ def _serialize_result(obj, visited=None):
|
|
|
791
718
|
return tuple(_serialize_result(v, visited) for v in obj)
|
|
792
719
|
|
|
793
720
|
# Convert numpy scalars to Python primitives for JSON serialization
|
|
794
|
-
obj_type = type(obj).__name__
|
|
795
721
|
if obj_type in ('float16', 'float32', 'float64'):
|
|
796
722
|
return float(obj)
|
|
797
723
|
if obj_type in ('int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'):
|
|
@@ -910,10 +836,52 @@ def main():
|
|
|
910
836
|
if p not in sys.path:
|
|
911
837
|
sys.path.insert(0, p)
|
|
912
838
|
|
|
913
|
-
#
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
839
|
+
# Try to import torch (optional - not all isolated envs need it)
|
|
840
|
+
_HAS_TORCH = False
|
|
841
|
+
try:
|
|
842
|
+
import torch
|
|
843
|
+
_HAS_TORCH = True
|
|
844
|
+
wlog(f"[worker] Torch imported: {torch.__version__}")
|
|
845
|
+
except ImportError:
|
|
846
|
+
wlog("[worker] Torch not available, using pickle for serialization")
|
|
847
|
+
|
|
848
|
+
# Setup log forwarding to host
|
|
849
|
+
# This makes print() and logging statements in node code visible to the user
|
|
850
|
+
import builtins
|
|
851
|
+
import logging
|
|
852
|
+
_original_print = builtins.print
|
|
853
|
+
|
|
854
|
+
def _forwarded_print(*args, **kwargs):
|
|
855
|
+
"""Forward print() calls to host via socket."""
|
|
856
|
+
# Build message from args
|
|
857
|
+
sep = kwargs.get('sep', ' ')
|
|
858
|
+
message = sep.join(str(a) for a in args)
|
|
859
|
+
# Send to host
|
|
860
|
+
try:
|
|
861
|
+
transport.send({"type": "log", "message": message})
|
|
862
|
+
except Exception:
|
|
863
|
+
pass # Don't fail if transport is closed
|
|
864
|
+
# Also log locally for debugging
|
|
865
|
+
wlog(f"[print] {message}")
|
|
866
|
+
|
|
867
|
+
builtins.print = _forwarded_print
|
|
868
|
+
|
|
869
|
+
# Also forward logging module output
|
|
870
|
+
class SocketLogHandler(logging.Handler):
|
|
871
|
+
def emit(self, record):
|
|
872
|
+
try:
|
|
873
|
+
msg = self.format(record)
|
|
874
|
+
transport.send({"type": "log", "message": msg})
|
|
875
|
+
wlog(f"[log] {msg}")
|
|
876
|
+
except Exception:
|
|
877
|
+
pass
|
|
878
|
+
|
|
879
|
+
# Add our handler to the root logger
|
|
880
|
+
_socket_handler = SocketLogHandler()
|
|
881
|
+
_socket_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
|
|
882
|
+
logging.root.addHandler(_socket_handler)
|
|
883
|
+
|
|
884
|
+
wlog("[worker] Print and logging forwarding enabled")
|
|
917
885
|
|
|
918
886
|
# Signal ready
|
|
919
887
|
transport.send({"status": "ready"})
|
|
@@ -942,23 +910,20 @@ def main():
|
|
|
942
910
|
transport.send({"status": "pong"})
|
|
943
911
|
continue
|
|
944
912
|
|
|
913
|
+
shm_registry = []
|
|
945
914
|
try:
|
|
946
915
|
request_type = request.get("type", "call_module")
|
|
947
916
|
module_name = request["module"]
|
|
948
|
-
inputs_path = request.get("inputs_path")
|
|
949
|
-
outputs_path = request.get("outputs_path")
|
|
950
917
|
wlog(f"[worker] Request: {request_type} {module_name}")
|
|
951
918
|
|
|
952
|
-
# Load inputs
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
inputs
|
|
956
|
-
|
|
919
|
+
# Load inputs from shared memory
|
|
920
|
+
kwargs_meta = request.get("kwargs")
|
|
921
|
+
if kwargs_meta:
|
|
922
|
+
wlog(f"[worker] Reconstructing inputs from shm...")
|
|
923
|
+
inputs = _from_shm(kwargs_meta)
|
|
957
924
|
inputs = _deserialize_isolated_objects(inputs)
|
|
958
|
-
# Resolve any object references from previous node calls
|
|
959
|
-
wlog(f"[worker] Resolving object references...")
|
|
960
925
|
inputs = _deserialize_input(inputs)
|
|
961
|
-
wlog(f"[worker] Inputs ready: {list(inputs.keys())}")
|
|
926
|
+
wlog(f"[worker] Inputs ready: {list(inputs.keys()) if isinstance(inputs, dict) else type(inputs)}")
|
|
962
927
|
else:
|
|
963
928
|
inputs = {}
|
|
964
929
|
|
|
@@ -987,14 +952,17 @@ def main():
|
|
|
987
952
|
func = getattr(module, func_name)
|
|
988
953
|
result = func(**inputs)
|
|
989
954
|
|
|
990
|
-
#
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
955
|
+
# Serialize result to shared memory
|
|
956
|
+
wlog(f"[worker] Serializing result to shm...")
|
|
957
|
+
result_meta = _to_shm(result, shm_registry)
|
|
958
|
+
wlog(f"[worker] Created {len(shm_registry)} shm blocks for result")
|
|
994
959
|
|
|
995
|
-
transport.send({"status": "ok"})
|
|
960
|
+
transport.send({"status": "ok", "result": result_meta})
|
|
961
|
+
# Note: don't cleanup shm_registry here - host needs to read it
|
|
996
962
|
|
|
997
963
|
except Exception as e:
|
|
964
|
+
# Cleanup shm on error since host won't read it
|
|
965
|
+
_cleanup_shm(shm_registry)
|
|
998
966
|
transport.send({
|
|
999
967
|
"status": "error",
|
|
1000
968
|
"error": str(e),
|
|
@@ -1008,9 +976,9 @@ if __name__ == "__main__":
|
|
|
1008
976
|
'''
|
|
1009
977
|
|
|
1010
978
|
|
|
1011
|
-
class
|
|
979
|
+
class SubprocessWorker(Worker):
|
|
1012
980
|
"""
|
|
1013
|
-
|
|
981
|
+
Cross-venv worker using persistent subprocess + socket IPC.
|
|
1014
982
|
|
|
1015
983
|
Uses Unix domain sockets (or TCP localhost on older Windows) for IPC.
|
|
1016
984
|
This completely separates IPC from stdout/stderr, so C libraries
|
|
@@ -1019,11 +987,11 @@ class PersistentVenvWorker(Worker):
|
|
|
1019
987
|
Benefits:
|
|
1020
988
|
- Works on Windows with different venv Python (full isolation)
|
|
1021
989
|
- Compiled CUDA extensions load correctly in the venv
|
|
1022
|
-
- ~50-100ms per call (
|
|
990
|
+
- ~50-100ms per call (persistent subprocess avoids spawn overhead)
|
|
1023
991
|
- Tensor transfer via shared memory files
|
|
1024
992
|
- Immune to stdout pollution from C libraries
|
|
1025
993
|
|
|
1026
|
-
Use this for
|
|
994
|
+
Use this for calls to isolated venvs with different Python/dependencies.
|
|
1027
995
|
"""
|
|
1028
996
|
|
|
1029
997
|
def __init__(
|
|
@@ -1050,7 +1018,7 @@ class PersistentVenvWorker(Worker):
|
|
|
1050
1018
|
self.working_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
1051
1019
|
self.sys_path = sys_path or []
|
|
1052
1020
|
self.extra_env = env or {}
|
|
1053
|
-
self.name = name or f"
|
|
1021
|
+
self.name = name or f"SubprocessWorker({self.python.parent.parent.name})"
|
|
1054
1022
|
|
|
1055
1023
|
if not self.python.exists():
|
|
1056
1024
|
raise FileNotFoundError(f"Python not found: {self.python}")
|
|
@@ -1196,21 +1164,26 @@ class PersistentVenvWorker(Worker):
|
|
|
1196
1164
|
# Use UTF-8 encoding for stdout/stderr to handle Unicode symbols
|
|
1197
1165
|
env["PYTHONIOENCODING"] = "utf-8"
|
|
1198
1166
|
|
|
1199
|
-
# Find ComfyUI base and
|
|
1167
|
+
# Find ComfyUI base and add to sys_path for real folder_paths/comfy modules
|
|
1168
|
+
# This works because comfy.options.args_parsing=False by default, so folder_paths
|
|
1169
|
+
# auto-detects its base directory from __file__ location
|
|
1200
1170
|
comfyui_base = self._find_comfyui_base()
|
|
1201
1171
|
if comfyui_base:
|
|
1202
|
-
env["COMFYUI_BASE"] = str(comfyui_base)
|
|
1172
|
+
env["COMFYUI_BASE"] = str(comfyui_base) # Keep for fallback/debugging
|
|
1203
1173
|
|
|
1204
|
-
#
|
|
1205
|
-
|
|
1206
|
-
|
|
1174
|
+
# Build sys_path: ComfyUI first (for real modules), then working_dir, then extras
|
|
1175
|
+
all_sys_path = []
|
|
1176
|
+
if comfyui_base:
|
|
1177
|
+
all_sys_path.append(str(comfyui_base))
|
|
1178
|
+
all_sys_path.append(str(self.working_dir))
|
|
1179
|
+
all_sys_path.extend(self.sys_path)
|
|
1207
1180
|
|
|
1208
1181
|
# Launch subprocess with the venv Python, passing socket address
|
|
1209
1182
|
# For pixi environments, use "pixi run python" to get proper environment activation
|
|
1210
1183
|
# (CONDA_PREFIX, Library paths, etc.) which fixes DLL loading issues with bpy
|
|
1211
1184
|
is_pixi = '.pixi' in str(self.python)
|
|
1212
1185
|
if _DEBUG:
|
|
1213
|
-
print(f"[
|
|
1186
|
+
print(f"[SubprocessWorker] is_pixi={is_pixi}, python={self.python}", flush=True)
|
|
1214
1187
|
if is_pixi:
|
|
1215
1188
|
# Find pixi project root (parent of .pixi directory)
|
|
1216
1189
|
pixi_project = self.python
|
|
@@ -1219,7 +1192,7 @@ class PersistentVenvWorker(Worker):
|
|
|
1219
1192
|
pixi_project = pixi_project.parent # Go up from .pixi to project root
|
|
1220
1193
|
pixi_toml = pixi_project / "pixi.toml"
|
|
1221
1194
|
if _DEBUG:
|
|
1222
|
-
print(f"[
|
|
1195
|
+
print(f"[SubprocessWorker] pixi_toml={pixi_toml}, exists={pixi_toml.exists()}", flush=True)
|
|
1223
1196
|
|
|
1224
1197
|
if pixi_toml.exists():
|
|
1225
1198
|
pixi_exe = get_pixi_path()
|
|
@@ -1246,15 +1219,15 @@ class PersistentVenvWorker(Worker):
|
|
|
1246
1219
|
launch_env = env
|
|
1247
1220
|
|
|
1248
1221
|
if _DEBUG:
|
|
1249
|
-
print(f"[
|
|
1222
|
+
print(f"[SubprocessWorker] launching cmd={cmd[:3]}...", flush=True)
|
|
1250
1223
|
if launch_env:
|
|
1251
1224
|
path_sep = ";" if sys.platform == "win32" else ":"
|
|
1252
1225
|
path_parts = launch_env.get("PATH", "").split(path_sep)
|
|
1253
|
-
print(f"[
|
|
1226
|
+
print(f"[SubprocessWorker] PATH has {len(path_parts)} entries:", flush=True)
|
|
1254
1227
|
for i, p in enumerate(path_parts[:10]): # Show first 10
|
|
1255
|
-
print(f"[
|
|
1228
|
+
print(f"[SubprocessWorker] [{i}] {p}", flush=True)
|
|
1256
1229
|
if len(path_parts) > 10:
|
|
1257
|
-
print(f"[
|
|
1230
|
+
print(f"[SubprocessWorker] ... and {len(path_parts) - 10} more", flush=True)
|
|
1258
1231
|
self._process = subprocess.Popen(
|
|
1259
1232
|
cmd,
|
|
1260
1233
|
stdin=subprocess.DEVNULL,
|
|
@@ -1341,9 +1314,21 @@ class PersistentVenvWorker(Worker):
|
|
|
1341
1314
|
# Send request
|
|
1342
1315
|
self._transport.send(request)
|
|
1343
1316
|
|
|
1344
|
-
# Read response with timeout
|
|
1317
|
+
# Read response with timeout, handling log messages along the way
|
|
1345
1318
|
try:
|
|
1346
|
-
|
|
1319
|
+
while True:
|
|
1320
|
+
response = self._transport.recv(timeout=timeout)
|
|
1321
|
+
if response is None:
|
|
1322
|
+
break # Timeout
|
|
1323
|
+
|
|
1324
|
+
# Handle log messages from worker
|
|
1325
|
+
if response.get("type") == "log":
|
|
1326
|
+
msg = response.get("message", "")
|
|
1327
|
+
print(f"[worker:{self.name}] {msg}", file=sys.stderr, flush=True)
|
|
1328
|
+
continue # Keep waiting for actual response
|
|
1329
|
+
|
|
1330
|
+
# Got a real response
|
|
1331
|
+
break
|
|
1347
1332
|
except ConnectionError as e:
|
|
1348
1333
|
# Socket closed - check if worker process died
|
|
1349
1334
|
self._shutdown = True
|
|
@@ -1406,49 +1391,43 @@ class PersistentVenvWorker(Worker):
|
|
|
1406
1391
|
"""
|
|
1407
1392
|
import sys
|
|
1408
1393
|
if _DEBUG:
|
|
1409
|
-
print(f"[
|
|
1394
|
+
print(f"[SubprocessWorker] call_method: {module_name}.{class_name}.{method_name}", file=sys.stderr, flush=True)
|
|
1410
1395
|
|
|
1411
1396
|
with self._lock:
|
|
1412
1397
|
if _DEBUG:
|
|
1413
|
-
print(f"[
|
|
1398
|
+
print(f"[SubprocessWorker] acquired lock, ensuring started...", file=sys.stderr, flush=True)
|
|
1414
1399
|
self._ensure_started()
|
|
1415
1400
|
if _DEBUG:
|
|
1416
|
-
print(f"[
|
|
1401
|
+
print(f"[SubprocessWorker] worker started/confirmed", file=sys.stderr, flush=True)
|
|
1417
1402
|
|
|
1418
1403
|
timeout = timeout or 600.0
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
import torch
|
|
1422
|
-
inputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_in.pt"
|
|
1423
|
-
outputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_out.pt"
|
|
1404
|
+
shm_registry = []
|
|
1424
1405
|
|
|
1425
1406
|
try:
|
|
1426
|
-
# Serialize kwargs
|
|
1407
|
+
# Serialize kwargs to shared memory
|
|
1427
1408
|
if kwargs:
|
|
1428
1409
|
if _DEBUG:
|
|
1429
|
-
print(f"[
|
|
1430
|
-
|
|
1431
|
-
if _DEBUG:
|
|
1432
|
-
print(f"[PersistentVenvWorker] saving to {inputs_path}...", file=sys.stderr, flush=True)
|
|
1433
|
-
torch.save(serialized_kwargs, str(inputs_path))
|
|
1410
|
+
print(f"[SubprocessWorker] serializing kwargs to shm...", file=sys.stderr, flush=True)
|
|
1411
|
+
kwargs_meta = _to_shm(kwargs, shm_registry)
|
|
1434
1412
|
if _DEBUG:
|
|
1435
|
-
print(f"[
|
|
1413
|
+
print(f"[SubprocessWorker] created {len(shm_registry)} shm blocks", file=sys.stderr, flush=True)
|
|
1414
|
+
else:
|
|
1415
|
+
kwargs_meta = None
|
|
1436
1416
|
|
|
1437
|
-
# Send request with
|
|
1417
|
+
# Send request with shared memory metadata
|
|
1438
1418
|
request = {
|
|
1439
1419
|
"type": "call_method",
|
|
1440
1420
|
"module": module_name,
|
|
1441
1421
|
"class_name": class_name,
|
|
1442
1422
|
"method_name": method_name,
|
|
1443
|
-
"self_state": self_state,
|
|
1444
|
-
"
|
|
1445
|
-
"outputs_path": str(outputs_path),
|
|
1423
|
+
"self_state": _serialize_for_ipc(self_state) if self_state else None,
|
|
1424
|
+
"kwargs": kwargs_meta,
|
|
1446
1425
|
}
|
|
1447
1426
|
if _DEBUG:
|
|
1448
|
-
print(f"[
|
|
1427
|
+
print(f"[SubprocessWorker] sending request via socket...", file=sys.stderr, flush=True)
|
|
1449
1428
|
response = self._send_request(request, timeout)
|
|
1450
1429
|
if _DEBUG:
|
|
1451
|
-
print(f"[
|
|
1430
|
+
print(f"[SubprocessWorker] got response: {response.get('status')}", file=sys.stderr, flush=True)
|
|
1452
1431
|
|
|
1453
1432
|
if response.get("status") == "error":
|
|
1454
1433
|
raise WorkerError(
|
|
@@ -1456,16 +1435,14 @@ class PersistentVenvWorker(Worker):
|
|
|
1456
1435
|
traceback=response.get("traceback"),
|
|
1457
1436
|
)
|
|
1458
1437
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1438
|
+
# Reconstruct result from shared memory
|
|
1439
|
+
result_meta = response.get("result")
|
|
1440
|
+
if result_meta is not None:
|
|
1441
|
+
return _from_shm(result_meta)
|
|
1461
1442
|
return None
|
|
1462
1443
|
|
|
1463
1444
|
finally:
|
|
1464
|
-
|
|
1465
|
-
try:
|
|
1466
|
-
p.unlink()
|
|
1467
|
-
except:
|
|
1468
|
-
pass
|
|
1445
|
+
_cleanup_shm(shm_registry)
|
|
1469
1446
|
|
|
1470
1447
|
def call_module(
|
|
1471
1448
|
self,
|
|
@@ -1479,25 +1456,16 @@ class PersistentVenvWorker(Worker):
|
|
|
1479
1456
|
self._ensure_started()
|
|
1480
1457
|
|
|
1481
1458
|
timeout = timeout or 600.0
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
# Save inputs
|
|
1485
|
-
import torch
|
|
1486
|
-
inputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_in.pt"
|
|
1487
|
-
outputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_out.pt"
|
|
1459
|
+
shm_registry = []
|
|
1488
1460
|
|
|
1489
1461
|
try:
|
|
1490
|
-
if kwargs
|
|
1491
|
-
serialized_kwargs = _serialize_for_ipc(kwargs)
|
|
1492
|
-
torch.save(serialized_kwargs, str(inputs_path))
|
|
1462
|
+
kwargs_meta = _to_shm(kwargs, shm_registry) if kwargs else None
|
|
1493
1463
|
|
|
1494
|
-
# Send request
|
|
1495
1464
|
request = {
|
|
1496
1465
|
"type": "call_module",
|
|
1497
1466
|
"module": module,
|
|
1498
1467
|
"func": func,
|
|
1499
|
-
"
|
|
1500
|
-
"outputs_path": str(outputs_path),
|
|
1468
|
+
"kwargs": kwargs_meta,
|
|
1501
1469
|
}
|
|
1502
1470
|
response = self._send_request(request, timeout)
|
|
1503
1471
|
|
|
@@ -1507,17 +1475,13 @@ class PersistentVenvWorker(Worker):
|
|
|
1507
1475
|
traceback=response.get("traceback"),
|
|
1508
1476
|
)
|
|
1509
1477
|
|
|
1510
|
-
|
|
1511
|
-
if
|
|
1512
|
-
return
|
|
1478
|
+
result_meta = response.get("result")
|
|
1479
|
+
if result_meta is not None:
|
|
1480
|
+
return _from_shm(result_meta)
|
|
1513
1481
|
return None
|
|
1514
1482
|
|
|
1515
1483
|
finally:
|
|
1516
|
-
|
|
1517
|
-
try:
|
|
1518
|
-
p.unlink()
|
|
1519
|
-
except:
|
|
1520
|
-
pass
|
|
1484
|
+
_cleanup_shm(shm_registry)
|
|
1521
1485
|
|
|
1522
1486
|
def shutdown(self) -> None:
|
|
1523
1487
|
"""Shut down the persistent worker."""
|
|
@@ -1569,4 +1533,4 @@ class PersistentVenvWorker(Worker):
|
|
|
1569
1533
|
|
|
1570
1534
|
def __repr__(self):
|
|
1571
1535
|
status = "alive" if self.is_alive() else "stopped"
|
|
1572
|
-
return f"<
|
|
1536
|
+
return f"<SubprocessWorker name={self.name!r} status={status}>"
|