inline-core 1.1.1__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.
- inline_core/__init__.py +7 -0
- inline_core/components/__init__.py +1 -0
- inline_core/components/conditioning.py +20 -0
- inline_core/components/interfaces.py +91 -0
- inline_core/config.py +33 -0
- inline_core/device/__init__.py +1 -0
- inline_core/device/auto.py +35 -0
- inline_core/device/detect.py +41 -0
- inline_core/device/memory.py +168 -0
- inline_core/device/policy.py +82 -0
- inline_core/device/types.py +31 -0
- inline_core/errors.py +42 -0
- inline_core/graph/__init__.py +1 -0
- inline_core/graph/cache.py +83 -0
- inline_core/graph/descriptor.py +81 -0
- inline_core/graph/executor.py +102 -0
- inline_core/graph/primitives.py +137 -0
- inline_core/graph/registry.py +61 -0
- inline_core/graph/runners.py +72 -0
- inline_core/graph/schema.py +136 -0
- inline_core/graph/topo.py +49 -0
- inline_core/graph/validate.py +56 -0
- inline_core/importer/__init__.py +1 -0
- inline_core/importer/comfy.py +162 -0
- inline_core/media.py +11 -0
- inline_core/models/__init__.py +8 -0
- inline_core/models/catalog.py +98 -0
- inline_core/models/zimage/__init__.py +11 -0
- inline_core/models/zimage/requirements.py +180 -0
- inline_core/models/zimage/runner.py +386 -0
- inline_core/parallel/__init__.py +13 -0
- inline_core/parallel/config.py +50 -0
- inline_core/parallel/group.py +136 -0
- inline_core/parallel/launch.py +44 -0
- inline_core/parallel/protocol.py +53 -0
- inline_core/parallel/registry.py +31 -0
- inline_core/parallel/worker.py +75 -0
- inline_core/runtime/__init__.py +1 -0
- inline_core/runtime/context.py +31 -0
- inline_core/runtime/file_store.py +51 -0
- inline_core/runtime/progress.py +83 -0
- inline_core/runtime/run.py +113 -0
- inline_core/runtime/store.py +18 -0
- inline_core/sampling/__init__.py +1 -0
- inline_core/sampling/batch.py +116 -0
- inline_core/server/__init__.py +1 -0
- inline_core/server/__main__.py +61 -0
- inline_core/server/app.py +327 -0
- inline_core/server/assets.py +47 -0
- inline_core/server/bootstrap.py +21 -0
- inline_core/server/frontend.py +37 -0
- inline_core/server/manager.py +195 -0
- inline_core/server/rpc.py +60 -0
- inline_core/server/run_store.py +155 -0
- inline_core/server/serialize.py +144 -0
- inline_core/studio/__init__.py +7 -0
- inline_core/studio/assets.py +194 -0
- inline_core/studio/config.py +29 -0
- inline_core/studio/fal.py +237 -0
- inline_core/studio/frames.py +552 -0
- inline_core/studio/generation.py +136 -0
- inline_core/studio/graph_build.py +109 -0
- inline_core/studio/handlers.py +236 -0
- inline_core/studio/models.py +173 -0
- inline_core/studio/moodboard.py +400 -0
- inline_core/studio/schema.py +278 -0
- inline_core/studio/store.py +291 -0
- inline_core/studio/timeline/__init__.py +6 -0
- inline_core/studio/timeline/compose.py +130 -0
- inline_core/studio/timeline/ffmpeg.py +76 -0
- inline_core/studio/timeline/render.py +120 -0
- inline_core/studio/timeline/resolve.py +189 -0
- inline_core/takes.py +31 -0
- inline_core-1.1.1.dist-info/METADATA +35 -0
- inline_core-1.1.1.dist-info/RECORD +77 -0
- inline_core-1.1.1.dist-info/WHEEL +4 -0
- inline_core-1.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""One running worker group per parallel config, created lazily and reused across jobs.
|
|
2
|
+
|
|
3
|
+
The server owns a single registry and shuts it down with the app; the parallel sampler asks it for
|
|
4
|
+
the group that matches a job's placement. Keying on the frozen ParallelConfig means the same model
|
|
5
|
+
and split reuse one process group.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .config import ParallelConfig
|
|
11
|
+
from .group import WorkerGroup
|
|
12
|
+
from .launch import Launcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GroupRegistry:
|
|
16
|
+
def __init__(self, launcher: Launcher | None = None) -> None:
|
|
17
|
+
self._launcher = launcher
|
|
18
|
+
self._groups: dict[ParallelConfig, WorkerGroup] = {}
|
|
19
|
+
|
|
20
|
+
def get_or_create(self, config: ParallelConfig) -> WorkerGroup:
|
|
21
|
+
group = self._groups.get(config)
|
|
22
|
+
if group is None:
|
|
23
|
+
group = WorkerGroup(config, self._launcher)
|
|
24
|
+
group.start()
|
|
25
|
+
self._groups[config] = group
|
|
26
|
+
return group
|
|
27
|
+
|
|
28
|
+
def shutdown_all(self) -> None:
|
|
29
|
+
while self._groups:
|
|
30
|
+
_, group = self._groups.popitem()
|
|
31
|
+
group.shutdown()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Rank entrypoint for a parallel denoise group. Launched by a Launcher, one process per rank.
|
|
2
|
+
|
|
3
|
+
Rank 0 owns the IPC channel to the manager: it receives jobs, streams progress, and returns results
|
|
4
|
+
through a pluggable handler. The handler decides what a job means: the stub handler (no torch) backs
|
|
5
|
+
the scaffold and the round-trip test; the xfuser handler (loads the sharded pipeline and runs the
|
|
6
|
+
collective denoise across all ranks) lands with the Z-Image runner (C2).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import socket
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .config import ADDR_ENV, CONFIG_ENV, ParallelConfig
|
|
17
|
+
from .protocol import MessageType, recv_message, send_message
|
|
18
|
+
|
|
19
|
+
# report(step, total) streams progress to the manager while a job runs.
|
|
20
|
+
ProgressFn = Callable[[int, int], None]
|
|
21
|
+
# handler(config, payload, report) -> result payload.
|
|
22
|
+
Handler = Callable[[ParallelConfig, dict[str, Any], ProgressFn], dict[str, Any]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _stub_handler(
|
|
26
|
+
config: ParallelConfig, payload: dict[str, Any], report: ProgressFn
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
total = int(payload.get("steps", 1))
|
|
29
|
+
for step in range(1, total + 1):
|
|
30
|
+
report(step, total)
|
|
31
|
+
return {"echo": payload, "model": config.model, "world_size": config.world_size}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _select_handler(config: ParallelConfig) -> Handler:
|
|
35
|
+
if config.stub:
|
|
36
|
+
return _stub_handler
|
|
37
|
+
raise NotImplementedError(
|
|
38
|
+
"the xfuser denoise handler lands with the Z-Image runner (C2); "
|
|
39
|
+
"only the stub handler is available today"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def serve(sock: socket.socket, config: ParallelConfig, handler: Handler) -> None:
|
|
44
|
+
send_message(sock, {"type": MessageType.READY, "worldSize": config.world_size})
|
|
45
|
+
while True:
|
|
46
|
+
message = recv_message(sock)
|
|
47
|
+
if message is None or message.get("type") == MessageType.SHUTDOWN:
|
|
48
|
+
return
|
|
49
|
+
if message.get("type") != MessageType.JOB:
|
|
50
|
+
continue
|
|
51
|
+
payload: dict[str, Any] = message.get("payload", {})
|
|
52
|
+
|
|
53
|
+
def report(step: int, total: int) -> None:
|
|
54
|
+
send_message(sock, {"type": MessageType.PROGRESS, "step": step, "total": total})
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
result = handler(config, payload, report)
|
|
58
|
+
except Exception as exc: # translate into a protocol error; the manager re-raises typed
|
|
59
|
+
send_message(sock, {"type": MessageType.ERROR, "message": str(exc)})
|
|
60
|
+
continue
|
|
61
|
+
send_message(sock, {"type": MessageType.RESULT, "payload": result})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
65
|
+
config = ParallelConfig.from_json(os.environ[CONFIG_ENV])
|
|
66
|
+
host, _, port = os.environ[ADDR_ENV].partition(":")
|
|
67
|
+
sock = socket.create_connection((host, int(port)))
|
|
68
|
+
try:
|
|
69
|
+
serve(sock, config, _select_handler(config))
|
|
70
|
+
finally:
|
|
71
|
+
sock.close()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Per-run execution: the progress event bus, run state, and the execution context."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""The per-run execution context threaded through every component call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from threading import Event
|
|
7
|
+
|
|
8
|
+
from ..device.policy import DevicePolicy
|
|
9
|
+
from .progress import ProgressEmitter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CancelToken:
|
|
13
|
+
"""Cooperative cancellation. The executor checks it between nodes and steps."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self._event = Event()
|
|
17
|
+
|
|
18
|
+
def cancel(self) -> None:
|
|
19
|
+
self._event.set()
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def cancelled(self) -> bool:
|
|
23
|
+
return self._event.is_set()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ExecutionContext:
|
|
28
|
+
run_id: str
|
|
29
|
+
policy: DevicePolicy
|
|
30
|
+
emitter: ProgressEmitter
|
|
31
|
+
cancel: CancelToken
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""A file-backed take store: writes a decoded image to <root>/<take_id>.png and records the take."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from ..media import MediaKind
|
|
12
|
+
from ..takes import Take
|
|
13
|
+
from .store import TakeStore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileTakeStore(TakeStore):
|
|
17
|
+
def __init__(self, root: Path) -> None:
|
|
18
|
+
self._root = root
|
|
19
|
+
|
|
20
|
+
def save(self, run_id: str, node_id: str, image: Any, params: dict[str, Any]) -> Take:
|
|
21
|
+
self._root.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
take_id = f"take_{uuid4().hex[:12]}"
|
|
23
|
+
path = self._root / f"{take_id}.png"
|
|
24
|
+
_to_pil(image).save(path, format="PNG")
|
|
25
|
+
data = path.read_bytes()
|
|
26
|
+
return Take(
|
|
27
|
+
id=take_id,
|
|
28
|
+
run_id=run_id,
|
|
29
|
+
node_id=node_id,
|
|
30
|
+
kind=MediaKind.IMAGE,
|
|
31
|
+
uri=str(path),
|
|
32
|
+
hash=f"sha256-{hashlib.sha256(data).hexdigest()}",
|
|
33
|
+
params=dict(params),
|
|
34
|
+
created_at=int(time.time() * 1000),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _to_pil(image: Any) -> Any:
|
|
39
|
+
from PIL import Image
|
|
40
|
+
|
|
41
|
+
if isinstance(image, Image.Image):
|
|
42
|
+
return image
|
|
43
|
+
if hasattr(image, "detach"): # a torch tensor
|
|
44
|
+
image = image.detach().to("cpu").numpy()
|
|
45
|
+
import numpy as np
|
|
46
|
+
|
|
47
|
+
array = np.asarray(image)
|
|
48
|
+
if array.dtype != np.uint8:
|
|
49
|
+
scaled = array * 255.0 if float(array.max(initial=0.0)) <= 1.0 else array
|
|
50
|
+
array = scaled.clip(0, 255).round().astype(np.uint8)
|
|
51
|
+
return Image.fromarray(array)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Run events and the emitter seam. Mirrors the websocket events in docs/contract.md section 6.
|
|
2
|
+
|
|
3
|
+
Coalescing (the contract's bounded event rate) is a wrapper added at the serving layer; the executor
|
|
4
|
+
and components just emit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
from ..takes import Take
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Phase(str, Enum):
|
|
17
|
+
QUEUED = "queued"
|
|
18
|
+
PREPARING = "preparing"
|
|
19
|
+
LOADING = "loading"
|
|
20
|
+
ENCODE = "encode"
|
|
21
|
+
SAMPLE = "sample"
|
|
22
|
+
DECODE = "decode"
|
|
23
|
+
SAVE = "save"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ProgressEvent:
|
|
28
|
+
run_id: str
|
|
29
|
+
node_id: str
|
|
30
|
+
phase: Phase
|
|
31
|
+
fraction: float
|
|
32
|
+
step: int | None = None
|
|
33
|
+
step_count: int | None = None
|
|
34
|
+
eta_ms: int | None = None
|
|
35
|
+
status: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class NodeDoneEvent:
|
|
40
|
+
run_id: str
|
|
41
|
+
node_id: str
|
|
42
|
+
cached: bool
|
|
43
|
+
takes: list[Take] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class RunDoneEvent:
|
|
48
|
+
run_id: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class CancelledEvent:
|
|
53
|
+
run_id: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ErrorEvent:
|
|
58
|
+
run_id: str
|
|
59
|
+
message: str
|
|
60
|
+
node_id: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
RunEvent = ProgressEvent | NodeDoneEvent | RunDoneEvent | CancelledEvent | ErrorEvent
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ProgressEmitter(ABC):
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def emit(self, event: RunEvent) -> None: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class NullEmitter(ProgressEmitter):
|
|
72
|
+
def emit(self, event: RunEvent) -> None:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CollectingEmitter(ProgressEmitter):
|
|
77
|
+
"""Keeps every event in order. For tests and the snapshot builder."""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self.events: list[RunEvent] = []
|
|
81
|
+
|
|
82
|
+
def emit(self, event: RunEvent) -> None:
|
|
83
|
+
self.events.append(event)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Run state: the durable snapshot GET /v1/runs/{id} serves, kept fresh by applying run events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from ..takes import Take
|
|
9
|
+
from .progress import (
|
|
10
|
+
CancelledEvent,
|
|
11
|
+
ErrorEvent,
|
|
12
|
+
NodeDoneEvent,
|
|
13
|
+
ProgressEmitter,
|
|
14
|
+
ProgressEvent,
|
|
15
|
+
RunDoneEvent,
|
|
16
|
+
RunEvent,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunStatus(str, Enum):
|
|
21
|
+
QUEUED = "queued"
|
|
22
|
+
RUNNING = "running"
|
|
23
|
+
DONE = "done"
|
|
24
|
+
ERROR = "error"
|
|
25
|
+
CANCELLED = "cancelled"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NodeState(str, Enum):
|
|
29
|
+
QUEUED = "queued"
|
|
30
|
+
RUNNING = "running"
|
|
31
|
+
CACHED = "cached"
|
|
32
|
+
DONE = "done"
|
|
33
|
+
ERROR = "error"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class NodeRuntimeState:
|
|
38
|
+
state: NodeState = NodeState.QUEUED
|
|
39
|
+
phase: str | None = None
|
|
40
|
+
fraction: float = 0.0
|
|
41
|
+
step: int | None = None
|
|
42
|
+
step_count: int | None = None
|
|
43
|
+
status: str = ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class RunError:
|
|
48
|
+
message: str
|
|
49
|
+
node_id: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class RunState:
|
|
54
|
+
run_id: str
|
|
55
|
+
target: str
|
|
56
|
+
status: RunStatus = RunStatus.QUEUED
|
|
57
|
+
fraction: float = 0.0
|
|
58
|
+
nodes: dict[str, NodeRuntimeState] = field(default_factory=dict)
|
|
59
|
+
takes: list[Take] = field(default_factory=list)
|
|
60
|
+
error: RunError | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_COMPLETE = (NodeState.DONE, NodeState.CACHED)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _recompute_fraction(state: RunState) -> None:
|
|
67
|
+
if not state.nodes:
|
|
68
|
+
state.fraction = 0.0
|
|
69
|
+
return
|
|
70
|
+
total = sum(1.0 if n.state in _COMPLETE else n.fraction for n in state.nodes.values())
|
|
71
|
+
state.fraction = total / len(state.nodes)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def apply_event(state: RunState, event: RunEvent) -> None:
|
|
75
|
+
"""Fold a run event into the run state. The single event -> snapshot mapping."""
|
|
76
|
+
if isinstance(event, ProgressEvent):
|
|
77
|
+
node = state.nodes.setdefault(event.node_id, NodeRuntimeState())
|
|
78
|
+
node.state = NodeState.RUNNING
|
|
79
|
+
node.phase = event.phase.value
|
|
80
|
+
node.fraction = event.fraction
|
|
81
|
+
node.step = event.step
|
|
82
|
+
node.step_count = event.step_count
|
|
83
|
+
node.status = event.status
|
|
84
|
+
_recompute_fraction(state)
|
|
85
|
+
elif isinstance(event, NodeDoneEvent):
|
|
86
|
+
node = state.nodes.setdefault(event.node_id, NodeRuntimeState())
|
|
87
|
+
node.state = NodeState.CACHED if event.cached else NodeState.DONE
|
|
88
|
+
node.fraction = 1.0
|
|
89
|
+
known = {t.id for t in state.takes}
|
|
90
|
+
state.takes.extend(t for t in event.takes if t.id not in known)
|
|
91
|
+
_recompute_fraction(state)
|
|
92
|
+
elif isinstance(event, RunDoneEvent):
|
|
93
|
+
state.status = RunStatus.DONE
|
|
94
|
+
state.fraction = 1.0
|
|
95
|
+
elif isinstance(event, CancelledEvent):
|
|
96
|
+
state.status = RunStatus.CANCELLED
|
|
97
|
+
elif isinstance(event, ErrorEvent):
|
|
98
|
+
state.status = RunStatus.ERROR
|
|
99
|
+
state.error = RunError(message=event.message, node_id=event.node_id)
|
|
100
|
+
if event.node_id is not None:
|
|
101
|
+
state.nodes.setdefault(event.node_id, NodeRuntimeState()).state = NodeState.ERROR
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class StateTrackingEmitter(ProgressEmitter):
|
|
105
|
+
"""Applies each event to a RunState, then forwards it to the real emitter."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, delegate: ProgressEmitter, state: RunState) -> None:
|
|
108
|
+
self._delegate = delegate
|
|
109
|
+
self._state = state
|
|
110
|
+
|
|
111
|
+
def emit(self, event: RunEvent) -> None:
|
|
112
|
+
apply_event(self._state, event)
|
|
113
|
+
self._delegate.emit(event)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The take store seam: persist a decoded output as an immutable take (bytes, hash, uri).
|
|
2
|
+
|
|
3
|
+
Phase 1's implementation writes into the project's takes/ folder. Kept behind this interface so a
|
|
4
|
+
fleet object store swaps in without touching the executor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..takes import Take
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TakeStore(ABC):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def save(self, run_id: str, node_id: str, image: Any, params: dict[str, Any]) -> Take:
|
|
18
|
+
"""Persist a decoded image (PIL, numpy, or tensor) as an immutable take."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The batched-sampler boundary: the graph submits jobs, this batches and steps them on the GPU."""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""The graph/GPU boundary. The graph never runs the denoise loop inline; it submits a SampleJob.
|
|
2
|
+
|
|
3
|
+
Phase 1 runs one job at a time behind this seam. Phase 5 adds cross-request batching (grouping by
|
|
4
|
+
model family, resolution, and adapter bucket) without changing the interface. Multi-GPU splits one
|
|
5
|
+
job across a worker group (XFuserBatchedSampler) when the device policy asks for a parallel denoise.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
13
|
+
|
|
14
|
+
from ..components.conditioning import Conditioning, Latents
|
|
15
|
+
from ..components.interfaces import Denoiser, Sampler, Scheduler, StepCallback, StepInfo
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..device.policy import Placement
|
|
19
|
+
from ..parallel.config import ParallelConfig
|
|
20
|
+
from ..parallel.group import ProgressHandler
|
|
21
|
+
from ..parallel.registry import GroupRegistry
|
|
22
|
+
from ..runtime.context import ExecutionContext
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class SampleJob:
|
|
27
|
+
"""One request to sample. Compatible jobs are grouped and stepped together."""
|
|
28
|
+
|
|
29
|
+
denoiser: Denoiser
|
|
30
|
+
scheduler: Scheduler
|
|
31
|
+
sampler: Sampler
|
|
32
|
+
latents: Latents
|
|
33
|
+
conditioning: Conditioning
|
|
34
|
+
steps: int
|
|
35
|
+
on_step: StepCallback | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BatchedSampler(ABC):
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def submit(self, job: SampleJob, ctx: ExecutionContext) -> Latents: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class InlineBatchedSampler(BatchedSampler):
|
|
44
|
+
"""Phase 1: no batching. Run the job through its sampler directly."""
|
|
45
|
+
|
|
46
|
+
def submit(self, job: SampleJob, ctx: ExecutionContext) -> Latents:
|
|
47
|
+
return job.sampler.sample(
|
|
48
|
+
job.denoiser,
|
|
49
|
+
job.scheduler,
|
|
50
|
+
job.latents,
|
|
51
|
+
job.conditioning,
|
|
52
|
+
steps=job.steps,
|
|
53
|
+
ctx=ctx,
|
|
54
|
+
on_step=job.on_step,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ParallelCodec(Protocol):
|
|
59
|
+
"""Bridges a SampleJob and its worker group: which group, request out, latents back.
|
|
60
|
+
|
|
61
|
+
The real codec moves torch tensors over a side channel and builds the xfuser request; it lives
|
|
62
|
+
with the model runner (C2) so this module stays torch-free and the group stays mockable.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def config(self, placement: Placement) -> ParallelConfig:
|
|
66
|
+
"""The group identity (model + split) for this placement."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def to_request(self, job: SampleJob) -> dict[str, Any]:
|
|
70
|
+
"""The serializable request the worker runs against its resident pipeline."""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def from_result(self, result: dict[str, Any], job: SampleJob) -> Latents:
|
|
74
|
+
"""The sampled latents the worker returned."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class XFuserBatchedSampler(BatchedSampler):
|
|
79
|
+
"""Routes a parallel-placement denoise to the worker group; everything else runs inline.
|
|
80
|
+
|
|
81
|
+
The device policy decides: a single-GPU or CPU run has no parallel placement, so it keeps the
|
|
82
|
+
in-process path with zero overhead. Only a multi-GPU denoiser placement crosses to the group.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
registry: GroupRegistry,
|
|
88
|
+
codec: ParallelCodec,
|
|
89
|
+
inline: BatchedSampler | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
self._registry = registry
|
|
92
|
+
self._codec = codec
|
|
93
|
+
self._inline = inline or InlineBatchedSampler()
|
|
94
|
+
|
|
95
|
+
def submit(self, job: SampleJob, ctx: ExecutionContext) -> Latents:
|
|
96
|
+
placement = ctx.policy.placement("denoiser")
|
|
97
|
+
if placement.parallel is None:
|
|
98
|
+
return self._inline.submit(job, ctx)
|
|
99
|
+
group = self._registry.get_or_create(self._codec.config(placement))
|
|
100
|
+
result = group.submit(self._codec.to_request(job), _forward_progress(job))
|
|
101
|
+
return self._codec.from_result(result, job)
|
|
102
|
+
|
|
103
|
+
def close(self) -> None:
|
|
104
|
+
self._registry.shutdown_all()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _forward_progress(job: SampleJob) -> ProgressHandler | None:
|
|
108
|
+
"""Feed worker step ticks into the job's on_step callback, so streaming stays unchanged."""
|
|
109
|
+
on_step = job.on_step
|
|
110
|
+
if on_step is None:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def report(step: int, total: int) -> None:
|
|
114
|
+
on_step(StepInfo(step=step, total=total))
|
|
115
|
+
|
|
116
|
+
return report
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The /v1 HTTP and websocket serving layer. A thin shell over the graph engine (see contract)."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Run the engine: `python -m inline_core.server`. Registers models whose deps are installed."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
6
|
+
|
|
7
|
+
from ..config import data_dir, server_host, server_port
|
|
8
|
+
from ..device.memory import MemoryPolicy
|
|
9
|
+
from ..graph.cache import InMemoryCache
|
|
10
|
+
from ..graph.registry import build_default_registry
|
|
11
|
+
from ..runtime.file_store import FileTakeStore
|
|
12
|
+
from ..studio import config as studio_config
|
|
13
|
+
from ..studio.store import StudioStore
|
|
14
|
+
from .app import create_app
|
|
15
|
+
from .bootstrap import register_models
|
|
16
|
+
from .frontend import resolve_frontend_root
|
|
17
|
+
from .rpc import EventBroadcaster, RpcRouter
|
|
18
|
+
from .run_store import SqliteRunStore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
policy = MemoryPolicy()
|
|
23
|
+
registry = build_default_registry()
|
|
24
|
+
data = data_dir()
|
|
25
|
+
takes = data / "takes"
|
|
26
|
+
store = FileTakeStore(takes)
|
|
27
|
+
run_store = SqliteRunStore(data / "runs.db")
|
|
28
|
+
registered = register_models(registry, store, policy)
|
|
29
|
+
print(f"Registered models: {registered or 'none (source nodes only)'}")
|
|
30
|
+
frontend_root = resolve_frontend_root()
|
|
31
|
+
fe = frontend_root or "none (API only); use --front-end-root or install the frontend package"
|
|
32
|
+
print(f"Frontend: {fe}")
|
|
33
|
+
# The Studio app-backend: Core is the sole native backend (projects, frames, moodboard, assets,
|
|
34
|
+
# generation, fal, timeline). Every InlineStudioApi channel is handled here.
|
|
35
|
+
rpc = RpcRouter()
|
|
36
|
+
events = EventBroadcaster()
|
|
37
|
+
store = StudioStore(
|
|
38
|
+
studio_config.data_dir(),
|
|
39
|
+
studio_config.workspace_dir(),
|
|
40
|
+
default_comfy_url=studio_config.DEFAULT_COMFY_URL,
|
|
41
|
+
default_core_url=studio_config.DEFAULT_CORE_URL,
|
|
42
|
+
)
|
|
43
|
+
print(f"Studio data: {studio_config.data_dir()} | workspace: {studio_config.workspace_dir()}")
|
|
44
|
+
app = create_app(
|
|
45
|
+
registry=registry,
|
|
46
|
+
cache=InMemoryCache(),
|
|
47
|
+
policy=policy,
|
|
48
|
+
run_store=run_store,
|
|
49
|
+
takes_dir=str(takes),
|
|
50
|
+
frontend_root=frontend_root,
|
|
51
|
+
rpc=rpc,
|
|
52
|
+
events=events,
|
|
53
|
+
studio_store=store,
|
|
54
|
+
)
|
|
55
|
+
host, port = server_host(), server_port()
|
|
56
|
+
print(f"Serving on http://{host}:{port}")
|
|
57
|
+
uvicorn.run(app, host=host, port=port)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|