comfy-env 0.1.9__tar.gz → 0.1.10__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.
Files changed (34) hide show
  1. {comfy_env-0.1.9 → comfy_env-0.1.10}/PKG-INFO +1 -1
  2. {comfy_env-0.1.9 → comfy_env-0.1.10}/pyproject.toml +1 -1
  3. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/isolation/wrap.py +29 -3
  4. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/core.py +32 -1
  5. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/prestartup.py +13 -2
  6. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/workers/mp.py +58 -0
  7. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/workers/subprocess.py +16 -4
  8. {comfy_env-0.1.9 → comfy_env-0.1.10}/.github/workflows/ci.yml +0 -0
  9. {comfy_env-0.1.9 → comfy_env-0.1.10}/.github/workflows/publish.yml +0 -0
  10. {comfy_env-0.1.9 → comfy_env-0.1.10}/.gitignore +0 -0
  11. {comfy_env-0.1.9 → comfy_env-0.1.10}/LICENSE +0 -0
  12. {comfy_env-0.1.9 → comfy_env-0.1.10}/README.md +0 -0
  13. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/__init__.py +0 -0
  14. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/cli.py +0 -0
  15. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/config/__init__.py +0 -0
  16. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/config/parser.py +0 -0
  17. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/config/types.py +0 -0
  18. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/errors.py +0 -0
  19. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/install.py +0 -0
  20. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/isolation/__init__.py +0 -0
  21. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/nodes.py +0 -0
  22. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/__init__.py +0 -0
  23. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/cuda_detection.py +0 -0
  24. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/platform/__init__.py +0 -0
  25. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/platform/base.py +0 -0
  26. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/platform/darwin.py +0 -0
  27. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/platform/linux.py +0 -0
  28. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/platform/windows.py +0 -0
  29. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/pixi/resolver.py +0 -0
  30. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
  31. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/templates/comfy-env.toml +0 -0
  32. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/workers/__init__.py +0 -0
  33. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/workers/base.py +0 -0
  34. {comfy_env-0.1.9 → comfy_env-0.1.10}/src/comfy_env/workers/tensor_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-env
3
- Version: 0.1.9
3
+ Version: 0.1.10
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "comfy-env"
3
- version = "0.1.9"
3
+ version = "0.1.10"
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"}
@@ -34,6 +34,12 @@ from typing import Any, Dict, Optional
34
34
  # Debug logging (set COMFY_ENV_DEBUG=1 to enable)
35
35
  _DEBUG = os.environ.get("COMFY_ENV_DEBUG", "").lower() in ("1", "true", "yes")
36
36
 
37
+
38
+ def get_env_name(dir_name: str) -> str:
39
+ """Convert directory name to env name: ComfyUI-UniRig → _env_unirig"""
40
+ name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
41
+ return f"_env_{name}"
42
+
37
43
  # Global worker cache (one per isolated environment)
38
44
  _workers: Dict[str, Any] = {}
39
45
  _workers_lock = threading.Lock()
@@ -144,13 +150,27 @@ def _find_env_paths(node_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
144
150
  """
145
151
  import glob
146
152
 
147
- # Check pixi environment first
153
+ # Check _env_<name> directory first (new pattern)
154
+ env_name = get_env_name(node_dir.name)
155
+ env_dir = node_dir / env_name
156
+ if env_dir.exists():
157
+ if sys.platform == "win32":
158
+ site_packages = env_dir / "Lib" / "site-packages"
159
+ lib_dir = env_dir / "Library" / "bin"
160
+ else:
161
+ pattern = str(env_dir / "lib" / "python*" / "site-packages")
162
+ matches = glob.glob(pattern)
163
+ site_packages = Path(matches[0]) if matches else None
164
+ lib_dir = env_dir / "lib"
165
+ if site_packages and site_packages.exists():
166
+ return site_packages, lib_dir if lib_dir.exists() else None
167
+
168
+ # Fallback: Check old .pixi/envs/default (for backward compat)
148
169
  pixi_env = node_dir / ".pixi" / "envs" / "default"
149
170
  if pixi_env.exists():
150
- # Find site-packages (pythonX.Y varies)
151
171
  if sys.platform == "win32":
152
172
  site_packages = pixi_env / "Lib" / "site-packages"
153
- lib_dir = pixi_env / "Library" / "bin" # Windows DLLs
173
+ lib_dir = pixi_env / "Library" / "bin"
154
174
  else:
155
175
  pattern = str(pixi_env / "lib" / "python*" / "site-packages")
156
176
  matches = glob.glob(pattern)
@@ -176,6 +196,12 @@ def _find_env_paths(node_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
176
196
 
177
197
  def _find_env_dir(node_dir: Path) -> Optional[Path]:
178
198
  """Find the environment directory (for cache key)."""
199
+ # Check _env_<name> first
200
+ env_name = get_env_name(node_dir.name)
201
+ env_dir = node_dir / env_name
202
+ if env_dir.exists():
203
+ return env_dir
204
+ # Fallback to old paths
179
205
  pixi_env = node_dir / ".pixi" / "envs" / "default"
180
206
  if pixi_env.exists():
181
207
  return pixi_env
@@ -281,6 +281,12 @@ def ensure_pixi(
281
281
  return pixi_path
282
282
 
283
283
 
284
+ def get_env_name(dir_name: str) -> str:
285
+ """Convert directory name to env name: ComfyUI-UniRig → _env_unirig"""
286
+ name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
287
+ return f"_env_{name}"
288
+
289
+
284
290
  def clean_pixi_artifacts(node_dir: Path, log: Callable[[str], None] = print) -> None:
285
291
  """Remove previous pixi installation artifacts."""
286
292
  for path in [node_dir / "pixi.toml", node_dir / "pixi.lock"]:
@@ -289,11 +295,21 @@ def clean_pixi_artifacts(node_dir: Path, log: Callable[[str], None] = print) ->
289
295
  pixi_dir = node_dir / ".pixi"
290
296
  if pixi_dir.exists():
291
297
  shutil.rmtree(pixi_dir)
298
+ # Also clean old _env_* directories
299
+ env_name = get_env_name(node_dir.name)
300
+ env_dir = node_dir / env_name
301
+ if env_dir.exists():
302
+ shutil.rmtree(env_dir)
292
303
 
293
304
 
294
305
  def get_pixi_python(node_dir: Path) -> Optional[Path]:
295
306
  """Get path to Python in the pixi environment."""
296
- env_dir = node_dir / ".pixi" / "envs" / "default"
307
+ # Check new _env_<name> location first
308
+ env_name = get_env_name(node_dir.name)
309
+ env_dir = node_dir / env_name
310
+ if not env_dir.exists():
311
+ # Fallback to old .pixi path
312
+ env_dir = node_dir / ".pixi" / "envs" / "default"
297
313
  if sys.platform == "win32":
298
314
  python_path = env_dir / "python.exe"
299
315
  else:
@@ -507,5 +523,20 @@ def pixi_install(
507
523
 
508
524
  log("CUDA packages installed")
509
525
 
526
+ # Move environment from .pixi/envs/default to _env_<name>
527
+ old_env = node_dir / ".pixi" / "envs" / "default"
528
+ env_name = get_env_name(node_dir.name)
529
+ new_env = node_dir / env_name
530
+
531
+ if old_env.exists():
532
+ if new_env.exists():
533
+ shutil.rmtree(new_env) # Clean old env
534
+ shutil.move(str(old_env), str(new_env))
535
+ # Clean up .pixi directory (keep pixi.toml and pixi.lock)
536
+ pixi_dir = node_dir / ".pixi"
537
+ if pixi_dir.exists():
538
+ shutil.rmtree(pixi_dir)
539
+ log(f"Moved environment to {new_env}")
540
+
510
541
  log("Installation complete!")
511
542
  return True
@@ -11,6 +11,12 @@ from pathlib import Path
11
11
  from typing import Optional, Dict
12
12
 
13
13
 
14
+ def get_env_name(dir_name: str) -> str:
15
+ """Convert directory name to env name: ComfyUI-UniRig → _env_unirig"""
16
+ name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
17
+ return f"_env_{name}"
18
+
19
+
14
20
  def _load_env_vars(config_path: str) -> Dict[str, str]:
15
21
  """
16
22
  Load [env_vars] section from comfy-env.toml.
@@ -121,10 +127,15 @@ def setup_env(node_dir: Optional[str] = None) -> None:
121
127
  for key, value in env_vars.items():
122
128
  os.environ[key] = value
123
129
 
124
- pixi_env = os.path.join(node_dir, ".pixi", "envs", "default")
130
+ # Check _env_<name> first, then fallback to old .pixi path
131
+ env_name = get_env_name(os.path.basename(node_dir))
132
+ pixi_env = os.path.join(node_dir, env_name)
125
133
 
126
134
  if not os.path.exists(pixi_env):
127
- return # No pixi environment
135
+ # Fallback to old .pixi path
136
+ pixi_env = os.path.join(node_dir, ".pixi", "envs", "default")
137
+ if not os.path.exists(pixi_env):
138
+ return # No environment found
128
139
 
129
140
  if sys.platform == "win32":
130
141
  # Windows: add to PATH for DLL loading
@@ -40,6 +40,53 @@ _SHUTDOWN = object()
40
40
  _CALL_METHOD = "call_method"
41
41
 
42
42
 
43
+ # ---------------------------------------------------------------------------
44
+ # Tensor file transfer - avoids CUDA IPC issues with cudaMallocAsync
45
+ # ---------------------------------------------------------------------------
46
+
47
+ def _save_tensors_to_files(obj, file_registry=None):
48
+ """Recursively save torch tensors to temp files for IPC."""
49
+ if file_registry is None:
50
+ file_registry = []
51
+
52
+ try:
53
+ import torch
54
+ if isinstance(obj, torch.Tensor):
55
+ import tempfile
56
+ f = tempfile.NamedTemporaryFile(suffix='.pt', delete=False)
57
+ torch.save(obj.cpu(), f.name) # Always save as CPU tensor
58
+ f.close()
59
+ file_registry.append(f.name)
60
+ return {"__tensor_file__": f.name, "dtype": str(obj.dtype), "device": str(obj.device)}
61
+ except ImportError:
62
+ pass
63
+
64
+ if isinstance(obj, dict):
65
+ return {k: _save_tensors_to_files(v, file_registry) for k, v in obj.items()}
66
+ elif isinstance(obj, list):
67
+ return [_save_tensors_to_files(v, file_registry) for v in obj]
68
+ elif isinstance(obj, tuple):
69
+ return tuple(_save_tensors_to_files(v, file_registry) for v in obj)
70
+ return obj
71
+
72
+
73
+ def _load_tensors_from_files(obj):
74
+ """Recursively load torch tensors from temp files."""
75
+ if isinstance(obj, dict):
76
+ if "__tensor_file__" in obj:
77
+ import os
78
+ import torch
79
+ tensor = torch.load(obj["__tensor_file__"], weights_only=True)
80
+ os.unlink(obj["__tensor_file__"]) # Cleanup temp file
81
+ return tensor
82
+ return {k: _load_tensors_from_files(v) for k, v in obj.items()}
83
+ elif isinstance(obj, list):
84
+ return [_load_tensors_from_files(v) for v in obj]
85
+ elif isinstance(obj, tuple):
86
+ return tuple(_load_tensors_from_files(v) for v in obj)
87
+ return obj
88
+
89
+
43
90
  def _dump_worker_env(worker_name: str = "unknown", print_to_terminal: bool = False):
44
91
  """Dump worker environment to .comfy-env/logs/ (always) and optionally print."""
45
92
  import json
@@ -205,14 +252,20 @@ def _worker_loop(queue_in, queue_out, sys_path_additions=None, lib_path=None, en
205
252
  # Handle method call protocol
206
253
  if isinstance(item, tuple) and len(item) == 6 and item[0] == _CALL_METHOD:
207
254
  _, module_name, class_name, method_name, self_state, kwargs = item
255
+ # Load tensors from files (saved by host to avoid cudaMallocAsync IPC issues)
256
+ kwargs = _load_tensors_from_files(kwargs)
208
257
  result = _execute_method_call(
209
258
  module_name, class_name, method_name, self_state, kwargs
210
259
  )
260
+ # Save tensors to files to avoid CUDA IPC issues with cudaMallocAsync
261
+ result = _save_tensors_to_files(result)
211
262
  queue_out.put(("ok", result))
212
263
  else:
213
264
  # Direct function call (legacy)
214
265
  func, args, kwargs = item
215
266
  result = func(*args, **kwargs)
267
+ # Save tensors to files to avoid CUDA IPC issues with cudaMallocAsync
268
+ result = _save_tensors_to_files(result)
216
269
  queue_out.put(("ok", result))
217
270
 
218
271
  except Exception as e:
@@ -646,6 +699,9 @@ class MPWorker(Worker):
646
699
  """
647
700
  self._ensure_started()
648
701
 
702
+ # Save tensors to files to avoid CUDA IPC issues with cudaMallocAsync
703
+ kwargs = _save_tensors_to_files(kwargs)
704
+
649
705
  # Send method call request using protocol
650
706
  self._queue_in.put((
651
707
  _CALL_METHOD,
@@ -672,6 +728,8 @@ class MPWorker(Worker):
672
728
 
673
729
  # Handle response
674
730
  if status == "ok":
731
+ # Load tensors from temp files
732
+ result = _load_tensors_from_files(result)
675
733
  return result
676
734
  elif status == "error":
677
735
  msg, tb = result
@@ -221,10 +221,12 @@ def _to_shm(obj, registry, visited=None):
221
221
  visited[obj_id] = result
222
222
  return result
223
223
 
224
- # torch.Tensor → convert to numpy → shared memory
224
+ # torch.Tensor → convert to numpy → shared memory (with marker to restore type)
225
225
  if t == 'Tensor':
226
226
  arr = obj.detach().cpu().numpy()
227
- return _to_shm(arr, registry, visited)
227
+ result = _to_shm(arr, registry, visited)
228
+ result["__was_tensor__"] = True
229
+ return result
228
230
 
229
231
  # trimesh.Trimesh → vertices + faces arrays → shared memory
230
232
  if t == 'Trimesh':
@@ -279,13 +281,17 @@ def _from_shm(obj, unlink=True):
279
281
  return [_from_shm(v, unlink) for v in obj]
280
282
  return obj
281
283
 
282
- # numpy array
284
+ # numpy array (or tensor that was converted to numpy)
283
285
  if "__shm_np__" in obj:
284
286
  block = shm.SharedMemory(name=obj["__shm_np__"])
285
287
  arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
286
288
  block.close()
287
289
  if unlink:
288
290
  block.unlink()
291
+ # Convert back to tensor if it was originally a tensor
292
+ if obj.get("__was_tensor__"):
293
+ import torch
294
+ return torch.from_numpy(arr)
289
295
  return arr
290
296
 
291
297
  # trimesh
@@ -544,7 +550,9 @@ def _to_shm(obj, registry, visited=None):
544
550
 
545
551
  if t == 'Tensor':
546
552
  arr = obj.detach().cpu().numpy()
547
- return _to_shm(arr, registry, visited)
553
+ result = _to_shm(arr, registry, visited)
554
+ result["__was_tensor__"] = True
555
+ return result
548
556
 
549
557
  if t == 'Trimesh':
550
558
  verts = np.ascontiguousarray(obj.vertices, dtype=np.float64)
@@ -587,6 +595,10 @@ def _from_shm(obj):
587
595
  block = shm.SharedMemory(name=obj["__shm_np__"])
588
596
  arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
589
597
  block.close()
598
+ # Convert back to tensor if it was originally a tensor
599
+ if obj.get("__was_tensor__"):
600
+ import torch
601
+ return torch.from_numpy(arr)
590
602
  return arr
591
603
  if "__shm_trimesh__" in obj:
592
604
  import trimesh
File without changes
File without changes
File without changes