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.
Files changed (55) hide show
  1. comfy_env/__init__.py +70 -122
  2. comfy_env/cli.py +78 -7
  3. comfy_env/config/__init__.py +19 -0
  4. comfy_env/config/parser.py +151 -0
  5. comfy_env/config/types.py +64 -0
  6. comfy_env/install.py +83 -361
  7. comfy_env/isolation/__init__.py +9 -0
  8. comfy_env/isolation/wrap.py +351 -0
  9. comfy_env/nodes.py +2 -2
  10. comfy_env/pixi/__init__.py +48 -0
  11. comfy_env/pixi/core.py +356 -0
  12. comfy_env/{resolver.py → pixi/resolver.py} +1 -14
  13. comfy_env/prestartup.py +60 -0
  14. comfy_env/templates/comfy-env-instructions.txt +30 -87
  15. comfy_env/templates/comfy-env.toml +68 -136
  16. comfy_env/workers/__init__.py +21 -32
  17. comfy_env/workers/base.py +1 -1
  18. comfy_env/workers/{torch_mp.py → mp.py} +47 -14
  19. comfy_env/workers/{venv.py → subprocess.py} +405 -441
  20. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
  21. comfy_env-0.0.66.dist-info/RECORD +34 -0
  22. comfy_env/decorator.py +0 -700
  23. comfy_env/env/__init__.py +0 -47
  24. comfy_env/env/config.py +0 -201
  25. comfy_env/env/config_file.py +0 -740
  26. comfy_env/env/manager.py +0 -636
  27. comfy_env/env/security.py +0 -267
  28. comfy_env/ipc/__init__.py +0 -55
  29. comfy_env/ipc/bridge.py +0 -476
  30. comfy_env/ipc/protocol.py +0 -265
  31. comfy_env/ipc/tensor.py +0 -371
  32. comfy_env/ipc/torch_bridge.py +0 -401
  33. comfy_env/ipc/transport.py +0 -318
  34. comfy_env/ipc/worker.py +0 -221
  35. comfy_env/isolation.py +0 -310
  36. comfy_env/pixi.py +0 -760
  37. comfy_env/stub_imports.py +0 -270
  38. comfy_env/stubs/__init__.py +0 -1
  39. comfy_env/stubs/comfy/__init__.py +0 -6
  40. comfy_env/stubs/comfy/model_management.py +0 -58
  41. comfy_env/stubs/comfy/utils.py +0 -29
  42. comfy_env/stubs/folder_paths.py +0 -71
  43. comfy_env/workers/pool.py +0 -241
  44. comfy_env-0.0.64.dist-info/RECORD +0 -48
  45. /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
  46. /comfy_env/{env → pixi}/platform/__init__.py +0 -0
  47. /comfy_env/{env → pixi}/platform/base.py +0 -0
  48. /comfy_env/{env → pixi}/platform/darwin.py +0 -0
  49. /comfy_env/{env → pixi}/platform/linux.py +0 -0
  50. /comfy_env/{env → pixi}/platform/windows.py +0 -0
  51. /comfy_env/{registry.py → pixi/registry.py} +0 -0
  52. /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
  53. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
  54. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
  55. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/licenses/LICENSE +0 -0
@@ -1,401 +0,0 @@
1
- """
2
- TorchBridge - Zero-copy CUDA tensor sharing via torch.multiprocessing.
3
-
4
- This bridge enables passing PyTorch tensors between processes WITHOUT
5
- copying data. CUDA tensors stay in GPU memory and are shared directly.
6
-
7
- Requirements:
8
- - Both host and worker must have the same PyTorch version
9
- - Use TorchWorker as the base class for your worker
10
-
11
- This is ideal for ML nodes where you pass large tensors between processes.
12
- """
13
-
14
- import threading
15
- import traceback
16
- import uuid
17
- from pathlib import Path
18
- from typing import Any, Callable, Dict, Optional
19
-
20
- try:
21
- import torch.multiprocessing as mp
22
- TORCH_AVAILABLE = True
23
- except ImportError:
24
- TORCH_AVAILABLE = False
25
-
26
-
27
- def _worker_process(
28
- worker_cls: type,
29
- to_worker: "mp.Queue",
30
- from_worker: "mp.Queue",
31
- worker_script: Optional[Path] = None,
32
- setup_args: Optional[Dict] = None,
33
- ):
34
- """
35
- Entry point for the worker process.
36
-
37
- This runs in a separate process spawned by torch.multiprocessing.
38
- """
39
- try:
40
- # If worker_script is provided, import the worker class from it
41
- if worker_script is not None:
42
- import importlib.util
43
- import sys
44
-
45
- spec = importlib.util.spec_from_file_location("worker_module", worker_script)
46
- module = importlib.util.module_from_spec(spec)
47
- sys.modules["worker_module"] = module
48
- spec.loader.exec_module(module)
49
-
50
- # Find the TorchWorker subclass in the module
51
- worker_cls = None
52
- for name in dir(module):
53
- obj = getattr(module, name)
54
- if isinstance(obj, type) and issubclass(obj, TorchWorker) and obj is not TorchWorker:
55
- worker_cls = obj
56
- break
57
-
58
- if worker_cls is None:
59
- from_worker.put({
60
- "id": "startup",
61
- "error": f"No TorchWorker subclass found in {worker_script}",
62
- "result": None,
63
- })
64
- return
65
-
66
- # Create worker instance
67
- worker = worker_cls()
68
- worker._to_host = from_worker
69
- worker._from_host = to_worker
70
-
71
- # Call setup
72
- if hasattr(worker, 'setup'):
73
- worker.setup(**(setup_args or {}))
74
-
75
- # Signal ready
76
- from_worker.put({"id": "startup", "result": "ready", "error": None})
77
-
78
- # Main loop
79
- while True:
80
- try:
81
- request = to_worker.get()
82
-
83
- if request is None:
84
- break
85
-
86
- req_id = request.get("id", "unknown")
87
- method = request.get("method")
88
- args = request.get("args", {})
89
-
90
- if method == "shutdown":
91
- from_worker.put({"id": req_id, "result": "ok", "error": None})
92
- break
93
-
94
- if method == "ping":
95
- from_worker.put({"id": req_id, "result": "pong", "error": None})
96
- continue
97
-
98
- # Get registered method
99
- handler = worker._methods.get(method)
100
- if handler is None:
101
- from_worker.put({
102
- "id": req_id,
103
- "error": f"Unknown method: {method}",
104
- "result": None,
105
- })
106
- continue
107
-
108
- # Call the method
109
- try:
110
- result = handler(**args)
111
- from_worker.put({"id": req_id, "result": result, "error": None})
112
- except Exception as e:
113
- from_worker.put({
114
- "id": req_id,
115
- "error": str(e),
116
- "traceback": traceback.format_exc(),
117
- "result": None,
118
- })
119
-
120
- except Exception as e:
121
- # Log but continue
122
- print(f"[TorchWorker] Error in main loop: {e}")
123
- traceback.print_exc()
124
-
125
- except Exception as e:
126
- # Startup error
127
- try:
128
- from_worker.put({
129
- "id": "startup",
130
- "error": f"Worker startup failed: {e}\n{traceback.format_exc()}",
131
- "result": None,
132
- })
133
- except Exception:
134
- pass
135
-
136
-
137
- class TorchWorker:
138
- """
139
- Base class for workers that use torch.multiprocessing for zero-copy tensors.
140
-
141
- Unlike BaseWorker which uses stdin/stdout JSON, TorchWorker uses
142
- torch.multiprocessing.Queue which enables zero-copy CUDA tensor sharing.
143
-
144
- Example:
145
- from comfy_env import TorchWorker, register
146
-
147
- class MyWorker(TorchWorker):
148
- def setup(self):
149
- import torch
150
- self.model = load_model()
151
-
152
- @register("process")
153
- def process(self, tensor):
154
- # tensor is a CUDA tensor - no copy happened!
155
- return self.model(tensor)
156
-
157
- if __name__ == "__main__":
158
- # Note: With TorchBridge, you don't need this
159
- # The bridge spawns the worker automatically
160
- pass
161
- """
162
-
163
- def __init__(self):
164
- self._methods: Dict[str, Callable] = {}
165
- self._to_host = None
166
- self._from_host = None
167
-
168
- # Auto-register methods decorated with @register
169
- for name in dir(self):
170
- method = getattr(self, name)
171
- if callable(method) and hasattr(method, '_register_name'):
172
- self._methods[method._register_name] = method
173
-
174
- def setup(self, **kwargs):
175
- """Override this to initialize your worker (load models, etc)."""
176
- pass
177
-
178
- def log(self, message: str):
179
- """Log a message (goes to stderr in the worker process)."""
180
- print(f"[{self.__class__.__name__}] {message}")
181
-
182
-
183
- class TorchBridge:
184
- """
185
- Bridge for zero-copy CUDA tensor sharing with workers.
186
-
187
- Unlike WorkerBridge which uses subprocess+JSON, TorchBridge uses
188
- torch.multiprocessing which enables zero-copy sharing of CUDA tensors.
189
-
190
- Requirements:
191
- - PyTorch must be installed in both host and worker
192
- - Worker must be a TorchWorker subclass
193
-
194
- Example:
195
- from comfy_env import TorchBridge, TorchWorker, register
196
- from pathlib import Path
197
-
198
- # In your node:
199
- bridge = TorchBridge(worker_script=Path("my_worker.py"))
200
-
201
- # CUDA tensors are passed without copying!
202
- output = bridge.process(tensor=my_cuda_tensor)
203
-
204
- # Or using call():
205
- output = bridge.call("process", tensor=my_cuda_tensor)
206
- """
207
-
208
- _instances: Dict[str, "TorchBridge"] = {}
209
- _instances_lock = threading.Lock()
210
-
211
- def __init__(
212
- self,
213
- worker_script: Optional[Path] = None,
214
- worker_cls: Optional[type] = None,
215
- log_callback: Optional[Callable[[str], None]] = None,
216
- auto_start: bool = True,
217
- ):
218
- """
219
- Initialize the TorchBridge.
220
-
221
- Args:
222
- worker_script: Path to a .py file containing a TorchWorker subclass
223
- worker_cls: Alternatively, pass the worker class directly
224
- log_callback: Optional callback for logging
225
- auto_start: Whether to auto-start on first call
226
- """
227
- if not TORCH_AVAILABLE:
228
- raise ImportError(
229
- "PyTorch is required for TorchBridge. "
230
- "Install it with: pip install torch"
231
- )
232
-
233
- if worker_script is None and worker_cls is None:
234
- raise ValueError("Either worker_script or worker_cls must be provided")
235
-
236
- self.worker_script = Path(worker_script) if worker_script else None
237
- self.worker_cls = worker_cls
238
- self.log = log_callback or print
239
- self.auto_start = auto_start
240
-
241
- self._to_worker: Optional["mp.Queue"] = None
242
- self._from_worker: Optional["mp.Queue"] = None
243
- self._process: Optional["mp.Process"] = None
244
- self._process_lock = threading.Lock()
245
-
246
- @property
247
- def is_running(self) -> bool:
248
- """Check if worker process is running."""
249
- with self._process_lock:
250
- return self._process is not None and self._process.is_alive()
251
-
252
- def start(self, **setup_args) -> None:
253
- """
254
- Start the worker process.
255
-
256
- Args:
257
- **setup_args: Arguments to pass to worker's setup() method
258
- """
259
- with self._process_lock:
260
- if self._process is not None and self._process.is_alive():
261
- return
262
-
263
- # Set spawn method if not already set
264
- try:
265
- mp.set_start_method("spawn", force=True)
266
- except RuntimeError:
267
- pass # Already set
268
-
269
- self._to_worker = mp.Queue()
270
- self._from_worker = mp.Queue()
271
-
272
- self.log("Starting TorchBridge worker process...")
273
-
274
- self._process = mp.Process(
275
- target=_worker_process,
276
- args=(
277
- self.worker_cls,
278
- self._to_worker,
279
- self._from_worker,
280
- self.worker_script,
281
- setup_args,
282
- ),
283
- )
284
- self._process.start()
285
-
286
- # Wait for ready signal
287
- try:
288
- response = self._from_worker.get(timeout=60.0)
289
- if response.get("error"):
290
- raise RuntimeError(f"Worker startup failed: {response['error']}")
291
- self.log("Worker started successfully")
292
- except Exception as e:
293
- self.stop()
294
- raise RuntimeError(f"Worker failed to start: {e}")
295
-
296
- def stop(self) -> None:
297
- """Stop the worker process."""
298
- with self._process_lock:
299
- if self._process is None:
300
- return
301
-
302
- self.log("Stopping worker...")
303
-
304
- # Send shutdown
305
- try:
306
- if self._to_worker:
307
- self._to_worker.put({"method": "shutdown", "id": "shutdown"})
308
- except Exception:
309
- pass
310
-
311
- # Wait for graceful shutdown
312
- if self._process.is_alive():
313
- self._process.join(timeout=5.0)
314
-
315
- # Force kill if needed
316
- if self._process.is_alive():
317
- self.log("Worker didn't stop gracefully, terminating...")
318
- self._process.terminate()
319
- self._process.join(timeout=2.0)
320
-
321
- if self._process.is_alive():
322
- self._process.kill()
323
-
324
- # Cleanup queues
325
- try:
326
- if self._to_worker:
327
- self._to_worker.close()
328
- if self._from_worker:
329
- self._from_worker.close()
330
- except Exception:
331
- pass
332
-
333
- self._process = None
334
- self._to_worker = None
335
- self._from_worker = None
336
- self.log("Worker stopped")
337
-
338
- def call(self, method: str, timeout: float = 300.0, **kwargs) -> Any:
339
- """
340
- Call a method on the worker.
341
-
342
- Args:
343
- method: Method name to call
344
- timeout: Timeout in seconds
345
- **kwargs: Arguments (can include CUDA tensors!)
346
-
347
- Returns:
348
- The method's return value
349
- """
350
- if self.auto_start and not self.is_running:
351
- self.start()
352
-
353
- if not self.is_running:
354
- raise RuntimeError("Worker is not running")
355
-
356
- req_id = str(uuid.uuid4())[:8]
357
-
358
- self._to_worker.put({
359
- "id": req_id,
360
- "method": method,
361
- "args": kwargs,
362
- })
363
-
364
- # Wait for response
365
- try:
366
- response = self._from_worker.get(timeout=timeout)
367
- except Exception:
368
- raise TimeoutError(f"Timeout waiting for response after {timeout}s")
369
-
370
- if response.get("error"):
371
- tb = response.get("traceback", "")
372
- raise RuntimeError(f"Worker error: {response['error']}\n{tb}")
373
-
374
- return response.get("result")
375
-
376
- def __getattr__(self, name: str) -> Callable[..., Any]:
377
- """Enable direct method calls: bridge.process(tensor=t)"""
378
- if name.startswith("_"):
379
- raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
380
-
381
- def method_wrapper(*args, timeout: float = 300.0, **kwargs) -> Any:
382
- if args:
383
- raise TypeError(
384
- f"bridge.{name}() doesn't accept positional arguments. "
385
- f"Use keyword arguments: bridge.{name}(arg1=val1, arg2=val2)"
386
- )
387
- return self.call(name, timeout=timeout, **kwargs)
388
-
389
- return method_wrapper
390
-
391
- def __enter__(self) -> "TorchBridge":
392
- return self
393
-
394
- def __exit__(self, exc_type, exc_val, exc_tb) -> None:
395
- self.stop()
396
-
397
- def __del__(self) -> None:
398
- try:
399
- self.stop()
400
- except Exception:
401
- pass