diff --git a/README.md b/README.md index e4dab6e..a11363a 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ In remote mode, assets and custom-node archives are uploaded into the configured Asset paths are planned once per queued prompt. Repeated references across nodes or remote components share one hash, sync-index lookup, and upload decision while every component still receives the same content-addressed remote path and reload metadata. -Custom-node sync is enabled by default in remote mode and disabled by default in local mode. When enabled, Modal-Sync packages `custom_nodes/` as a whole-tree manifest plus content-addressed code archives for each top-level custom-node package. Package-owned model artifacts such as `.pth`, `.safetensors`, `.gguf`, and `.onnx` files are stored separately and linked into the extracted package on the worker, so a code edit does not recompress or reupload a multi-gigabyte model. Nested virtual environments, caches, compiled Python artifacts, logs, and temporary files are excluded. Unchanged code and asset digests are reused through a Modal `Dict` sync index instead of probing the volume for many marker files. +Custom-node sync is enabled by default in remote mode and disabled by default in local mode. When enabled, Modal-Sync packages `custom_nodes/` as a whole-tree manifest plus content-addressed code archives for each top-level custom-node package. Package-owned model artifacts such as `.pth`, `.safetensors`, `.gguf`, and `.onnx` files are stored separately and linked into the extracted package on the worker, so a code edit does not recompress or reupload a multi-gigabyte model. Nested virtual environments, caches, compiled Python artifacts, logs, and temporary files are excluded. Unchanged code and asset digests are reused through a Modal `Dict` sync index instead of probing the volume for many marker files. The remote image includes the GL and GLib runtime libraries required when a custom-node dependency installs GUI-enabled OpenCV, even though Modal-Sync's core runtime uses the headless OpenCV wheel. When a synced top-level custom-node package has a `requirements.txt`, those requirements are folded into the Modal image build. `-r other-file.txt` includes are followed relative to the declaring package; pip option and constraint lines are ignored. diff --git a/comfyui_modal_sync_cloud.py b/comfyui_modal_sync_cloud.py index 8901e40..f71a1ab 100644 --- a/comfyui_modal_sync_cloud.py +++ b/comfyui_modal_sync_cloud.py @@ -50,6 +50,7 @@ from runtime_environment import ( # noqa: E402 - paths are bootstrapped above. REMOTE_PYTHON_VERSION, build_remote_runtime_identity, custom_node_runtime_packages as _custom_node_runtime_packages, + remote_apt_packages as _comfyui_apt_packages, remote_runtime_packages as _comfyui_runtime_packages, remote_torch_packages as _comfyui_torch_packages, ) @@ -5898,6 +5899,7 @@ if modal is not None: # pragma: no branch - remote entrypoint configuration. ) image = ( modal.Image.debian_slim(python_version=REMOTE_PYTHON_VERSION) + .apt_install(*_comfyui_apt_packages()) .env( { "COMFY_MODAL_RUNTIME_FINGERPRINT": runtime_identity.fingerprint, diff --git a/docs/LEARNINGS.md b/docs/LEARNINGS.md index e7b1546..576eb70 100644 --- a/docs/LEARNINGS.md +++ b/docs/LEARNINGS.md @@ -301,3 +301,4 @@ - Custom-node source and package-owned model artifacts need different content-addressed lifecycles. The measured tree contained 182,825,396 bytes of ordinary package files but 3,062,086,445 bytes in two model artifacts; keeping those artifacts outside per-package ZIPs means a code-only edit no longer recompresses or reuploads them, while directory exclusions continue to discard accidentally synchronized `.venv` trees entirely. - Modal Volume persistence belongs to the stream-owning request lifecycle, not a daemon compute thread that can outlive its RPC client during shutdown or preemption. Defer bridge-object writes from the worker, merge them with any oversized final result, perform one volume commit on the consumer thread, and only then publish completed invocation metadata so retries cannot observe a durable ref before its bytes are committed. - A stale mounted-volume snapshot is not a reason to call `Volume.reload()` while a warm ComfyUI worker holds mmap-backed model tensors. Modal rejects that operation with `there are open files preventing the operation`; durable object misses should instead fetch the committed path through `Volume.read_file()`, validate its recorded size and SHA256, and leave both the mount and warm loader state untouched. +- Pinning `opencv-python-headless` in the core Modal runtime does not guarantee that `cv2` stays headless. Custom-node requirements can install `opencv-python` directly, and dependencies such as RMBG's `transparent-background` can pull it transitively. The Modal image therefore needs `libgl1` and `libglib2.0-0` so GUI-enabled OpenCV remains importable for custom nodes such as RMBG and Inspire Pack. diff --git a/runtime_environment.py b/runtime_environment.py index 1b3aaee..e67b519 100644 --- a/runtime_environment.py +++ b/runtime_environment.py @@ -17,6 +17,10 @@ REMOTE_PYTHON_VERSION = "3.11" REMOTE_MODAL_SDK_SPEC = "modal==1.4.2" PYTORCH_CUDA_INDEX_URL = "https://download.pytorch.org/whl/cu128" +_REMOTE_APT_PACKAGES = ( + "libgl1", + "libglib2.0-0", +) _REMOTE_RUNTIME_PACKAGES = ( "aiohttp==3.13.3", "alembic==1.18.4", @@ -125,6 +129,11 @@ def remote_runtime_packages() -> tuple[str, ...]: return _REMOTE_RUNTIME_PACKAGES +def remote_apt_packages() -> tuple[str, ...]: + """Return system libraries required by Python packages in the Modal image.""" + return _REMOTE_APT_PACKAGES + + def remote_torch_packages() -> tuple[str, ...]: """Return the exact CUDA PyTorch package set used by Modal images.""" return _REMOTE_TORCH_PACKAGES @@ -295,6 +304,7 @@ def build_remote_runtime_identity( "protocol_version": REMOTE_APP_PROTOCOL_VERSION, "python_version": REMOTE_PYTHON_VERSION, "modal_sdk_spec": REMOTE_MODAL_SDK_SPEC, + "apt_packages": list(remote_apt_packages()), "runtime_packages": list(remote_runtime_packages()), "torch_packages": list(remote_torch_packages()), "custom_node_packages": list(custom_node_runtime_packages(custom_nodes_dir)), diff --git a/tests/test_modal_executor_node.py b/tests/test_modal_executor_node.py index ff02b0c..665aadf 100644 --- a/tests/test_modal_executor_node.py +++ b/tests/test_modal_executor_node.py @@ -3402,6 +3402,8 @@ def test_modal_cloud_installs_comfyui_runtime_packages( modal_cloud_module: Any, ) -> None: """The Modal cloud image should include the core packages ComfyUI imports at runtime.""" + assert modal_cloud_module._comfyui_apt_packages() == ("libgl1", "libglib2.0-0") + packages = modal_cloud_module._comfyui_runtime_packages() package_names = {package.split("==", maxsplit=1)[0] for package in packages} diff --git a/tests/test_runtime_environment.py b/tests/test_runtime_environment.py index 9e032b4..7d3e99b 100644 --- a/tests/test_runtime_environment.py +++ b/tests/test_runtime_environment.py @@ -54,11 +54,13 @@ def _runtime_identity( def test_remote_environment_is_fully_pinned(runtime_environment_module: Any) -> None: """The supported Python, ComfyUI, and CUDA environments should not float.""" + apt_packages = runtime_environment_module.remote_apt_packages() runtime_packages = runtime_environment_module.remote_runtime_packages() torch_packages = runtime_environment_module.remote_torch_packages() assert runtime_environment_module.REMOTE_APP_PROTOCOL_VERSION >= 5 assert runtime_environment_module.REMOTE_PYTHON_VERSION == "3.11" + assert apt_packages == ("libgl1", "libglib2.0-0") assert all("==" in requirement for requirement in runtime_packages) assert all("==" in requirement for requirement in torch_packages) assert len(runtime_packages) == len(set(runtime_packages)) @@ -94,6 +96,21 @@ def test_runtime_identity_changes_with_source_and_runtime_options( assert option_changed.fingerprint != source_changed.fingerprint +def test_runtime_identity_records_system_packages( + runtime_environment_module: Any, + tmp_path: Path, +) -> None: + """The deployment manifest should record system libraries installed in the image.""" + repo_root = tmp_path / "repo" + comfyui_root = tmp_path / "ComfyUI" + repo_root.mkdir() + comfyui_root.mkdir() + + identity = _runtime_identity(runtime_environment_module, repo_root, comfyui_root) + + assert identity.manifest["apt_packages"] == ["libgl1", "libglib2.0-0"] + + def test_runtime_identity_tracks_custom_node_requirements_but_ignores_payload_source( runtime_environment_module: Any, tmp_path: Path,