comfy-env 0.0.64__py3-none-any.whl → 0.0.66__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comfy_env/__init__.py +70 -122
- comfy_env/cli.py +78 -7
- comfy_env/config/__init__.py +19 -0
- comfy_env/config/parser.py +151 -0
- comfy_env/config/types.py +64 -0
- comfy_env/install.py +83 -361
- comfy_env/isolation/__init__.py +9 -0
- comfy_env/isolation/wrap.py +351 -0
- comfy_env/nodes.py +2 -2
- comfy_env/pixi/__init__.py +48 -0
- comfy_env/pixi/core.py +356 -0
- comfy_env/{resolver.py → pixi/resolver.py} +1 -14
- comfy_env/prestartup.py +60 -0
- comfy_env/templates/comfy-env-instructions.txt +30 -87
- comfy_env/templates/comfy-env.toml +68 -136
- comfy_env/workers/__init__.py +21 -32
- comfy_env/workers/base.py +1 -1
- comfy_env/workers/{torch_mp.py → mp.py} +47 -14
- comfy_env/workers/{venv.py → subprocess.py} +405 -441
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
- comfy_env-0.0.66.dist-info/RECORD +34 -0
- comfy_env/decorator.py +0 -700
- comfy_env/env/__init__.py +0 -47
- comfy_env/env/config.py +0 -201
- comfy_env/env/config_file.py +0 -740
- comfy_env/env/manager.py +0 -636
- comfy_env/env/security.py +0 -267
- comfy_env/ipc/__init__.py +0 -55
- comfy_env/ipc/bridge.py +0 -476
- comfy_env/ipc/protocol.py +0 -265
- comfy_env/ipc/tensor.py +0 -371
- comfy_env/ipc/torch_bridge.py +0 -401
- comfy_env/ipc/transport.py +0 -318
- comfy_env/ipc/worker.py +0 -221
- comfy_env/isolation.py +0 -310
- comfy_env/pixi.py +0 -760
- comfy_env/stub_imports.py +0 -270
- comfy_env/stubs/__init__.py +0 -1
- comfy_env/stubs/comfy/__init__.py +0 -6
- comfy_env/stubs/comfy/model_management.py +0 -58
- comfy_env/stubs/comfy/utils.py +0 -29
- comfy_env/stubs/folder_paths.py +0 -71
- comfy_env/workers/pool.py +0 -241
- comfy_env-0.0.64.dist-info/RECORD +0 -48
- /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
- /comfy_env/{env → pixi}/platform/__init__.py +0 -0
- /comfy_env/{env → pixi}/platform/base.py +0 -0
- /comfy_env/{env → pixi}/platform/darwin.py +0 -0
- /comfy_env/{env → pixi}/platform/linux.py +0 -0
- /comfy_env/{env → pixi}/platform/windows.py +0 -0
- /comfy_env/{registry.py → pixi/registry.py} +0 -0
- /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/licenses/LICENSE +0 -0
comfy_env/ipc/torch_bridge.py
DELETED
|
@@ -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
|