Stage caching for batch runs, and an engine that lives in the command
Docs / docs (push) Successful in 19s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m5s
Playwright Tests / test-playwright (2, 2) (push) Failing after 20s
pre-commit / pre-commit (push) Failing after 2m33s
Test Backend / test-backend (push) Successful in 2m7s
Compose Smoke Test / test-compose (push) Failing after 20s
Playwright Tests / merge-reports (push) Failing after 1m3s
Publish / publish (push) Failing after 12s
Docs / docs (push) Successful in 19s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m5s
Playwright Tests / test-playwright (2, 2) (push) Failing after 20s
pre-commit / pre-commit (push) Failing after 2m33s
Test Backend / test-backend (push) Successful in 2m7s
Compose Smoke Test / test-compose (push) Failing after 20s
Playwright Tests / merge-reports (push) Failing after 1m3s
Publish / publish (push) Failing after 12s
A code node in a batch run is now fingerprinted by its source, its raw settings and the values it reads — an artifact input counting as its digest, which is what the content addressing was always for. A run that finds the key restores what the earlier one returned and skips the node, recorded as `cached`. The run history is the cache: `run_node.outputs` beside the `cache_key` the schema already had, no second store. On for code nodes, never for the built-in and connector types that have side effects; off per node with `@node(cache=False)` and per run with `--no-cache`. Emissions are not replayed on a hit, so a cached training node returns its result without redrawing its curve. Recorded in NOTEPAD.md with the two other deliberate limits. `fluksio run --local` boots the real app in the command's own process and drives it through its ASGI interface behind the ordinary client, so a run no longer needs a `serve` terminal beside it — same data directory, same history, and the cache carries between the two. It always waits, because the engine it starts lives exactly as long as the command. Also: `fluksio sweep --param lr=0.1,0.01` for the product of the lists, `run --follow` for a run's numbers as they arrive, Ctrl-C cancelling a waited run rather than abandoning it, coloured statuses on a terminal, and `name` made optional on the metrics endpoint so a follower can ask for every series. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -137,3 +137,121 @@ def test_run_syncs_by_default_and_can_be_told_not_to() -> None:
|
||||
|
||||
assert _parser().parse_args(["run", "train"]).no_sync is False
|
||||
assert _parser().parse_args(["run", "train", "--no-sync"]).no_sync is True
|
||||
|
||||
|
||||
def test_a_sweep_is_the_product_of_the_parameters_given() -> None:
|
||||
"""`--param lr=0.1,0.01 --param epochs=1,2` is four runs, typed by the flow."""
|
||||
import pytest
|
||||
|
||||
from fluksio.sdk import SyncError
|
||||
from fluksio.sdk.cli import _grid
|
||||
|
||||
definition = {
|
||||
"inputs": [
|
||||
{"spec": {"name": "lr", "dtype": "float"}},
|
||||
{"spec": {"name": "epochs", "dtype": "int"}},
|
||||
]
|
||||
}
|
||||
|
||||
grid = _grid(definition, ["lr=0.1,0.01", "epochs=1,2"], seed=7)
|
||||
assert [entry["params"] for entry in grid] == [
|
||||
{"lr": 0.1, "epochs": 1},
|
||||
{"lr": 0.1, "epochs": 2},
|
||||
{"lr": 0.01, "epochs": 1},
|
||||
{"lr": 0.01, "epochs": 2},
|
||||
]
|
||||
assert all(entry["seed"] == 7 for entry in grid)
|
||||
|
||||
with pytest.raises(SyncError, match="not an input of this flow"):
|
||||
_grid(definition, ["nonesuch=1"], seed=None)
|
||||
with pytest.raises(SyncError, match="name=value"):
|
||||
_grid(definition, ["lr"], seed=None)
|
||||
|
||||
|
||||
def test_the_local_engine_is_asked_for_rather_than_guessed() -> None:
|
||||
from fluksio.cli import _parser
|
||||
|
||||
parser = _parser()
|
||||
assert parser.parse_args(["run", "train"]).local is False
|
||||
assert parser.parse_args(["run", "train", "--local"]).local is True
|
||||
assert parser.parse_args(["runs", "--local"]).local is True
|
||||
assert parser.parse_args(["sweep", "train", "--param", "lr=1"]).local is False
|
||||
|
||||
|
||||
def test_a_local_run_always_waits(monkeypatch) -> None:
|
||||
"""The engine is this process, so a run nobody waits for is thrown away."""
|
||||
from contextlib import contextmanager
|
||||
|
||||
from fluksio.cli import _parser
|
||||
from fluksio.sdk import cli
|
||||
|
||||
submitted: dict[str, object] = {}
|
||||
|
||||
class FakeHandle:
|
||||
id = "run-1"
|
||||
status = "ok"
|
||||
result: dict[str, object] = {}
|
||||
|
||||
def wait(self, timeout: float = 0.0) -> "FakeHandle":
|
||||
submitted["waited"] = True
|
||||
return self
|
||||
|
||||
class FakeClient:
|
||||
def get_flow(self, name: str) -> dict[str, object]:
|
||||
return {"definition": {"inputs": []}}
|
||||
|
||||
def submit(self, flow, params, seed=None, no_cache=False):
|
||||
submitted["flow"] = flow
|
||||
submitted["no_cache"] = no_cache
|
||||
return FakeHandle()
|
||||
|
||||
def run(self, run_id: str) -> dict[str, object]:
|
||||
return {"nodes": [{"status": "cached"}, {"status": "ok"}]}
|
||||
|
||||
@contextmanager
|
||||
def fake_engine():
|
||||
yield FakeClient()
|
||||
|
||||
monkeypatch.setattr(cli, "_engine_client", fake_engine)
|
||||
|
||||
args = _parser().parse_args(["run", "train", "--local", "--no-sync", "--no-cache"])
|
||||
assert cli.cmd_run(args, []) == 0
|
||||
assert submitted == {"flow": "train", "no_cache": True, "waited": True}
|
||||
|
||||
|
||||
def test_ctrl_c_while_waiting_cancels_the_run(monkeypatch) -> None:
|
||||
"""Interrupting means stop the run, not walk away leaving it going."""
|
||||
from contextlib import contextmanager
|
||||
|
||||
from fluksio.cli import _parser
|
||||
from fluksio.sdk import cli
|
||||
|
||||
cancelled: list[str] = []
|
||||
|
||||
class FakeHandle:
|
||||
id = "run-1"
|
||||
status = "running"
|
||||
result: dict[str, object] = {}
|
||||
|
||||
def wait(self, timeout: float = 0.0):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
class FakeClient:
|
||||
def get_flow(self, name: str) -> dict[str, object]:
|
||||
return {"definition": {"inputs": []}}
|
||||
|
||||
def submit(self, flow, params, seed=None, no_cache=False):
|
||||
return FakeHandle()
|
||||
|
||||
def cancel(self, run_id: str) -> None:
|
||||
cancelled.append(run_id)
|
||||
|
||||
@contextmanager
|
||||
def fake_engine():
|
||||
yield FakeClient()
|
||||
|
||||
monkeypatch.setattr(cli, "_engine_client", fake_engine)
|
||||
|
||||
args = _parser().parse_args(["run", "train", "--local", "--no-sync"])
|
||||
assert cli.cmd_run(args, []) == 130
|
||||
assert cancelled == ["run-1"]
|
||||
|
||||
Reference in New Issue
Block a user