Name the sizes a node can ask for

Raw cpus and gpus are a property of the machines an installation has, so a node
written against a cluster quietly stops meaning anything when the cluster is
replaced. A node says "gpu-small" instead, and what that is stored here —
editable, and read again every time the node is built, so changing the flavor
changes what the next run gets.

Memory joins the schema properly (`ram`, in MB, accepting "2G"), along with
`duration_s` for how long a node is expected to take. That one is recorded and
shown and nothing else yet: a statement for whoever is planning around the node,
not a limit — the limit is still `timeout`.

A flavor and a number for the same thing is refused, compared by value so an
editor writing the whole object back with its defaults still round-trips. A name
nothing stores is refused at the save, which covers the canvas and `fluksio
sync` at once, and deleting one a node still asks for says which node.

Four sizes are seeded on an installation that has none, and never re-seeded:
re-adding one somebody deliberately removed is an argument nobody wins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6HeySA27EkGANZN95QySW
This commit is contained in:
2026-08-27 08:59:10 +02:00
co-authored by Claude Opus 5
parent 6ff56533f5
commit a82f88cf0a
15 changed files with 720 additions and 19 deletions
+23 -7
View File
@@ -64,7 +64,12 @@ from fluksio.flow.pipeline import (
)
from fluksio.flow.placement import Placer
from fluksio.flow.remote import RemoteWorkerHub
from fluksio.flow.resources import ResourceAccountant, derive_env
from fluksio.flow.resources import (
ResourceAccountant,
UnknownFlavor,
derive_env,
resolve_flavor,
)
from fluksio.flow.schemas import (
BrainEdge,
BrainGraph,
@@ -869,11 +874,10 @@ class FlowController:
# Building
# -------------------------------------------------------------------------
def _runs_elsewhere(self, node_def: NodeDef) -> bool:
def _runs_elsewhere(self, wanted: Resources | None) -> bool:
"""Whether this node asks for more than this machine could ever give."""
if node_def.resources is None or self.placer is None:
if wanted is None or self.placer is None:
return False
wanted = node_def.resources
return not self.placer.local.fits(wanted.cpus, wanted.gpus, wanted.ram or 0)
def _placed(
@@ -1016,6 +1020,18 @@ class FlowController:
# one importing torch is correct on the GPU box and a
# missing module here, so checking it here would fail a
# node that is fine.
try:
# A named size is read now rather than stored on the
# node, so editing the flavor edits the next run.
wanted = (
resolve_flavor(node_def.resources)
if node_def.resources is not None
else None
)
except UnknownFlavor as exc:
entry.status = NodeStatus.ERROR
entry.error = str(exc)
return entry
remote_only = (
node_def.device
and node_def.device_policy == "require"
@@ -1025,7 +1041,7 @@ class FlowController:
problem = self.remote.compile(
node_def.device or "", owner, local, code
)
elif self._runs_elsewhere(node_def):
elif self._runs_elsewhere(wanted):
# Asks for more than this machine has, so it will run on
# one that has it. Same reason as a device: checking the
# import here would fail a node that is fine there.
@@ -1043,13 +1059,13 @@ class FlowController:
if node_def.timeout is not None
else settings.FLOW_NODE_TIMEOUT
)
if node_def.resources is not None and self.placer is not None:
if wanted is not None and self.placer is not None:
# Says how much of a machine it takes, so which machine
# and how much of it are one decision — including when
# it also names a device, which used to mean the two
# answers disagreed and nothing was accounted at all.
function = self._placed(
node_def.resources,
wanted,
node_def.device,
node_def.device_policy,
owner,