221 lines
7.2 KiB
Python
221 lines
7.2 KiB
Python
"""ComfyUI V3 node for resident LLM inference in the Modal worker."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from typing import Any, Sequence
|
|
|
|
import comfy.model_management
|
|
import comfy.utils
|
|
from comfy_api.latest import io
|
|
|
|
if __package__:
|
|
from .llm_profiles import MODAL_LLM_NODE_ID, llm_profile_options
|
|
from .modal_llm_runtime import run_modal_llm_inference
|
|
else: # pragma: no cover - the stable remote runtime imports this module top-level.
|
|
from llm_profiles import MODAL_LLM_NODE_ID, llm_profile_options
|
|
from modal_llm_runtime import run_modal_llm_inference
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _modal_llm_primary_inputs() -> list[io.Input]:
|
|
"""Return the prompt and multimodal inputs shown by default."""
|
|
return [
|
|
io.String.Input(
|
|
"prompt",
|
|
default="",
|
|
multiline=True,
|
|
tooltip="Text instructions or content for the resident Modal language model.",
|
|
),
|
|
io.String.Input(
|
|
"model_profile",
|
|
default=llm_profile_options()[0],
|
|
tooltip=(
|
|
"A curated profile or Hugging Face ID such as owner/model. The first run "
|
|
"inspects, pins, and stages a compatible model on the Modal Volume."
|
|
),
|
|
),
|
|
io.Image.Input(
|
|
"images",
|
|
optional=True,
|
|
tooltip="Optional ComfyUI IMAGE batch for visual understanding.",
|
|
),
|
|
io.Video.Input(
|
|
"video",
|
|
optional=True,
|
|
tooltip="Optional native ComfyUI VIDEO sampled into bounded timestamped frames.",
|
|
),
|
|
io.Custom("OPENAI_INPUT_FILES").Input(
|
|
"files",
|
|
optional=True,
|
|
tooltip="Optional UTF-8 text or PDF files from OpenAI ChatGPT Input Files.",
|
|
),
|
|
io.String.Input(
|
|
"system_prompt",
|
|
default="",
|
|
multiline=True,
|
|
optional=True,
|
|
tooltip="Optional system instruction for the model.",
|
|
),
|
|
]
|
|
|
|
|
|
def _modal_llm_advanced_inputs() -> list[io.Input]:
|
|
"""Return bounded generation and residency controls."""
|
|
return [
|
|
io.Int.Input(
|
|
"max_new_tokens",
|
|
default=512,
|
|
min=1,
|
|
max=32768,
|
|
step=1,
|
|
advanced=True,
|
|
tooltip="Maximum number of response tokens.",
|
|
),
|
|
io.Float.Input(
|
|
"temperature",
|
|
default=0.2,
|
|
min=0.0,
|
|
max=2.0,
|
|
step=0.05,
|
|
advanced=True,
|
|
tooltip="Sampling temperature. Zero selects deterministic greedy decoding.",
|
|
),
|
|
io.Float.Input(
|
|
"top_p",
|
|
default=0.95,
|
|
min=0.01,
|
|
max=1.0,
|
|
step=0.01,
|
|
advanced=True,
|
|
tooltip="Nucleus-sampling probability mass when temperature is non-zero.",
|
|
),
|
|
io.Int.Input(
|
|
"seed",
|
|
default=0,
|
|
min=0,
|
|
max=0x7FFFFFFFFFFFFFFF,
|
|
step=1,
|
|
advanced=True,
|
|
tooltip="Generation seed.",
|
|
),
|
|
io.Int.Input(
|
|
"video_frames",
|
|
default=12,
|
|
min=1,
|
|
max=32,
|
|
step=1,
|
|
advanced=True,
|
|
tooltip="Number of uniformly spaced video frames to analyze.",
|
|
),
|
|
io.Float.Input(
|
|
"reserve_free_vram_gb",
|
|
default=24.0,
|
|
min=0.0,
|
|
max=256.0,
|
|
step=1.0,
|
|
advanced=True,
|
|
tooltip="VRAM kept free for ComfyUI image/video models before loading this LLM.",
|
|
),
|
|
io.Boolean.Input(
|
|
"keep_model_loaded",
|
|
default=True,
|
|
advanced=True,
|
|
tooltip="Keep the model resident for subsequent nodes in this warm Modal container.",
|
|
),
|
|
]
|
|
|
|
|
|
class ModalLLM(io.ComfyNode):
|
|
"""Generate text with a resident multimodal Transformers model on Modal."""
|
|
|
|
@classmethod
|
|
def define_schema(cls) -> io.Schema:
|
|
"""Expose an OpenAI-like multimodal node contract for in-worker inference."""
|
|
return io.Schema(
|
|
node_id=MODAL_LLM_NODE_ID,
|
|
display_name="Modal LLM",
|
|
category="Modal/text",
|
|
essentials_category="Text Generation",
|
|
description=(
|
|
"Run text, image, video, and bounded file understanding through a "
|
|
"revision-pinned language model resident beside ComfyUI on the Modal GPU. "
|
|
"Enable Run on Modal for this node."
|
|
),
|
|
inputs=[*_modal_llm_primary_inputs(), *_modal_llm_advanced_inputs()],
|
|
outputs=[
|
|
io.String.Output(display_name="response"),
|
|
io.String.Output(display_name="metadata_json"),
|
|
],
|
|
hidden=[io.Hidden.unique_id],
|
|
not_idempotent=True,
|
|
is_experimental=True,
|
|
)
|
|
|
|
@classmethod
|
|
def execute(
|
|
cls,
|
|
prompt: str,
|
|
model_profile: str,
|
|
images: Any | None = None,
|
|
video: Any | None = None,
|
|
files: Sequence[Any] | None = None,
|
|
system_prompt: str = "",
|
|
max_new_tokens: int = 512,
|
|
temperature: float = 0.2,
|
|
top_p: float = 0.95,
|
|
seed: int = 0,
|
|
video_frames: int = 12,
|
|
reserve_free_vram_gb: float = 24.0,
|
|
keep_model_loaded: bool = True,
|
|
unique_id: str | None = None,
|
|
) -> io.NodeOutput:
|
|
"""Run one cancellation-aware resident inference request."""
|
|
if os.getenv("COMFY_MODAL_REMOTE_WORKER") != "1":
|
|
raise RuntimeError(
|
|
"Modal LLM only runs inside the Modal worker. Enable 'Run on Modal' for this "
|
|
"node and choose a GPU workflow target."
|
|
)
|
|
progress_bar = comfy.utils.ProgressBar(max_new_tokens, node_id=unique_id)
|
|
|
|
def report_progress(generated_tokens: int) -> None:
|
|
"""Mirror token generation into ComfyUI progress and cancellation."""
|
|
comfy.model_management.throw_exception_if_processing_interrupted()
|
|
progress_bar.update_absolute(generated_tokens, max_new_tokens)
|
|
|
|
logger.info(
|
|
"Starting Modal LLM profile=%s max_new_tokens=%d images=%s video=%s files=%d.",
|
|
model_profile,
|
|
max_new_tokens,
|
|
getattr(images, "shape", None),
|
|
video is not None,
|
|
len(files or ()),
|
|
)
|
|
result = run_modal_llm_inference(
|
|
prompt=prompt,
|
|
model_profile=model_profile,
|
|
images=images,
|
|
video=video,
|
|
files=files,
|
|
system_prompt=system_prompt,
|
|
max_new_tokens=max_new_tokens,
|
|
temperature=temperature,
|
|
top_p=top_p,
|
|
seed=seed,
|
|
video_frames=video_frames,
|
|
reserve_free_vram_gb=reserve_free_vram_gb,
|
|
keep_model_loaded=keep_model_loaded,
|
|
progress_callback=report_progress,
|
|
)
|
|
progress_bar.update_absolute(result.metadata["output_tokens"], max_new_tokens)
|
|
return io.NodeOutput(
|
|
result.text,
|
|
json.dumps(result.metadata, sort_keys=True, separators=(",", ":")),
|
|
)
|
|
|
|
|
|
__all__ = ["ModalLLM"]
|