comfy-env 0.1.20__tar.gz → 0.1.22__tar.gz
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-0.1.20 → comfy_env-0.1.22}/PKG-INFO +2 -1
- {comfy_env-0.1.20 → comfy_env-0.1.22}/pyproject.toml +2 -1
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/__init__.py +0 -2
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/cli.py +15 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/install.py +4 -1
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/isolation/__init__.py +0 -2
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/isolation/workers/__init__.py +1 -3
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/isolation/workers/base.py +1 -1
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/isolation/workers/subprocess.py +213 -17
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/isolation/wrap.py +6 -14
- comfy_env-0.1.20/src/comfy_env/isolation/workers/mp.py +0 -875
- {comfy_env-0.1.20 → comfy_env-0.1.22}/.github/workflows/ci.yml +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/.github/workflows/publish.yml +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/.gitignore +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/LICENSE +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/README.md +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/config/__init__.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/config/parser.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/config/types.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/detection/__init__.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/detection/cuda.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/detection/gpu.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/detection/platform.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/detection/runtime.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/environment/__init__.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/environment/cache.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/environment/libomp.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/environment/paths.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/environment/setup.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/isolation/tensor_utils.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/packages/__init__.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/packages/apt.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/packages/cuda_wheels.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/packages/node_dependencies.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/packages/pixi.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/packages/toml_generator.py +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
- {comfy_env-0.1.20 → comfy_env-0.1.22}/src/comfy_env/templates/comfy-env.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comfy-env
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.22
|
|
4
4
|
Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
|
|
5
5
|
Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
|
|
6
6
|
Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: numpy
|
|
20
21
|
Requires-Dist: pip>=21.0
|
|
21
22
|
Requires-Dist: tomli-w>=1.0.0
|
|
22
23
|
Requires-Dist: tomli>=2.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "comfy-env"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.22"
|
|
4
4
|
description = "Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -23,6 +23,7 @@ dependencies = [
|
|
|
23
23
|
"tomli-w>=1.0.0",
|
|
24
24
|
"uv>=0.4.0",
|
|
25
25
|
"pip>=21.0",
|
|
26
|
+
"numpy",
|
|
26
27
|
]
|
|
27
28
|
|
|
28
29
|
[project.optional-dependencies]
|
|
@@ -108,7 +108,6 @@ from .isolation import (
|
|
|
108
108
|
# Workers
|
|
109
109
|
Worker,
|
|
110
110
|
WorkerError,
|
|
111
|
-
MPWorker,
|
|
112
111
|
SubprocessWorker,
|
|
113
112
|
# Tensor utilities
|
|
114
113
|
TensorKeeper,
|
|
@@ -168,7 +167,6 @@ __all__ = [
|
|
|
168
167
|
# Workers
|
|
169
168
|
"Worker",
|
|
170
169
|
"WorkerError",
|
|
171
|
-
"MPWorker",
|
|
172
170
|
"SubprocessWorker",
|
|
173
171
|
"TensorKeeper",
|
|
174
172
|
]
|
|
@@ -44,6 +44,9 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
44
44
|
p.add_argument("--config", "-c", type=str, help="Config path")
|
|
45
45
|
p.add_argument("--dry-run", action="store_true", help="Preview only")
|
|
46
46
|
|
|
47
|
+
# cleanup
|
|
48
|
+
sub.add_parser("cleanup", help="Remove orphaned environments")
|
|
49
|
+
|
|
47
50
|
parsed = parser.parse_args(args)
|
|
48
51
|
if not parsed.command:
|
|
49
52
|
parser.print_help()
|
|
@@ -52,6 +55,7 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
52
55
|
commands = {
|
|
53
56
|
"init": cmd_init, "generate": cmd_generate, "install": cmd_install,
|
|
54
57
|
"info": cmd_info, "doctor": cmd_doctor, "apt-install": cmd_apt_install,
|
|
58
|
+
"cleanup": cmd_cleanup,
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
try:
|
|
@@ -237,5 +241,16 @@ def cmd_apt_install(args) -> int:
|
|
|
237
241
|
return result.returncode
|
|
238
242
|
|
|
239
243
|
|
|
244
|
+
def cmd_cleanup(args) -> int:
|
|
245
|
+
from .environment.cache import cleanup_orphaned_envs
|
|
246
|
+
print("Cleaning orphaned environments...")
|
|
247
|
+
cleaned = cleanup_orphaned_envs()
|
|
248
|
+
if cleaned:
|
|
249
|
+
print(f"Removed {cleaned} orphaned environment(s)")
|
|
250
|
+
else:
|
|
251
|
+
print("No orphaned environments found")
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
|
|
240
255
|
if __name__ == "__main__":
|
|
241
256
|
sys.exit(main())
|
|
@@ -116,9 +116,12 @@ def _install_via_pixi(cfg: ComfyEnvConfig, node_dir: Path, log: Callable[[str],
|
|
|
116
116
|
from .packages.toml_generator import write_pixi_toml
|
|
117
117
|
from .packages.cuda_wheels import get_wheel_url, CUDA_TORCH_MAP
|
|
118
118
|
from .detection import get_recommended_cuda_version
|
|
119
|
-
from .environment.cache import get_central_env_path, write_marker, write_env_metadata, MARKER_FILE, get_cache_dir
|
|
119
|
+
from .environment.cache import get_central_env_path, write_marker, write_env_metadata, MARKER_FILE, get_cache_dir, cleanup_orphaned_envs
|
|
120
120
|
import shutil, subprocess, sys
|
|
121
121
|
|
|
122
|
+
# Clean up orphaned environments before installing
|
|
123
|
+
cleanup_orphaned_envs(log)
|
|
124
|
+
|
|
122
125
|
deps = cfg.pixi_passthrough.get("dependencies", {})
|
|
123
126
|
pypi_deps = cfg.pixi_passthrough.get("pypi-dependencies", {})
|
|
124
127
|
if not cfg.cuda_packages and not deps and not pypi_deps:
|
|
@@ -11,7 +11,6 @@ from .wrap import (
|
|
|
11
11
|
from .workers import (
|
|
12
12
|
Worker,
|
|
13
13
|
WorkerError,
|
|
14
|
-
MPWorker,
|
|
15
14
|
SubprocessWorker,
|
|
16
15
|
)
|
|
17
16
|
from .tensor_utils import (
|
|
@@ -29,7 +28,6 @@ __all__ = [
|
|
|
29
28
|
# Workers
|
|
30
29
|
"Worker",
|
|
31
30
|
"WorkerError",
|
|
32
|
-
"MPWorker",
|
|
33
31
|
"SubprocessWorker",
|
|
34
32
|
# Tensor utilities
|
|
35
33
|
"TensorKeeper",
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Workers - Process isolation implementations.
|
|
3
3
|
|
|
4
|
-
Provides
|
|
4
|
+
Provides subprocess-based workers for isolated execution.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .base import Worker, WorkerError
|
|
8
|
-
from .mp import MPWorker
|
|
9
8
|
from .subprocess import SubprocessWorker
|
|
10
9
|
|
|
11
10
|
__all__ = [
|
|
12
11
|
"Worker",
|
|
13
12
|
"WorkerError",
|
|
14
|
-
"MPWorker",
|
|
15
13
|
"SubprocessWorker",
|
|
16
14
|
]
|
|
@@ -297,6 +297,45 @@ def _to_shm(obj, registry, visited=None):
|
|
|
297
297
|
return obj
|
|
298
298
|
|
|
299
299
|
|
|
300
|
+
def _deserialize_tensor_ref(data):
|
|
301
|
+
"""Deserialize tensor from PyTorch shared memory (TensorRef format)."""
|
|
302
|
+
import torch
|
|
303
|
+
import torch.multiprocessing.reductions as reductions
|
|
304
|
+
|
|
305
|
+
dtype_str = data["dtype"]
|
|
306
|
+
dtype = getattr(torch, dtype_str.split(".")[-1])
|
|
307
|
+
|
|
308
|
+
manager_path = data["manager_path"]
|
|
309
|
+
storage_key = data["storage_key"]
|
|
310
|
+
storage_size = data["storage_size"]
|
|
311
|
+
|
|
312
|
+
# Encode to bytes if needed
|
|
313
|
+
if isinstance(manager_path, str):
|
|
314
|
+
manager_path = manager_path.encode("utf-8")
|
|
315
|
+
if isinstance(storage_key, str):
|
|
316
|
+
storage_key = storage_key.encode("utf-8")
|
|
317
|
+
|
|
318
|
+
# Rebuild storage from shared memory file
|
|
319
|
+
rebuilt_storage = reductions.rebuild_storage_filename(
|
|
320
|
+
torch.UntypedStorage, manager_path, storage_key, storage_size
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Wrap in TypedStorage
|
|
324
|
+
typed_storage = torch.storage.TypedStorage(
|
|
325
|
+
wrap_storage=rebuilt_storage, dtype=dtype, _internal=True
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Rebuild tensor
|
|
329
|
+
metadata = (
|
|
330
|
+
data["tensor_offset"],
|
|
331
|
+
tuple(data["tensor_size"]),
|
|
332
|
+
tuple(data["tensor_stride"]),
|
|
333
|
+
data["requires_grad"],
|
|
334
|
+
)
|
|
335
|
+
tensor = reductions.rebuild_tensor(torch.Tensor, typed_storage, metadata)
|
|
336
|
+
return tensor
|
|
337
|
+
|
|
338
|
+
|
|
300
339
|
def _from_shm(obj, unlink=True):
|
|
301
340
|
"""Reconstruct object from shared memory metadata."""
|
|
302
341
|
if not isinstance(obj, dict):
|
|
@@ -304,7 +343,15 @@ def _from_shm(obj, unlink=True):
|
|
|
304
343
|
return [_from_shm(v, unlink) for v in obj]
|
|
305
344
|
return obj
|
|
306
345
|
|
|
307
|
-
#
|
|
346
|
+
# TensorRef -> use PyTorch's native deserialization (new format)
|
|
347
|
+
if obj.get("__type__") == "TensorRef":
|
|
348
|
+
tensor = _deserialize_tensor_ref(obj)
|
|
349
|
+
# Convert back to numpy if it was originally numpy
|
|
350
|
+
if obj.get("__was_numpy__"):
|
|
351
|
+
return tensor.numpy()
|
|
352
|
+
return tensor
|
|
353
|
+
|
|
354
|
+
# numpy array (or tensor that was converted to numpy) - legacy format
|
|
308
355
|
if "__shm_np__" in obj:
|
|
309
356
|
block = shm.SharedMemory(name=obj["__shm_np__"])
|
|
310
357
|
arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
|
|
@@ -362,10 +409,10 @@ def _serialize_for_ipc(obj, visited=None):
|
|
|
362
409
|
if obj_id in visited:
|
|
363
410
|
return visited[obj_id] # Return cached serialized result
|
|
364
411
|
|
|
365
|
-
# Handle Path objects -
|
|
412
|
+
# Handle Path objects - mark for reconstruction
|
|
366
413
|
from pathlib import PurePath
|
|
367
414
|
if isinstance(obj, PurePath):
|
|
368
|
-
return str(obj)
|
|
415
|
+
return {"__path__": str(obj)}
|
|
369
416
|
|
|
370
417
|
# Check if this is a custom object with broken module path
|
|
371
418
|
if (hasattr(obj, '__dict__') and
|
|
@@ -434,6 +481,8 @@ import socket
|
|
|
434
481
|
import struct
|
|
435
482
|
import traceback
|
|
436
483
|
import faulthandler
|
|
484
|
+
import collections
|
|
485
|
+
import time
|
|
437
486
|
from types import SimpleNamespace
|
|
438
487
|
|
|
439
488
|
# Enable faulthandler to dump traceback on SIGSEGV/SIGABRT/etc
|
|
@@ -533,6 +582,34 @@ if sys.platform == "win32":
|
|
|
533
582
|
from multiprocessing import shared_memory as shm
|
|
534
583
|
import numpy as np
|
|
535
584
|
|
|
585
|
+
# Set PyTorch to use file_system sharing (uses /dev/shm, no resource_tracker)
|
|
586
|
+
try:
|
|
587
|
+
import torch
|
|
588
|
+
import torch.multiprocessing as mp
|
|
589
|
+
mp.set_sharing_strategy("file_system")
|
|
590
|
+
wlog("[worker] PyTorch sharing strategy set to file_system")
|
|
591
|
+
except ImportError:
|
|
592
|
+
wlog("[worker] PyTorch not available")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# Tensor keeper - holds tensor references to prevent GC before parent reads shared memory
|
|
596
|
+
class TensorKeeper:
|
|
597
|
+
"""Keep tensors alive for a retention period to prevent shared memory deletion."""
|
|
598
|
+
def __init__(self, retention_seconds=30.0):
|
|
599
|
+
self.retention_seconds = retention_seconds
|
|
600
|
+
self._keeper = collections.deque()
|
|
601
|
+
self._lock = threading.Lock()
|
|
602
|
+
|
|
603
|
+
def keep(self, t):
|
|
604
|
+
now = time.time()
|
|
605
|
+
with self._lock:
|
|
606
|
+
self._keeper.append((now, t))
|
|
607
|
+
# Cleanup old entries
|
|
608
|
+
while self._keeper and now - self._keeper[0][0] > self.retention_seconds:
|
|
609
|
+
self._keeper.popleft()
|
|
610
|
+
|
|
611
|
+
_tensor_keeper = TensorKeeper()
|
|
612
|
+
|
|
536
613
|
|
|
537
614
|
def _prepare_trimesh_for_pickle(mesh):
|
|
538
615
|
"""
|
|
@@ -548,6 +625,43 @@ def _prepare_trimesh_for_pickle(mesh):
|
|
|
548
625
|
return mesh
|
|
549
626
|
|
|
550
627
|
|
|
628
|
+
def _serialize_tensor_native(t, registry):
|
|
629
|
+
"""Serialize tensor using PyTorch's native shared memory (no resource_tracker)."""
|
|
630
|
+
import torch
|
|
631
|
+
import torch.multiprocessing.reductions as reductions
|
|
632
|
+
|
|
633
|
+
# Keep tensor alive until parent reads it
|
|
634
|
+
_tensor_keeper.keep(t)
|
|
635
|
+
|
|
636
|
+
# Put tensor in shared memory via PyTorch's manager
|
|
637
|
+
if not t.is_shared():
|
|
638
|
+
t.share_memory_()
|
|
639
|
+
|
|
640
|
+
storage = t.untyped_storage()
|
|
641
|
+
sfunc, sargs = reductions.reduce_storage(storage)
|
|
642
|
+
|
|
643
|
+
if sfunc.__name__ == "rebuild_storage_filename":
|
|
644
|
+
# sargs: (cls, manager_path, storage_key, size)
|
|
645
|
+
return {
|
|
646
|
+
"__type__": "TensorRef",
|
|
647
|
+
"strategy": "file_system",
|
|
648
|
+
"manager_path": sargs[1].decode("utf-8") if isinstance(sargs[1], bytes) else sargs[1],
|
|
649
|
+
"storage_key": sargs[2].decode("utf-8") if isinstance(sargs[2], bytes) else sargs[2],
|
|
650
|
+
"storage_size": sargs[3],
|
|
651
|
+
"dtype": str(t.dtype),
|
|
652
|
+
"tensor_size": list(t.size()),
|
|
653
|
+
"tensor_stride": list(t.stride()),
|
|
654
|
+
"tensor_offset": t.storage_offset(),
|
|
655
|
+
"requires_grad": t.requires_grad,
|
|
656
|
+
}
|
|
657
|
+
else:
|
|
658
|
+
# Fallback: force file_system strategy
|
|
659
|
+
import torch.multiprocessing as mp
|
|
660
|
+
mp.set_sharing_strategy("file_system")
|
|
661
|
+
t.share_memory_()
|
|
662
|
+
return _serialize_tensor_native(t, registry)
|
|
663
|
+
|
|
664
|
+
|
|
551
665
|
def _to_shm(obj, registry, visited=None):
|
|
552
666
|
"""Serialize to shared memory. Returns JSON-safe metadata."""
|
|
553
667
|
if visited is None:
|
|
@@ -557,22 +671,26 @@ def _to_shm(obj, registry, visited=None):
|
|
|
557
671
|
return visited[obj_id]
|
|
558
672
|
t = type(obj).__name__
|
|
559
673
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
registry
|
|
565
|
-
result = {"__shm_np__": block.name, "shape": list(arr.shape), "dtype": str(arr.dtype)}
|
|
674
|
+
# Tensor -> use PyTorch's native shared memory (bypasses resource_tracker)
|
|
675
|
+
if t == 'Tensor':
|
|
676
|
+
import torch
|
|
677
|
+
tensor = obj.detach().cpu().contiguous()
|
|
678
|
+
result = _serialize_tensor_native(tensor, registry)
|
|
566
679
|
visited[obj_id] = result
|
|
567
680
|
return result
|
|
568
681
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
682
|
+
# ndarray -> convert to tensor, use PyTorch's native shared memory
|
|
683
|
+
if t == 'ndarray':
|
|
684
|
+
import torch
|
|
685
|
+
arr = np.ascontiguousarray(obj)
|
|
686
|
+
tensor = torch.from_numpy(arr)
|
|
687
|
+
result = _serialize_tensor_native(tensor, registry)
|
|
688
|
+
result["__was_numpy__"] = True
|
|
689
|
+
result["numpy_dtype"] = str(arr.dtype)
|
|
690
|
+
visited[obj_id] = result
|
|
573
691
|
return result
|
|
574
692
|
|
|
575
|
-
# trimesh.Trimesh -> pickle -> shared memory
|
|
693
|
+
# trimesh.Trimesh -> pickle -> shared memory
|
|
576
694
|
if t == 'Trimesh':
|
|
577
695
|
import pickle
|
|
578
696
|
obj = _prepare_trimesh_for_pickle(obj)
|
|
@@ -601,12 +719,62 @@ def _to_shm(obj, registry, visited=None):
|
|
|
601
719
|
|
|
602
720
|
return obj
|
|
603
721
|
|
|
722
|
+
|
|
723
|
+
def _deserialize_tensor_native(data):
|
|
724
|
+
"""Deserialize tensor from PyTorch shared memory."""
|
|
725
|
+
import torch
|
|
726
|
+
import torch.multiprocessing.reductions as reductions
|
|
727
|
+
|
|
728
|
+
dtype_str = data["dtype"]
|
|
729
|
+
dtype = getattr(torch, dtype_str.split(".")[-1])
|
|
730
|
+
|
|
731
|
+
manager_path = data["manager_path"]
|
|
732
|
+
storage_key = data["storage_key"]
|
|
733
|
+
storage_size = data["storage_size"]
|
|
734
|
+
|
|
735
|
+
# Encode to bytes if needed
|
|
736
|
+
if isinstance(manager_path, str):
|
|
737
|
+
manager_path = manager_path.encode("utf-8")
|
|
738
|
+
if isinstance(storage_key, str):
|
|
739
|
+
storage_key = storage_key.encode("utf-8")
|
|
740
|
+
|
|
741
|
+
# Rebuild storage from shared memory file
|
|
742
|
+
rebuilt_storage = reductions.rebuild_storage_filename(
|
|
743
|
+
torch.UntypedStorage, manager_path, storage_key, storage_size
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Wrap in TypedStorage
|
|
747
|
+
typed_storage = torch.storage.TypedStorage(
|
|
748
|
+
wrap_storage=rebuilt_storage, dtype=dtype, _internal=True
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# Rebuild tensor
|
|
752
|
+
metadata = (
|
|
753
|
+
data["tensor_offset"],
|
|
754
|
+
tuple(data["tensor_size"]),
|
|
755
|
+
tuple(data["tensor_stride"]),
|
|
756
|
+
data["requires_grad"],
|
|
757
|
+
)
|
|
758
|
+
tensor = reductions.rebuild_tensor(torch.Tensor, typed_storage, metadata)
|
|
759
|
+
return tensor
|
|
760
|
+
|
|
761
|
+
|
|
604
762
|
def _from_shm(obj):
|
|
605
763
|
"""Reconstruct from shared memory metadata. Does NOT unlink - caller handles that."""
|
|
606
764
|
if not isinstance(obj, dict):
|
|
607
765
|
if isinstance(obj, list):
|
|
608
766
|
return [_from_shm(v) for v in obj]
|
|
609
767
|
return obj
|
|
768
|
+
|
|
769
|
+
# TensorRef -> use PyTorch's native deserialization (new format, worker->parent)
|
|
770
|
+
if obj.get("__type__") == "TensorRef":
|
|
771
|
+
tensor = _deserialize_tensor_native(obj)
|
|
772
|
+
# Convert back to numpy if it was originally numpy
|
|
773
|
+
if obj.get("__was_numpy__"):
|
|
774
|
+
return tensor.numpy()
|
|
775
|
+
return tensor
|
|
776
|
+
|
|
777
|
+
# __shm_np__ -> legacy format (parent->worker, uses Python SharedMemory)
|
|
610
778
|
if "__shm_np__" in obj:
|
|
611
779
|
block = shm.SharedMemory(name=obj["__shm_np__"])
|
|
612
780
|
arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
|
|
@@ -616,13 +784,16 @@ def _from_shm(obj):
|
|
|
616
784
|
import torch
|
|
617
785
|
return torch.from_numpy(arr)
|
|
618
786
|
return arr
|
|
619
|
-
|
|
787
|
+
|
|
788
|
+
# trimesh (pickled)
|
|
620
789
|
if "__shm_trimesh__" in obj:
|
|
621
790
|
import pickle
|
|
622
791
|
block = shm.SharedMemory(name=obj["name"])
|
|
623
792
|
mesh_bytes = bytes(block.buf[:obj["size"]])
|
|
624
793
|
block.close()
|
|
794
|
+
block.unlink()
|
|
625
795
|
return pickle.loads(mesh_bytes)
|
|
796
|
+
|
|
626
797
|
return {k: _from_shm(v) for k, v in obj.items()}
|
|
627
798
|
|
|
628
799
|
def _cleanup_shm(registry):
|
|
@@ -634,6 +805,25 @@ def _cleanup_shm(registry):
|
|
|
634
805
|
pass
|
|
635
806
|
registry.clear()
|
|
636
807
|
|
|
808
|
+
# Shared memory keeper - holds references to prevent premature GC
|
|
809
|
+
class ShmKeeper:
|
|
810
|
+
"""Keep shm blocks alive for a retention period to prevent race conditions."""
|
|
811
|
+
def __init__(self, retention_seconds=30.0):
|
|
812
|
+
self.retention_seconds = retention_seconds
|
|
813
|
+
self._keeper = collections.deque()
|
|
814
|
+
self._lock = threading.Lock()
|
|
815
|
+
|
|
816
|
+
def keep(self, blocks):
|
|
817
|
+
now = time.time()
|
|
818
|
+
with self._lock:
|
|
819
|
+
self._keeper.append((now, list(blocks))) # Copy the list
|
|
820
|
+
# Cleanup old entries
|
|
821
|
+
while self._keeper and now - self._keeper[0][0] > self.retention_seconds:
|
|
822
|
+
old_time, old_blocks = self._keeper.popleft()
|
|
823
|
+
_cleanup_shm(old_blocks)
|
|
824
|
+
|
|
825
|
+
_shm_keeper = ShmKeeper()
|
|
826
|
+
|
|
637
827
|
# =============================================================================
|
|
638
828
|
# Object Reference System - keep complex objects in worker, pass refs to host
|
|
639
829
|
# =============================================================================
|
|
@@ -796,6 +986,9 @@ def _connect(addr):
|
|
|
796
986
|
def _deserialize_isolated_objects(obj):
|
|
797
987
|
"""Reconstruct objects serialized with __isolated_object__ marker."""
|
|
798
988
|
if isinstance(obj, dict):
|
|
989
|
+
if obj.get("__path__"):
|
|
990
|
+
from pathlib import Path
|
|
991
|
+
return Path(obj["__path__"])
|
|
799
992
|
if obj.get("__isolated_object__"):
|
|
800
993
|
attrs = {k: _deserialize_isolated_objects(v) for k, v in obj.get("__attrs__", {}).items()}
|
|
801
994
|
ns = SimpleNamespace(**attrs)
|
|
@@ -941,6 +1134,7 @@ def main():
|
|
|
941
1134
|
wlog(f"[worker] Creating instance...")
|
|
942
1135
|
instance = object.__new__(cls)
|
|
943
1136
|
if self_state:
|
|
1137
|
+
self_state = _deserialize_isolated_objects(self_state)
|
|
944
1138
|
instance.__dict__.update(self_state)
|
|
945
1139
|
wlog(f"[worker] Calling {method_name}...")
|
|
946
1140
|
method = getattr(instance, method_name)
|
|
@@ -957,7 +1151,7 @@ def main():
|
|
|
957
1151
|
wlog(f"[worker] Created {len(shm_registry)} shm blocks for result")
|
|
958
1152
|
|
|
959
1153
|
transport.send({"status": "ok", "result": result_meta})
|
|
960
|
-
#
|
|
1154
|
+
_shm_keeper.keep(shm_registry) # Keep alive for 30s until host reads
|
|
961
1155
|
|
|
962
1156
|
except Exception as e:
|
|
963
1157
|
# Cleanup shm on error since host won't read it
|
|
@@ -1134,7 +1328,9 @@ class SubprocessWorker(Worker):
|
|
|
1134
1328
|
lib_dir = self.python.parent.parent / "lib"
|
|
1135
1329
|
if lib_dir.is_dir():
|
|
1136
1330
|
existing = env.get("LD_LIBRARY_PATH", "")
|
|
1137
|
-
|
|
1331
|
+
# Also include system library paths so apt-installed libs (OpenGL, etc.) are found
|
|
1332
|
+
system_libs = "/usr/lib/x86_64-linux-gnu:/usr/lib:/lib/x86_64-linux-gnu"
|
|
1333
|
+
env["LD_LIBRARY_PATH"] = f"{lib_dir}:{system_libs}:{existing}" if existing else f"{lib_dir}:{system_libs}"
|
|
1138
1334
|
|
|
1139
1335
|
# On Windows, pass host Python directory so worker can add it via os.add_dll_directory()
|
|
1140
1336
|
# This fixes "DLL load failed" errors for packages like opencv-python-headless
|
|
@@ -72,22 +72,14 @@ def _get_worker(env_dir: Path, working_dir: Path, sys_path: list[str],
|
|
|
72
72
|
if cache_key in _workers and _workers[cache_key].is_alive():
|
|
73
73
|
return _workers[cache_key]
|
|
74
74
|
|
|
75
|
-
host_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
76
|
-
iso_ver = _get_python_version(env_dir)
|
|
77
75
|
python = env_dir / ("python.exe" if sys.platform == "win32" else "bin/python")
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# Same version - use MPWorker with venv Python for true isolation (like pyisolate)
|
|
86
|
-
# This fixes Windows where spawn would otherwise re-import main.py
|
|
87
|
-
from .workers.mp import MPWorker
|
|
88
|
-
print(f"[comfy-env] MPWorker: {python}")
|
|
89
|
-
worker = MPWorker(name=working_dir.name, sys_path=sys_path, lib_path=lib_path,
|
|
90
|
-
env_vars=env_vars, python=str(python))
|
|
77
|
+
# Always use SubprocessWorker - MPWorker's spawn mechanism tries to re-import
|
|
78
|
+
# the parent's __main__ (ComfyUI's main.py), which fails with import errors.
|
|
79
|
+
# SubprocessWorker uses a clean entry script that avoids this issue.
|
|
80
|
+
from .workers.subprocess import SubprocessWorker
|
|
81
|
+
print(f"[comfy-env] SubprocessWorker: {python}")
|
|
82
|
+
worker = SubprocessWorker(python=str(python), working_dir=working_dir, sys_path=sys_path, name=working_dir.name)
|
|
91
83
|
|
|
92
84
|
_workers[cache_key] = worker
|
|
93
85
|
return worker
|