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:
@@ -26,14 +26,21 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlmodel import Session
|
||||
|
||||
from fluksio.flow.schemas import Resources
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: What a flavor may be called. Dashes allowed, unlike a flow or node name —
|
||||
#: "gpu-small" reads better than "gpu_small" on a dropdown.
|
||||
FLAVOR_NAME = re.compile(r"^[a-z][a-z0-9_-]*$")
|
||||
|
||||
#: Every spelling of "how many threads may you use" that a scientific stack
|
||||
#: reads out of the environment at import. Set together, because a process
|
||||
#: usually pulls in more than one of them.
|
||||
@@ -175,6 +182,42 @@ def derive_env(wanted: Resources, allocation: Allocation) -> dict[str, str]:
|
||||
return env
|
||||
|
||||
|
||||
class UnknownFlavor(ValueError):
|
||||
"""A node asks for a size that is not stored here."""
|
||||
|
||||
|
||||
def resolve_flavor(wanted: Resources) -> Resources:
|
||||
"""The concrete numbers behind a declaration.
|
||||
|
||||
Read when the node is built rather than stored on it, so editing a flavor
|
||||
changes what the next run gets. A node that names one that has been deleted
|
||||
is an error rather than a default: running a training step against a size
|
||||
nobody chose is worse than a node that says what is wrong with it.
|
||||
"""
|
||||
if not wanted.flavor:
|
||||
return wanted
|
||||
# Imported here: this module is the books, and the books have no business
|
||||
# knowing about the database until somebody asks for a stored size.
|
||||
from fluksio.core.db import engine
|
||||
from fluksio.models import Flavor
|
||||
|
||||
with Session(engine) as session:
|
||||
row = session.get(Flavor, wanted.flavor)
|
||||
if row is None:
|
||||
raise UnknownFlavor(
|
||||
f"flavor '{wanted.flavor}' does not exist — `fluksio flavors` lists them"
|
||||
)
|
||||
# Built fresh rather than copied, so the flavor-and-numbers check runs on
|
||||
# the result and this cannot quietly produce something invalid.
|
||||
return Resources(
|
||||
cpus=row.cpus,
|
||||
gpus=row.gpus,
|
||||
ram=row.ram,
|
||||
env=wanted.env,
|
||||
duration_s=wanted.duration_s,
|
||||
)
|
||||
|
||||
|
||||
def fair_share_env(cpus: int, workers: int) -> dict[str, str]:
|
||||
"""Thread caps for the shared pool, where nothing declared anything.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user