Schedule a node across every machine, not just this one
The engine answered "where does this node run" twice, in two ways that could not see each other: a device sent it to a worker carrying that label, and resources were counted against the engine's own cores. Declaring both meant the second answer won and nothing was counted at all — which the data-science getting-started page and the worked example both do. One question now, in flow/placement.py: of every machine attached, which could grant what this node asked for, and which of those has it free. The books move onto each machine — one accountant per worker, built from the inventory it reported — and the waiting moves above them, where one condition variable can be woken by a release anywhere or by a worker attaching. Locks go one way: placer, then a machine's books, never back. So a node asking for a card now finds the box that has one, rather than being clamped down to none and run here. When nothing can grant the ask at all it is still cut down and run — a flow written on a cluster has to work on a laptop — but the ceiling is one real machine now, since taking the largest of each dimension separately can describe a machine nobody has. Two things fixed on the way. A device on a connector node held every batch run of its flow forever, waiting for a worker that could never run an entry point. And `prefer` falling back to the engine skipped the books, so the fallback held nothing. The bench flow's node has taken a `params` argument that with_settings has not forwarded for some time, so the benchmark could not run at all: 62 ms median submit-to-result with this, against the 61 ms on record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6HeySA27EkGANZN95QySW
This commit is contained in:
@@ -32,6 +32,7 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fluksio.flow.nodes.base import NodeOutputError
|
||||
from fluksio.flow.resources import ResourceAccountant
|
||||
from fluksio.flow.workers import NodeTimeout, RemoteError, _remote_class
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -106,6 +107,14 @@ class RemoteWorker:
|
||||
self.labels = set(labels)
|
||||
self.info = info or {}
|
||||
self.inventory = inventory or WorkerInventory()
|
||||
# This machine's books, held here so that a worker reconnecting — which
|
||||
# is a new object — starts from a clean set rather than from counters
|
||||
# the engine kept for a socket that is gone.
|
||||
self.accountant = ResourceAccountant(
|
||||
cpus=self.inventory.cpus,
|
||||
gpus=self.inventory.gpus,
|
||||
ram_mb=self.inventory.ram_mb,
|
||||
)
|
||||
self.attached_at = time.time()
|
||||
self.last_seen = time.time()
|
||||
self._send = send
|
||||
@@ -251,9 +260,13 @@ class RemoteWorker:
|
||||
class RemoteWorkerHub:
|
||||
"""Every attached worker, and which of them a node may run on."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, on_change: Callable[[], None] | None = None) -> None:
|
||||
self._workers: dict[str, RemoteWorker] = {}
|
||||
self._lock = threading.Lock()
|
||||
#: Told when the set of attached workers changes, so whoever is waiting
|
||||
#: for a machine can look again. Called outside the lock: it takes one
|
||||
#: of its own, and the two are only ever taken in that order.
|
||||
self.on_change = on_change
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Attachment
|
||||
@@ -270,6 +283,7 @@ class RemoteWorkerHub:
|
||||
logger.info(
|
||||
"Worker '%s' attached with labels %s", worker.name, sorted(worker.labels)
|
||||
)
|
||||
self._changed()
|
||||
|
||||
def detach(self, name: str) -> None:
|
||||
with self._lock:
|
||||
@@ -277,6 +291,11 @@ class RemoteWorkerHub:
|
||||
if worker is not None:
|
||||
worker.detach()
|
||||
logger.info("Worker '%s' detached", name)
|
||||
self._changed()
|
||||
|
||||
def _changed(self) -> None:
|
||||
if self.on_change is not None:
|
||||
self.on_change()
|
||||
|
||||
def workers(self) -> list[RemoteWorker]:
|
||||
with self._lock:
|
||||
@@ -326,6 +345,38 @@ class RemoteWorkerHub:
|
||||
worker = self.pick(label)
|
||||
if worker is None:
|
||||
raise NoWorker(f"no worker labelled '{label}' is attached")
|
||||
return self.run_on(
|
||||
worker,
|
||||
flow,
|
||||
node,
|
||||
source,
|
||||
kwargs,
|
||||
node_id,
|
||||
timeout,
|
||||
run_id=run_id,
|
||||
on_event=on_event,
|
||||
env=env,
|
||||
)
|
||||
|
||||
def run_on(
|
||||
self,
|
||||
worker: RemoteWorker,
|
||||
flow: str,
|
||||
node: str,
|
||||
source: str,
|
||||
kwargs: dict[str, Any],
|
||||
node_id: str,
|
||||
timeout: float,
|
||||
run_id: str = "",
|
||||
on_event: Callable[[dict[str, Any]], None] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> Any:
|
||||
"""Run this node on the worker the caller has already settled on.
|
||||
|
||||
The placer holds that machine's cores for the length of the call, so
|
||||
resolving the label a second time here could send the work somewhere
|
||||
else and leave the claim on a machine doing nothing.
|
||||
"""
|
||||
payload = {
|
||||
"op": "run",
|
||||
"call_id": f"{run_id}:{node_id}" if run_id else node_id,
|
||||
|
||||
Reference in New Issue
Block a user