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
+11 -3
View File
@@ -268,7 +268,7 @@ def node(
device: str | None = None,
device_policy: str = "require",
cache: bool = True,
resources: dict[str, Any] | None = None,
resources: dict[str, Any] | str | None = None,
) -> Callable[[F], F]:
"""Mark a function as a node, declaring its ports.
@@ -302,14 +302,22 @@ def node(
library that sizes itself to every visible core is told otherwise. ``env``
is for the tuning the engine must not invent, such as ``XLA_FLAGS``.
Declaring nothing is the default and changes nothing.
A string is a stored flavor — ``resources="gpu-small"`` — which is the same
thing as ``{"flavor": "gpu-small"}`` and stands in for cpus, gpus and ram.
Sizes accept ``"2G"``; ``duration_s`` accepts ``"30m"``.
"""
if device_policy not in ("require", "prefer"):
raise SyncError("device_policy is 'require' or 'prefer'")
if isinstance(resources, str):
resources = {"flavor": resources}
if resources is not None:
unknown = sorted(set(resources) - {"cpus", "gpus", "env"})
known = {"cpus", "gpus", "ram", "flavor", "duration_s", "env"}
unknown = sorted(set(resources) - known)
if unknown:
raise SyncError(
f"resources={{'{unknown[0]}': ...}} is not one of cpus, gpus, env"
f"resources={{'{unknown[0]}': ...}} is not one of "
f"{', '.join(sorted(known))}"
)
if timeout is not None and timeout < 0:
raise SyncError(