comfy-env 0.0.65__py3-none-any.whl → 0.0.67__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 +68 -122
- comfy_env/cli.py +74 -204
- 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 +69 -128
- 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} +397 -443
- {comfy_env-0.0.65.dist-info → comfy_env-0.0.67.dist-info}/METADATA +23 -92
- comfy_env-0.0.67.dist-info/RECORD +32 -0
- comfy_env/decorator.py +0 -700
- comfy_env/env/__init__.py +0 -46
- comfy_env/env/config.py +0 -191
- comfy_env/env/config_file.py +0 -706
- 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/registry.py +0 -130
- 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/wheel_sources.yml +0 -141
- comfy_env/workers/pool.py +0 -241
- comfy_env-0.0.65.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-0.0.65.dist-info → comfy_env-0.0.67.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.65.dist-info → comfy_env-0.0.67.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.65.dist-info → comfy_env-0.0.67.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
|
|
|
@@ -257,104 +393,6 @@ def _serialize_for_ipc(obj, visited=None):
|
|
|
257
393
|
return obj
|
|
258
394
|
|
|
259
395
|
|
|
260
|
-
# Worker script template - minimal, runs in target venv
|
|
261
|
-
_WORKER_SCRIPT = '''
|
|
262
|
-
import sys
|
|
263
|
-
import os
|
|
264
|
-
import json
|
|
265
|
-
import traceback
|
|
266
|
-
from types import SimpleNamespace
|
|
267
|
-
|
|
268
|
-
# On Windows, add DLL directories for proper library loading
|
|
269
|
-
if sys.platform == "win32" and hasattr(os, "add_dll_directory"):
|
|
270
|
-
_host_python_dir = os.environ.get("COMFYUI_HOST_PYTHON_DIR")
|
|
271
|
-
if _host_python_dir:
|
|
272
|
-
try:
|
|
273
|
-
os.add_dll_directory(_host_python_dir)
|
|
274
|
-
_dlls_dir = os.path.join(_host_python_dir, "DLLs")
|
|
275
|
-
if os.path.isdir(_dlls_dir):
|
|
276
|
-
os.add_dll_directory(_dlls_dir)
|
|
277
|
-
except Exception:
|
|
278
|
-
pass
|
|
279
|
-
_pixi_library_bin = os.environ.get("COMFYUI_PIXI_LIBRARY_BIN")
|
|
280
|
-
if _pixi_library_bin:
|
|
281
|
-
try:
|
|
282
|
-
os.add_dll_directory(_pixi_library_bin)
|
|
283
|
-
except Exception:
|
|
284
|
-
pass
|
|
285
|
-
|
|
286
|
-
def _deserialize_isolated_objects(obj):
|
|
287
|
-
"""Reconstruct objects serialized with __isolated_object__ marker."""
|
|
288
|
-
if isinstance(obj, dict):
|
|
289
|
-
if obj.get("__isolated_object__"):
|
|
290
|
-
# Reconstruct as SimpleNamespace (supports .attr access)
|
|
291
|
-
attrs = {k: _deserialize_isolated_objects(v) for k, v in obj.get("__attrs__", {}).items()}
|
|
292
|
-
ns = SimpleNamespace(**attrs)
|
|
293
|
-
ns.__class_name__ = obj.get("__class_name__", "Unknown")
|
|
294
|
-
return ns
|
|
295
|
-
return {k: _deserialize_isolated_objects(v) for k, v in obj.items()}
|
|
296
|
-
elif isinstance(obj, list):
|
|
297
|
-
return [_deserialize_isolated_objects(v) for v in obj]
|
|
298
|
-
elif isinstance(obj, tuple):
|
|
299
|
-
return tuple(_deserialize_isolated_objects(v) for v in obj)
|
|
300
|
-
return obj
|
|
301
|
-
|
|
302
|
-
def main():
|
|
303
|
-
# Read request from file
|
|
304
|
-
request_path = sys.argv[1]
|
|
305
|
-
response_path = sys.argv[2]
|
|
306
|
-
|
|
307
|
-
with open(request_path, 'r') as f:
|
|
308
|
-
request = json.load(f)
|
|
309
|
-
|
|
310
|
-
try:
|
|
311
|
-
# Setup paths
|
|
312
|
-
for p in request.get("sys_path", []):
|
|
313
|
-
if p not in sys.path:
|
|
314
|
-
sys.path.insert(0, p)
|
|
315
|
-
|
|
316
|
-
# Import torch for tensor I/O
|
|
317
|
-
import torch
|
|
318
|
-
|
|
319
|
-
# Load inputs
|
|
320
|
-
inputs_path = request.get("inputs_path")
|
|
321
|
-
if inputs_path:
|
|
322
|
-
inputs = torch.load(inputs_path, weights_only=False)
|
|
323
|
-
inputs = _deserialize_isolated_objects(inputs)
|
|
324
|
-
else:
|
|
325
|
-
inputs = {}
|
|
326
|
-
|
|
327
|
-
# Import and call function
|
|
328
|
-
module_name = request["module"]
|
|
329
|
-
func_name = request["func"]
|
|
330
|
-
|
|
331
|
-
module = __import__(module_name, fromlist=[func_name])
|
|
332
|
-
func = getattr(module, func_name)
|
|
333
|
-
|
|
334
|
-
result = func(**inputs)
|
|
335
|
-
|
|
336
|
-
# Save outputs
|
|
337
|
-
outputs_path = request.get("outputs_path")
|
|
338
|
-
if outputs_path:
|
|
339
|
-
torch.save(result, outputs_path)
|
|
340
|
-
|
|
341
|
-
response = {"status": "ok"}
|
|
342
|
-
|
|
343
|
-
except Exception as e:
|
|
344
|
-
response = {
|
|
345
|
-
"status": "error",
|
|
346
|
-
"error": str(e),
|
|
347
|
-
"traceback": traceback.format_exc(),
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
with open(response_path, 'w') as f:
|
|
351
|
-
json.dump(response, f)
|
|
352
|
-
|
|
353
|
-
if __name__ == "__main__":
|
|
354
|
-
main()
|
|
355
|
-
'''
|
|
356
|
-
|
|
357
|
-
|
|
358
396
|
def _get_shm_dir() -> Path:
|
|
359
397
|
"""Get shared memory directory for efficient tensor transfer."""
|
|
360
398
|
# Linux: /dev/shm is RAM-backed tmpfs
|
|
@@ -364,249 +402,6 @@ def _get_shm_dir() -> Path:
|
|
|
364
402
|
return Path(tempfile.gettempdir())
|
|
365
403
|
|
|
366
404
|
|
|
367
|
-
class VenvWorker(Worker):
|
|
368
|
-
"""
|
|
369
|
-
Worker using subprocess for cross-venv isolation.
|
|
370
|
-
|
|
371
|
-
This worker spawns a new Python process for each call, using
|
|
372
|
-
a different Python interpreter (from another venv). Tensors are
|
|
373
|
-
transferred via torch.save/load through shared memory.
|
|
374
|
-
|
|
375
|
-
For long-running workloads, consider using persistent mode which
|
|
376
|
-
keeps the subprocess alive between calls.
|
|
377
|
-
"""
|
|
378
|
-
|
|
379
|
-
def __init__(
|
|
380
|
-
self,
|
|
381
|
-
python: Union[str, Path],
|
|
382
|
-
working_dir: Optional[Union[str, Path]] = None,
|
|
383
|
-
sys_path: Optional[List[str]] = None,
|
|
384
|
-
env: Optional[Dict[str, str]] = None,
|
|
385
|
-
name: Optional[str] = None,
|
|
386
|
-
persistent: bool = True,
|
|
387
|
-
):
|
|
388
|
-
"""
|
|
389
|
-
Initialize the worker.
|
|
390
|
-
|
|
391
|
-
Args:
|
|
392
|
-
python: Path to Python executable in target venv.
|
|
393
|
-
working_dir: Working directory for subprocess.
|
|
394
|
-
sys_path: Additional paths to add to sys.path in subprocess.
|
|
395
|
-
env: Additional environment variables.
|
|
396
|
-
name: Optional name for logging.
|
|
397
|
-
persistent: If True, keep subprocess alive between calls (faster).
|
|
398
|
-
"""
|
|
399
|
-
self.python = Path(python)
|
|
400
|
-
self.working_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
401
|
-
self.sys_path = sys_path or []
|
|
402
|
-
self.extra_env = env or {}
|
|
403
|
-
self.name = name or f"VenvWorker({self.python.parent.parent.name})"
|
|
404
|
-
self.persistent = persistent
|
|
405
|
-
|
|
406
|
-
# Verify Python exists
|
|
407
|
-
if not self.python.exists():
|
|
408
|
-
raise FileNotFoundError(f"Python not found: {self.python}")
|
|
409
|
-
|
|
410
|
-
# Create temp directory for IPC files
|
|
411
|
-
self._temp_dir = Path(tempfile.mkdtemp(prefix='comfyui_venv_'))
|
|
412
|
-
self._shm_dir = _get_shm_dir()
|
|
413
|
-
|
|
414
|
-
# Persistent process state
|
|
415
|
-
self._process: Optional[subprocess.Popen] = None
|
|
416
|
-
self._shutdown = False
|
|
417
|
-
|
|
418
|
-
# Write worker script
|
|
419
|
-
self._worker_script = self._temp_dir / "worker.py"
|
|
420
|
-
self._worker_script.write_text(_WORKER_SCRIPT)
|
|
421
|
-
|
|
422
|
-
def call(
|
|
423
|
-
self,
|
|
424
|
-
func: Callable,
|
|
425
|
-
*args,
|
|
426
|
-
timeout: Optional[float] = None,
|
|
427
|
-
**kwargs
|
|
428
|
-
) -> Any:
|
|
429
|
-
"""
|
|
430
|
-
Execute a function - NOT SUPPORTED for VenvWorker.
|
|
431
|
-
|
|
432
|
-
VenvWorker cannot pickle arbitrary functions across venv boundaries.
|
|
433
|
-
Use call_module() instead to call functions by module path.
|
|
434
|
-
|
|
435
|
-
Raises:
|
|
436
|
-
NotImplementedError: Always.
|
|
437
|
-
"""
|
|
438
|
-
raise NotImplementedError(
|
|
439
|
-
f"{self.name}: VenvWorker cannot call arbitrary functions. "
|
|
440
|
-
f"Use call_module(module='...', func='...', **kwargs) instead."
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
def call_module(
|
|
444
|
-
self,
|
|
445
|
-
module: str,
|
|
446
|
-
func: str,
|
|
447
|
-
timeout: Optional[float] = None,
|
|
448
|
-
**kwargs
|
|
449
|
-
) -> Any:
|
|
450
|
-
"""
|
|
451
|
-
Call a function by module path in the isolated venv.
|
|
452
|
-
|
|
453
|
-
Args:
|
|
454
|
-
module: Module name (e.g., "my_package.my_module").
|
|
455
|
-
func: Function name within the module.
|
|
456
|
-
timeout: Timeout in seconds (None = 600s default).
|
|
457
|
-
**kwargs: Keyword arguments passed to the function.
|
|
458
|
-
Must be torch.save-compatible (tensors, dicts, etc.).
|
|
459
|
-
|
|
460
|
-
Returns:
|
|
461
|
-
Return value of module.func(**kwargs).
|
|
462
|
-
|
|
463
|
-
Raises:
|
|
464
|
-
WorkerError: If function raises an exception.
|
|
465
|
-
TimeoutError: If execution exceeds timeout.
|
|
466
|
-
"""
|
|
467
|
-
if self._shutdown:
|
|
468
|
-
raise RuntimeError(f"{self.name}: Worker has been shut down")
|
|
469
|
-
|
|
470
|
-
timeout = timeout or 600.0 # 10 minute default
|
|
471
|
-
|
|
472
|
-
# Create unique ID for this call
|
|
473
|
-
call_id = str(uuid.uuid4())[:8]
|
|
474
|
-
|
|
475
|
-
# Paths for IPC (use shm for tensors, temp for json)
|
|
476
|
-
inputs_path = self._shm_dir / f"comfyui_venv_{call_id}_in.pt"
|
|
477
|
-
outputs_path = self._shm_dir / f"comfyui_venv_{call_id}_out.pt"
|
|
478
|
-
request_path = self._temp_dir / f"request_{call_id}.json"
|
|
479
|
-
response_path = self._temp_dir / f"response_{call_id}.json"
|
|
480
|
-
|
|
481
|
-
try:
|
|
482
|
-
# Save inputs via torch.save (handles tensors natively)
|
|
483
|
-
# Serialize custom objects with broken __module__ paths first
|
|
484
|
-
import torch
|
|
485
|
-
if kwargs:
|
|
486
|
-
serialized_kwargs = _serialize_for_ipc(kwargs)
|
|
487
|
-
torch.save(serialized_kwargs, str(inputs_path))
|
|
488
|
-
|
|
489
|
-
# Build request
|
|
490
|
-
request = {
|
|
491
|
-
"module": module,
|
|
492
|
-
"func": func,
|
|
493
|
-
"sys_path": [str(self.working_dir)] + self.sys_path,
|
|
494
|
-
"inputs_path": str(inputs_path) if kwargs else None,
|
|
495
|
-
"outputs_path": str(outputs_path),
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
request_path.write_text(json.dumps(request))
|
|
499
|
-
|
|
500
|
-
# Build environment
|
|
501
|
-
env = os.environ.copy()
|
|
502
|
-
env.update(self.extra_env)
|
|
503
|
-
env["COMFYUI_ISOLATION_WORKER"] = "1"
|
|
504
|
-
|
|
505
|
-
# For conda/pixi environments, add lib dir to LD_LIBRARY_PATH (Linux)
|
|
506
|
-
lib_dir = self.python.parent.parent / "lib"
|
|
507
|
-
if lib_dir.is_dir():
|
|
508
|
-
existing = env.get("LD_LIBRARY_PATH", "")
|
|
509
|
-
env["LD_LIBRARY_PATH"] = f"{lib_dir}:{existing}" if existing else str(lib_dir)
|
|
510
|
-
|
|
511
|
-
# On Windows, pass host Python directory and pixi Library/bin for DLL loading
|
|
512
|
-
if sys.platform == "win32":
|
|
513
|
-
env["COMFYUI_HOST_PYTHON_DIR"] = str(Path(sys.executable).parent)
|
|
514
|
-
|
|
515
|
-
# For pixi environments with MKL, add Library/bin to PATH for DLL loading
|
|
516
|
-
# Pixi has python.exe directly in env dir, not in Scripts/
|
|
517
|
-
env_dir = self.python.parent
|
|
518
|
-
library_bin = env_dir / "Library" / "bin"
|
|
519
|
-
if library_bin.is_dir():
|
|
520
|
-
existing_path = env.get("PATH", "")
|
|
521
|
-
env["PATH"] = f"{env_dir};{library_bin};{existing_path}"
|
|
522
|
-
env["COMFYUI_PIXI_LIBRARY_BIN"] = str(library_bin)
|
|
523
|
-
# Allow duplicate OpenMP libraries (MKL's libiomp5md.dll + PyTorch's libomp.dll)
|
|
524
|
-
env["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
525
|
-
# Use UTF-8 encoding for stdout/stderr to handle Unicode symbols
|
|
526
|
-
env["PYTHONIOENCODING"] = "utf-8"
|
|
527
|
-
|
|
528
|
-
# Run subprocess
|
|
529
|
-
cmd = [
|
|
530
|
-
str(self.python),
|
|
531
|
-
str(self._worker_script),
|
|
532
|
-
str(request_path),
|
|
533
|
-
str(response_path),
|
|
534
|
-
]
|
|
535
|
-
|
|
536
|
-
process = subprocess.Popen(
|
|
537
|
-
cmd,
|
|
538
|
-
cwd=str(self.working_dir),
|
|
539
|
-
env=env,
|
|
540
|
-
stdout=subprocess.PIPE,
|
|
541
|
-
stderr=subprocess.PIPE,
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
try:
|
|
545
|
-
stdout, stderr = process.communicate(timeout=timeout)
|
|
546
|
-
except subprocess.TimeoutExpired:
|
|
547
|
-
process.kill()
|
|
548
|
-
process.wait()
|
|
549
|
-
raise TimeoutError(f"{self.name}: Call timed out after {timeout}s")
|
|
550
|
-
|
|
551
|
-
# Check for process error
|
|
552
|
-
if process.returncode != 0:
|
|
553
|
-
raise WorkerError(
|
|
554
|
-
f"Subprocess failed with code {process.returncode}",
|
|
555
|
-
traceback=stderr.decode('utf-8', errors='replace'),
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
# Read response
|
|
559
|
-
if not response_path.exists():
|
|
560
|
-
raise WorkerError(
|
|
561
|
-
f"No response file",
|
|
562
|
-
traceback=stderr.decode('utf-8', errors='replace'),
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
response = json.loads(response_path.read_text())
|
|
566
|
-
|
|
567
|
-
if response["status"] == "error":
|
|
568
|
-
raise WorkerError(
|
|
569
|
-
response.get("error", "Unknown error"),
|
|
570
|
-
traceback=response.get("traceback"),
|
|
571
|
-
)
|
|
572
|
-
|
|
573
|
-
# Load result
|
|
574
|
-
if outputs_path.exists():
|
|
575
|
-
result = torch.load(str(outputs_path), weights_only=False)
|
|
576
|
-
return result
|
|
577
|
-
else:
|
|
578
|
-
return None
|
|
579
|
-
|
|
580
|
-
finally:
|
|
581
|
-
# Cleanup IPC files
|
|
582
|
-
for path in [inputs_path, outputs_path, request_path, response_path]:
|
|
583
|
-
try:
|
|
584
|
-
if path.exists():
|
|
585
|
-
path.unlink()
|
|
586
|
-
except:
|
|
587
|
-
pass
|
|
588
|
-
|
|
589
|
-
def shutdown(self) -> None:
|
|
590
|
-
"""Shut down the worker and clean up resources."""
|
|
591
|
-
if self._shutdown:
|
|
592
|
-
return
|
|
593
|
-
|
|
594
|
-
self._shutdown = True
|
|
595
|
-
|
|
596
|
-
# Clean up temp directory
|
|
597
|
-
try:
|
|
598
|
-
shutil.rmtree(self._temp_dir, ignore_errors=True)
|
|
599
|
-
except:
|
|
600
|
-
pass
|
|
601
|
-
|
|
602
|
-
def is_alive(self) -> bool:
|
|
603
|
-
"""VenvWorker spawns fresh process per call, so always 'alive' if not shutdown."""
|
|
604
|
-
return not self._shutdown
|
|
605
|
-
|
|
606
|
-
def __repr__(self):
|
|
607
|
-
return f"<VenvWorker name={self.name!r} python={self.python}>"
|
|
608
|
-
|
|
609
|
-
|
|
610
405
|
# Persistent worker script - runs as __main__ in the venv Python subprocess
|
|
611
406
|
# Uses Unix socket (or TCP localhost) for IPC - completely separate from stdout/stderr
|
|
612
407
|
_PERSISTENT_WORKER_SCRIPT = '''
|
|
@@ -722,6 +517,99 @@ if sys.platform == "win32":
|
|
|
722
517
|
except Exception as e:
|
|
723
518
|
wlog(f"[worker] Failed to add pixi Library/bin: {e}")
|
|
724
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
|
+
|
|
725
613
|
# =============================================================================
|
|
726
614
|
# Object Reference System - keep complex objects in worker, pass refs to host
|
|
727
615
|
# =============================================================================
|
|
@@ -767,12 +655,38 @@ def _should_use_reference(obj):
|
|
|
767
655
|
# Dicts, lists, tuples - recurse into contents (don't ref the container)
|
|
768
656
|
if isinstance(obj, (dict, list, tuple)):
|
|
769
657
|
return False
|
|
770
|
-
# Trimesh - pass by value
|
|
658
|
+
# Trimesh - pass by value but needs special handling (see _prepare_trimesh_for_pickle)
|
|
771
659
|
if obj_type == 'Trimesh':
|
|
772
660
|
return False
|
|
773
661
|
# Everything else (custom classes) - pass by reference
|
|
774
662
|
return True
|
|
775
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
|
+
|
|
776
690
|
def _serialize_result(obj, visited=None):
|
|
777
691
|
"""Convert result for IPC - complex objects become references."""
|
|
778
692
|
if visited is None:
|
|
@@ -791,6 +705,11 @@ def _serialize_result(obj, visited=None):
|
|
|
791
705
|
|
|
792
706
|
visited.add(obj_id)
|
|
793
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
|
+
|
|
794
713
|
if isinstance(obj, dict):
|
|
795
714
|
return {k: _serialize_result(v, visited) for k, v in obj.items()}
|
|
796
715
|
if isinstance(obj, list):
|
|
@@ -799,7 +718,6 @@ def _serialize_result(obj, visited=None):
|
|
|
799
718
|
return tuple(_serialize_result(v, visited) for v in obj)
|
|
800
719
|
|
|
801
720
|
# Convert numpy scalars to Python primitives for JSON serialization
|
|
802
|
-
obj_type = type(obj).__name__
|
|
803
721
|
if obj_type in ('float16', 'float32', 'float64'):
|
|
804
722
|
return float(obj)
|
|
805
723
|
if obj_type in ('int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'):
|
|
@@ -918,10 +836,52 @@ def main():
|
|
|
918
836
|
if p not in sys.path:
|
|
919
837
|
sys.path.insert(0, p)
|
|
920
838
|
|
|
921
|
-
#
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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")
|
|
925
885
|
|
|
926
886
|
# Signal ready
|
|
927
887
|
transport.send({"status": "ready"})
|
|
@@ -950,23 +910,20 @@ def main():
|
|
|
950
910
|
transport.send({"status": "pong"})
|
|
951
911
|
continue
|
|
952
912
|
|
|
913
|
+
shm_registry = []
|
|
953
914
|
try:
|
|
954
915
|
request_type = request.get("type", "call_module")
|
|
955
916
|
module_name = request["module"]
|
|
956
|
-
inputs_path = request.get("inputs_path")
|
|
957
|
-
outputs_path = request.get("outputs_path")
|
|
958
917
|
wlog(f"[worker] Request: {request_type} {module_name}")
|
|
959
918
|
|
|
960
|
-
# Load inputs
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
inputs
|
|
964
|
-
|
|
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)
|
|
965
924
|
inputs = _deserialize_isolated_objects(inputs)
|
|
966
|
-
# Resolve any object references from previous node calls
|
|
967
|
-
wlog(f"[worker] Resolving object references...")
|
|
968
925
|
inputs = _deserialize_input(inputs)
|
|
969
|
-
wlog(f"[worker] Inputs ready: {list(inputs.keys())}")
|
|
926
|
+
wlog(f"[worker] Inputs ready: {list(inputs.keys()) if isinstance(inputs, dict) else type(inputs)}")
|
|
970
927
|
else:
|
|
971
928
|
inputs = {}
|
|
972
929
|
|
|
@@ -995,14 +952,17 @@ def main():
|
|
|
995
952
|
func = getattr(module, func_name)
|
|
996
953
|
result = func(**inputs)
|
|
997
954
|
|
|
998
|
-
#
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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")
|
|
1002
959
|
|
|
1003
|
-
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
|
|
1004
962
|
|
|
1005
963
|
except Exception as e:
|
|
964
|
+
# Cleanup shm on error since host won't read it
|
|
965
|
+
_cleanup_shm(shm_registry)
|
|
1006
966
|
transport.send({
|
|
1007
967
|
"status": "error",
|
|
1008
968
|
"error": str(e),
|
|
@@ -1016,9 +976,9 @@ if __name__ == "__main__":
|
|
|
1016
976
|
'''
|
|
1017
977
|
|
|
1018
978
|
|
|
1019
|
-
class
|
|
979
|
+
class SubprocessWorker(Worker):
|
|
1020
980
|
"""
|
|
1021
|
-
|
|
981
|
+
Cross-venv worker using persistent subprocess + socket IPC.
|
|
1022
982
|
|
|
1023
983
|
Uses Unix domain sockets (or TCP localhost on older Windows) for IPC.
|
|
1024
984
|
This completely separates IPC from stdout/stderr, so C libraries
|
|
@@ -1027,11 +987,11 @@ class PersistentVenvWorker(Worker):
|
|
|
1027
987
|
Benefits:
|
|
1028
988
|
- Works on Windows with different venv Python (full isolation)
|
|
1029
989
|
- Compiled CUDA extensions load correctly in the venv
|
|
1030
|
-
- ~50-100ms per call (
|
|
990
|
+
- ~50-100ms per call (persistent subprocess avoids spawn overhead)
|
|
1031
991
|
- Tensor transfer via shared memory files
|
|
1032
992
|
- Immune to stdout pollution from C libraries
|
|
1033
993
|
|
|
1034
|
-
Use this for
|
|
994
|
+
Use this for calls to isolated venvs with different Python/dependencies.
|
|
1035
995
|
"""
|
|
1036
996
|
|
|
1037
997
|
def __init__(
|
|
@@ -1058,7 +1018,7 @@ class PersistentVenvWorker(Worker):
|
|
|
1058
1018
|
self.working_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
1059
1019
|
self.sys_path = sys_path or []
|
|
1060
1020
|
self.extra_env = env or {}
|
|
1061
|
-
self.name = name or f"
|
|
1021
|
+
self.name = name or f"SubprocessWorker({self.python.parent.parent.name})"
|
|
1062
1022
|
|
|
1063
1023
|
if not self.python.exists():
|
|
1064
1024
|
raise FileNotFoundError(f"Python not found: {self.python}")
|
|
@@ -1204,21 +1164,26 @@ class PersistentVenvWorker(Worker):
|
|
|
1204
1164
|
# Use UTF-8 encoding for stdout/stderr to handle Unicode symbols
|
|
1205
1165
|
env["PYTHONIOENCODING"] = "utf-8"
|
|
1206
1166
|
|
|
1207
|
-
# 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
|
|
1208
1170
|
comfyui_base = self._find_comfyui_base()
|
|
1209
1171
|
if comfyui_base:
|
|
1210
|
-
env["COMFYUI_BASE"] = str(comfyui_base)
|
|
1172
|
+
env["COMFYUI_BASE"] = str(comfyui_base) # Keep for fallback/debugging
|
|
1211
1173
|
|
|
1212
|
-
#
|
|
1213
|
-
|
|
1214
|
-
|
|
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)
|
|
1215
1180
|
|
|
1216
1181
|
# Launch subprocess with the venv Python, passing socket address
|
|
1217
1182
|
# For pixi environments, use "pixi run python" to get proper environment activation
|
|
1218
1183
|
# (CONDA_PREFIX, Library paths, etc.) which fixes DLL loading issues with bpy
|
|
1219
1184
|
is_pixi = '.pixi' in str(self.python)
|
|
1220
1185
|
if _DEBUG:
|
|
1221
|
-
print(f"[
|
|
1186
|
+
print(f"[SubprocessWorker] is_pixi={is_pixi}, python={self.python}", flush=True)
|
|
1222
1187
|
if is_pixi:
|
|
1223
1188
|
# Find pixi project root (parent of .pixi directory)
|
|
1224
1189
|
pixi_project = self.python
|
|
@@ -1227,7 +1192,7 @@ class PersistentVenvWorker(Worker):
|
|
|
1227
1192
|
pixi_project = pixi_project.parent # Go up from .pixi to project root
|
|
1228
1193
|
pixi_toml = pixi_project / "pixi.toml"
|
|
1229
1194
|
if _DEBUG:
|
|
1230
|
-
print(f"[
|
|
1195
|
+
print(f"[SubprocessWorker] pixi_toml={pixi_toml}, exists={pixi_toml.exists()}", flush=True)
|
|
1231
1196
|
|
|
1232
1197
|
if pixi_toml.exists():
|
|
1233
1198
|
pixi_exe = get_pixi_path()
|
|
@@ -1254,15 +1219,15 @@ class PersistentVenvWorker(Worker):
|
|
|
1254
1219
|
launch_env = env
|
|
1255
1220
|
|
|
1256
1221
|
if _DEBUG:
|
|
1257
|
-
print(f"[
|
|
1222
|
+
print(f"[SubprocessWorker] launching cmd={cmd[:3]}...", flush=True)
|
|
1258
1223
|
if launch_env:
|
|
1259
1224
|
path_sep = ";" if sys.platform == "win32" else ":"
|
|
1260
1225
|
path_parts = launch_env.get("PATH", "").split(path_sep)
|
|
1261
|
-
print(f"[
|
|
1226
|
+
print(f"[SubprocessWorker] PATH has {len(path_parts)} entries:", flush=True)
|
|
1262
1227
|
for i, p in enumerate(path_parts[:10]): # Show first 10
|
|
1263
|
-
print(f"[
|
|
1228
|
+
print(f"[SubprocessWorker] [{i}] {p}", flush=True)
|
|
1264
1229
|
if len(path_parts) > 10:
|
|
1265
|
-
print(f"[
|
|
1230
|
+
print(f"[SubprocessWorker] ... and {len(path_parts) - 10} more", flush=True)
|
|
1266
1231
|
self._process = subprocess.Popen(
|
|
1267
1232
|
cmd,
|
|
1268
1233
|
stdin=subprocess.DEVNULL,
|
|
@@ -1349,9 +1314,21 @@ class PersistentVenvWorker(Worker):
|
|
|
1349
1314
|
# Send request
|
|
1350
1315
|
self._transport.send(request)
|
|
1351
1316
|
|
|
1352
|
-
# Read response with timeout
|
|
1317
|
+
# Read response with timeout, handling log messages along the way
|
|
1353
1318
|
try:
|
|
1354
|
-
|
|
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
|
|
1355
1332
|
except ConnectionError as e:
|
|
1356
1333
|
# Socket closed - check if worker process died
|
|
1357
1334
|
self._shutdown = True
|
|
@@ -1414,51 +1391,43 @@ class PersistentVenvWorker(Worker):
|
|
|
1414
1391
|
"""
|
|
1415
1392
|
import sys
|
|
1416
1393
|
if _DEBUG:
|
|
1417
|
-
print(f"[
|
|
1394
|
+
print(f"[SubprocessWorker] call_method: {module_name}.{class_name}.{method_name}", file=sys.stderr, flush=True)
|
|
1418
1395
|
|
|
1419
1396
|
with self._lock:
|
|
1420
1397
|
if _DEBUG:
|
|
1421
|
-
print(f"[
|
|
1398
|
+
print(f"[SubprocessWorker] acquired lock, ensuring started...", file=sys.stderr, flush=True)
|
|
1422
1399
|
self._ensure_started()
|
|
1423
1400
|
if _DEBUG:
|
|
1424
|
-
print(f"[
|
|
1401
|
+
print(f"[SubprocessWorker] worker started/confirmed", file=sys.stderr, flush=True)
|
|
1425
1402
|
|
|
1426
1403
|
timeout = timeout or 600.0
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
import torch
|
|
1430
|
-
inputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_in.pt"
|
|
1431
|
-
outputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_out.pt"
|
|
1404
|
+
shm_registry = []
|
|
1432
1405
|
|
|
1433
1406
|
try:
|
|
1434
|
-
# Serialize kwargs
|
|
1407
|
+
# Serialize kwargs to shared memory
|
|
1435
1408
|
if kwargs:
|
|
1436
1409
|
if _DEBUG:
|
|
1437
|
-
print(f"[
|
|
1438
|
-
|
|
1439
|
-
if _DEBUG:
|
|
1440
|
-
print(f"[PersistentVenvWorker] saving to {inputs_path}...", file=sys.stderr, flush=True)
|
|
1441
|
-
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)
|
|
1442
1412
|
if _DEBUG:
|
|
1443
|
-
print(f"[
|
|
1413
|
+
print(f"[SubprocessWorker] created {len(shm_registry)} shm blocks", file=sys.stderr, flush=True)
|
|
1414
|
+
else:
|
|
1415
|
+
kwargs_meta = None
|
|
1444
1416
|
|
|
1445
|
-
# Send request with
|
|
1446
|
-
# Serialize self_state to handle Path objects and other non-JSON types
|
|
1447
|
-
serialized_self_state = _serialize_for_ipc(self_state) if self_state else None
|
|
1417
|
+
# Send request with shared memory metadata
|
|
1448
1418
|
request = {
|
|
1449
1419
|
"type": "call_method",
|
|
1450
1420
|
"module": module_name,
|
|
1451
1421
|
"class_name": class_name,
|
|
1452
1422
|
"method_name": method_name,
|
|
1453
|
-
"self_state":
|
|
1454
|
-
"
|
|
1455
|
-
"outputs_path": str(outputs_path),
|
|
1423
|
+
"self_state": _serialize_for_ipc(self_state) if self_state else None,
|
|
1424
|
+
"kwargs": kwargs_meta,
|
|
1456
1425
|
}
|
|
1457
1426
|
if _DEBUG:
|
|
1458
|
-
print(f"[
|
|
1427
|
+
print(f"[SubprocessWorker] sending request via socket...", file=sys.stderr, flush=True)
|
|
1459
1428
|
response = self._send_request(request, timeout)
|
|
1460
1429
|
if _DEBUG:
|
|
1461
|
-
print(f"[
|
|
1430
|
+
print(f"[SubprocessWorker] got response: {response.get('status')}", file=sys.stderr, flush=True)
|
|
1462
1431
|
|
|
1463
1432
|
if response.get("status") == "error":
|
|
1464
1433
|
raise WorkerError(
|
|
@@ -1466,16 +1435,14 @@ class PersistentVenvWorker(Worker):
|
|
|
1466
1435
|
traceback=response.get("traceback"),
|
|
1467
1436
|
)
|
|
1468
1437
|
|
|
1469
|
-
|
|
1470
|
-
|
|
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)
|
|
1471
1442
|
return None
|
|
1472
1443
|
|
|
1473
1444
|
finally:
|
|
1474
|
-
|
|
1475
|
-
try:
|
|
1476
|
-
p.unlink()
|
|
1477
|
-
except:
|
|
1478
|
-
pass
|
|
1445
|
+
_cleanup_shm(shm_registry)
|
|
1479
1446
|
|
|
1480
1447
|
def call_module(
|
|
1481
1448
|
self,
|
|
@@ -1489,25 +1456,16 @@ class PersistentVenvWorker(Worker):
|
|
|
1489
1456
|
self._ensure_started()
|
|
1490
1457
|
|
|
1491
1458
|
timeout = timeout or 600.0
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
# Save inputs
|
|
1495
|
-
import torch
|
|
1496
|
-
inputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_in.pt"
|
|
1497
|
-
outputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_out.pt"
|
|
1459
|
+
shm_registry = []
|
|
1498
1460
|
|
|
1499
1461
|
try:
|
|
1500
|
-
if kwargs
|
|
1501
|
-
serialized_kwargs = _serialize_for_ipc(kwargs)
|
|
1502
|
-
torch.save(serialized_kwargs, str(inputs_path))
|
|
1462
|
+
kwargs_meta = _to_shm(kwargs, shm_registry) if kwargs else None
|
|
1503
1463
|
|
|
1504
|
-
# Send request
|
|
1505
1464
|
request = {
|
|
1506
1465
|
"type": "call_module",
|
|
1507
1466
|
"module": module,
|
|
1508
1467
|
"func": func,
|
|
1509
|
-
"
|
|
1510
|
-
"outputs_path": str(outputs_path),
|
|
1468
|
+
"kwargs": kwargs_meta,
|
|
1511
1469
|
}
|
|
1512
1470
|
response = self._send_request(request, timeout)
|
|
1513
1471
|
|
|
@@ -1517,17 +1475,13 @@ class PersistentVenvWorker(Worker):
|
|
|
1517
1475
|
traceback=response.get("traceback"),
|
|
1518
1476
|
)
|
|
1519
1477
|
|
|
1520
|
-
|
|
1521
|
-
if
|
|
1522
|
-
return
|
|
1478
|
+
result_meta = response.get("result")
|
|
1479
|
+
if result_meta is not None:
|
|
1480
|
+
return _from_shm(result_meta)
|
|
1523
1481
|
return None
|
|
1524
1482
|
|
|
1525
1483
|
finally:
|
|
1526
|
-
|
|
1527
|
-
try:
|
|
1528
|
-
p.unlink()
|
|
1529
|
-
except:
|
|
1530
|
-
pass
|
|
1484
|
+
_cleanup_shm(shm_registry)
|
|
1531
1485
|
|
|
1532
1486
|
def shutdown(self) -> None:
|
|
1533
1487
|
"""Shut down the persistent worker."""
|
|
@@ -1579,4 +1533,4 @@ class PersistentVenvWorker(Worker):
|
|
|
1579
1533
|
|
|
1580
1534
|
def __repr__(self):
|
|
1581
1535
|
status = "alive" if self.is_alive() else "stopped"
|
|
1582
|
-
return f"<
|
|
1536
|
+
return f"<SubprocessWorker name={self.name!r} status={status}>"
|