comfy-env¶
comfy-env provides environment management and automatic CUDA wheel resolution for ComfyUI custom nodepacks.
The promise
You click the install button for a nodepack in ComfyUI Manager, and after install the pack just runs, without breaking any other pre existing nodepack.
No missing build tools. No CUDA toolkit needed. No hunting for the one torch version that satisfies everything. No PhD in dependency management. 100% certainty that installing a nodepack from ComfyUI Manager won't destroy your existing setup.
That is the whole point: nodepacks should behave like real software (the aim).
Several things stand between the current status of ComfyUI and that promise.
comfy-env addresses two of them:
- Environment isolation: Vanilla ComfyUI loads every pack into one shared environment, so two packs that need incompatible versions of the same library cannot coexist. Installing a custom nodepack can potentially damage the existing installation.
- CUDA / prebuilt wheels / conda packages: dependencies pip alone cannot deliver (conda-only native libraries), dependencies (like compiled CUDA extensions) that can take a long time and manual work to find or compile (compiled CUDA extensions) for the user's exact machine and operating system.
ComfyUI background¶
comfy-env has to honour the contract vanilla ComfyUI already defines: how a
pack is discovered, what __init__.py must export, which hooks run when etc.
The rest of this page assumes that the user is already familiar with this crucial context.
If you're not, please read this page first.
The two problems: environment isolation and CUDA/conda packages¶
Problem 1: Environment isolation¶
One shared environment for every pack breaks in predictable ways:
- Conflicting Python deps:
- Node A pins
numpy<2 - Node B needs
numpy>=2. - pip installs into one shared env, so whichever lands last wins and the other crashes on import.
- Node A pins
- Conflicting native libraries: a duplicate
OpenMP runtime is a classic example.
- torch bundles one (
libiomp5) - another pip installed pack
bundles another (
libomp/libgomp) - loading both into one process
aborts with
OMP: Error #15or silently corrupts numerics
- torch bundles one (
- Wrong interpreter entirely:
- ComfyUI is running Python 3.12
- Nodepack C needs Python 3.11 (it might need a Blender
bpywheel) - Best case scenario: Nodepack C doesn't install at all. Worst case scenario: Nodepack C installs and then crashes ComfyUI when loading
comfy-env's answer is process isolation: any nodepack subdirectory that declares a
comfy-env.toml gets its own pixi-managed environment: separate
interpreter, conda packages, pip packages.
Its nodes then execute in a persistent subprocess worker using that interpreter.
If the isolated nodepack wants to register API routes
comfy-env re-registers forwarding proxies in the parent
(_register_proxy_routes): the endpoint answers on ComfyUI's own server and
the call crosses to the persistent subprocess worker just like node execution.
As a principle, comfy-env never installs anything into the host environment: the host
env's only comfy-env-related content is comfy-env itself
(ADR-0003).
Problem 2: CUDA, prebuilt wheels, and conda packages¶
There are two kinds of dependency pip install alone cannot deliver:
| Kind | Why pip fails | |
|---|---|---|
| 2A | CUDA packages | they are on PyPI, but only for a fraction of the builds users have and may require long compilations |
| 2B | Conda packages | they are not on PyPI at all |
2A — CUDA packages¶
flash-attn, nvdiffrast, pytorch3d, gsplat, nunchaku.
Each wheel must be compiled for one exact combination of five axes:
| Axis | Values |
|---|---|
| Python ABI | 3.10 / 3.11 / 3.12 / 3.13 |
| torch | 2.4 … 2.11 |
| CUDA | 12.x / 13.0 |
| OS | Windows / Linux |
| GPU arch | sm_50+ on cu124/cu126 rows; sm_70/sm_75+ on cu128 and newer; a few packages floor higher (flash-attn, natten) |
Upstream publishes a fraction of that matrix, and building the rest needs a CUDA toolkit, a C++ compiler and time, all three of which are often in short supply.
The answer: a prebuilt wheel index,
cuda-wheels. Packages listed
under [cuda] in comfy-env.toml are resolved at install time against the machine's detected
(GPU, torch, Python, OS) and installed ready-made.
(ADR-0004).
Accelerator-agnostic in principle. Backend detection already recognises
ROCm (torch's +rocm tag), and a separate rocm-wheels index mirroring
cuda-wheels is planned. At the moment this is blocked only on the maintainer not owning ROCm
hardware. Today only CUDA is compiled end to end.
"Aren't we just reinventing conda?", you are absolutely right.
Here's why this logic currently lives in comfy-env at all.
2B — Conda packages¶
Some dependencies absolutely require us to use conda, and we can broadly subdivide them into three categories:
| # | Reason | Examples |
|---|---|---|
| 1 | Not Python. | headless GL/X stack (mesalib, libglu, libglvnd, xorg-libsm), libstdcxx-ng, pythonocc-core (no PyPI distribution at any version) |
| 2 | Copyleft. A wheel vendors the native library into the artifact, fusing a GPL derivative work and forcing copyleft (or a commercial licence) onto the wheel and everyone who installs it. Conda's separate-package model keeps the boundary at install-time aggregation, with conda-forge carrying source-availability compliance. | cgal, ipopt, gurobi... |
| 3 | Root-free toolchains. Install-time compilation on an end-user machine needs compilers and CUDA dev packages, per-user, solver-managed, no admin rights. conda-forge is the only channel that delivers these. | c-compiler, cxx-compiler, cuda-nvcc, cuda-cccl, cuda-cudart-dev, occt-rt |
(ADR-0002 has the full argument):
This is why comfy-env generates pixi manifests. Pixi is the uv equivalent for conda, speaking conda-forge and PyPI in one file with one lockfile (ADR-0003).
The three-call contract¶
A consuming nodepack integrates with exactly three lines:
# install.py
from comfy_env import install; install()
# prestartup_script.py
from comfy_env import setup_env; setup_env()
# __init__.py
from comfy_env import register_nodes
NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = register_nodes()
Each maps to a lifecycle phase and has its own documentation page:
| Call | Phase | Page |
|---|---|---|
install() |
build time (Manager install / python install.py) |
install() |
setup_env() |
every launch, before the server boots | setup_env() |
register_nodes() |
every launch, node registration | register_nodes() |
Once register_nodes() has built the proxies, every node execution crosses a
process boundary: how a call and its tensors travel, and what each side may
import, is in The process boundary.
System context¶
flowchart TD
subgraph host["ComfyUI main process (host Python env)"]
comfyui["ComfyUI core"]
pack["Nodepack<br/>install.py / prestartup_script.py / __init__.py"]
ce["comfy-env library"]
comfyui --> pack --> ce
end
subgraph ws["Machine-wide workspace"]
env1["%LOCALAPPDATA%/Programs/comfy-env (Windows)<br/>~/.ce (macOS, Linux)<br/>envs/<name>_<abi-tag>/pixi.toml<br/>envs/<name>_<abi-tag>/.pixi/envs/default/"]
end
worker["Isolated worker subprocess<br/>(interpreter from the pixi env)"]
pixi["pixi binary (+ uv underneath)<br/>pinned + sha256-verified,<br/>~/.comfy-env/pixi/<version>/"]
idx["cuda-wheels simple index<br/>(GitHub Pages)"]
rel["GitHub Releases API<br/>(network fallback)"]
reg["Comfy Registry / GitHub<br/>(node dependencies)"]
ce -->|"generates manifests, runs pixi install"| pixi
pixi --> env1
ce -->|"resolves prebuilt CUDA wheel URLs"| idx
idx -.->|"unreachable"| rel
ce -->|"clones [node_packs] peers"| reg
ce ==>|"socket IPC + shared memory"| worker
env1 -.->|"provides interpreter"| worker
The workspace is shared machine-wide: env names (<plugin>-<subdir>,
ComfyUI- prefix stripped, lowercased) act as global identifiers, so two
ComfyUI installs that declare the same node reuse one materialized env
(ADR-0007).
Memory management¶
ComfyUI manages RAM and VRAM well, and every bit of that assumes one process.
Multi-process memory management does not exist upstream yet, so comfy-env does what it can from outside: it tells ComfyUI how much of the card the packs are really using, asks it to make room when a pack needs some, and has packs let go when they go quiet.
comfy-env's memory management is the whole story.
Logging¶
ComfyUI captures output by replacing sys.stdout and sys.stderr with a
wrapper that fans each write out to the terminal, an in-memory ring and the
browser's terminal panel. That interception is a Python object, not a file
descriptor, so a subprocess escapes it entirely — and comfy-env is
subprocesses.
So worker output travels back over the same IPC socket everything else uses,
as log frames the parent reprints in-process. What cannot travel that way,
and what comfy-env writes to disk instead, is in
comfy-env's logging.
Import layering¶
The modules under src/comfy_env/ form a layered, acyclic import graph:
nothing under isolation/ imports the top orchestrator wrap.py, and the
transport leaf _ipc_shared.py imports nothing from comfy_env at all. The
full graph, the invariants, and the CI contracts that check them are in
Import layering.
Where to go next¶
Nodepack Author Reference -- everything a pack declares:
- The three calls:
install(),setup_env(),register_nodes() - Config reference:
comfy-env.tomlandcomfy-env-root.toml - Accelerator declarations:
ACCELERATORand lazy imports - Custom wire types:
[types]+serialization.py
Nodepack User Reference -- the machine it runs on:
- Commands:
install,info,gc, ... - Settings reference: machine-global env vars
- System footprint: exactly what comfy-env writes outside the ComfyUI folder, why, and how to remove it
Internals -- how it works underneath:
- Module inventory: what every file under
src/comfy_env/does - Why not just conda?: the hand-rolled
[cuda]resolver and what deleting it would take - The process boundary: how tensors cross between parent and worker
- The three seals: the hashes that decide what rebuilds
- Decision records: the "why" behind each of these choices