Make the docs state things rather than argue them
Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s
Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s
The site read as a design journal: rationale paragraphs, hedges
("deliberately", "on purpose", "genuinely"), meta-commentary about the docs
themselves, and one em-dash every ten lines carrying an aside.
Roughly twenty rationale blocks are gone or reduced to what a reader needs
in order to use the thing. Em-dashes go from 507 to 135, and what is left is
structural rather than prose: list and definition separators, table cells,
and four inside code blocks that quote what the CLI actually prints.
Also: api.example.com becomes api.fluksio.com (the emails stay, since
bootstrap.py really defaults to admin@example.com and RFC 2606 reserves it);
the mqtt table gains the two settings it had drifted behind on and inject's
wording matches the engine; llms.txt lists the two connector pages that were
in the nav but not in it; and the two device/device_policy notes now agree.
Builds clean under `zensical build --strict`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
This commit is contained in:
@@ -43,7 +43,7 @@ A machine that should only *run nodes* for an engine elsewhere installs less:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install fluksio-worker
|
pip install fluksio-worker
|
||||||
fluksio-worker --url wss://api.example.com/api/v1/workers/attach --token "$TOKEN" --labels gpu
|
fluksio-worker --url wss://api.fluksio.com/api/v1/workers/attach --token "$TOKEN" --labels gpu
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|||||||
+12
-12
@@ -11,16 +11,16 @@ build, publish and run flows.
|
|||||||
MCP_ENABLED=true
|
MCP_ENABLED=true
|
||||||
```
|
```
|
||||||
|
|
||||||
It is off by default, because switching it on opens client registration to
|
It is off by default: switching it on opens client registration to whoever can
|
||||||
whoever can reach the host. Turn it on deliberately.
|
reach the host.
|
||||||
|
|
||||||
The endpoint is `https://api.${DOMAIN}/mcp` — or `http://api.localhost/mcp` on
|
The endpoint is `https://api.${DOMAIN}/mcp`, or `http://api.localhost/mcp` on a
|
||||||
a local stack.
|
local stack.
|
||||||
|
|
||||||
## Connecting a client
|
## Connecting a client
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
claude mcp add --transport http fluksio https://api.example.com/mcp
|
claude mcp add --transport http fluksio https://api.fluksio.com/mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
The OAuth flow does the rest: the client registers itself, you approve it in
|
The OAuth flow does the rest: the client registers itself, you approve it in
|
||||||
@@ -30,7 +30,7 @@ the browser, and it exchanges the grant for a token. Nothing to paste.
|
|||||||
|
|
||||||
An agent acts as **the person who approved it**, and only through the same REST
|
An agent acts as **the person who approved it**, and only through the same REST
|
||||||
API the dashboard uses. It cannot do anything you could not do in the browser,
|
API the dashboard uses. It cannot do anything you could not do in the browser,
|
||||||
and it cannot do anything *you* cannot do — an agent approved by a non-superuser
|
and it cannot do anything *you* cannot do. An agent approved by a non-superuser
|
||||||
is not one either.
|
is not one either.
|
||||||
|
|
||||||
Its tokens are signed with a keypair of their own, kept beside the flow store.
|
Its tokens are signed with a keypair of their own, kept beside the flow store.
|
||||||
@@ -53,7 +53,7 @@ Read-only:
|
|||||||
| `get_message_history` | one message's recent numeric values |
|
| `get_message_history` | one message's recent numeric values |
|
||||||
| `get_graph` | every flow as one graph |
|
| `get_graph` | every flow as one graph |
|
||||||
| `list_shared_nodes` | the shared library and its usages |
|
| `list_shared_nodes` | the shared library and its usages |
|
||||||
| `list_secrets` | secret **names** — never values |
|
| `list_secrets` | secret **names**, never values |
|
||||||
| `get_modules` | the packages node code can import |
|
| `get_modules` | the packages node code can import |
|
||||||
| `get_health` | flows, nodes, queue and loop lag |
|
| `get_health` | flows, nodes, queue and loop lag |
|
||||||
| `get_metrics` | executions, errors and timings per minute |
|
| `get_metrics` | executions, errors and timings per minute |
|
||||||
@@ -87,10 +87,10 @@ Operating:
|
|||||||
The tools do not reach into the engine. Each one calls the same REST endpoint
|
The tools do not reach into the engine. Each one calls the same REST endpoint
|
||||||
the dashboard calls, over an in-process transport.
|
the dashboard calls, over an in-process transport.
|
||||||
|
|
||||||
That keeps exactly one description of what a flow is and how it may be changed
|
That keeps exactly one description of what a flow is and how it may be changed:
|
||||||
— the validation, the draft/publish split, the version check that stops two
|
the validation, the draft/publish split, and the version check that stops two
|
||||||
clients overwriting each other. An agent that saved a flow through a private
|
clients overwriting each other. An agent saving a flow through a private back
|
||||||
back door would be an agent that could write one the canvas cannot open.
|
door could write one the canvas cannot open.
|
||||||
|
|
||||||
The caller's token rides along on every hop, so the API sees the agent's own
|
The caller's token rides along on every hop, so the API sees the agent's own
|
||||||
identity rather than a service account. The audit trail on Home names it.
|
identity rather than a service account. The audit trail on Home names it.
|
||||||
@@ -105,7 +105,7 @@ you what the canvas would have told you.
|
|||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
- MCP is not currently reachable through a [portal](../interface/portal.md)
|
- MCP is not currently reachable through a [portal](../interface/portal.md)
|
||||||
tunnel — the proxy forwards `/api/v1/` only. Connect an agent on the same
|
tunnel: the proxy forwards `/api/v1/` only. Connect an agent on the same
|
||||||
network as the instance.
|
network as the instance.
|
||||||
- Secrets are never readable, by an agent or by anyone else. `list_secrets`
|
- Secrets are never readable, by an agent or by anyone else. `list_secrets`
|
||||||
returns names.
|
returns names.
|
||||||
|
|||||||
+13
-14
@@ -1,7 +1,7 @@
|
|||||||
# The HTTP API
|
# The HTTP API
|
||||||
|
|
||||||
Everything the browser does, the API does first. The dashboard is a generated
|
Everything the browser does, the API does first. The dashboard is a generated
|
||||||
client of this schema, not a privileged path into the engine — so anything you
|
client of this schema, not a privileged path into the engine, so anything you
|
||||||
can click, you can script.
|
can click, you can script.
|
||||||
|
|
||||||
Base URL: `https://api.${DOMAIN}/api/v1`, or `http://127.0.0.1:8000/api/v1` for
|
Base URL: `https://api.${DOMAIN}/api/v1`, or `http://127.0.0.1:8000/api/v1` for
|
||||||
@@ -24,7 +24,7 @@ A bearer token, valid for eight days. `POST /login/test-token` checks one.
|
|||||||
Password recovery and reset are `POST /password-recovery/{email}` and
|
Password recovery and reset are `POST /password-recovery/{email}` and
|
||||||
`POST /reset-password/`.
|
`POST /reset-password/`.
|
||||||
|
|
||||||
Agents authenticate differently — see [Agents over MCP](agents.md).
|
Agents authenticate differently; see [Agents over MCP](agents.md).
|
||||||
|
|
||||||
!!! tip "The interactive schema"
|
!!! tip "The interactive schema"
|
||||||
|
|
||||||
@@ -33,9 +33,8 @@ Agents authenticate differently — see [Agents over MCP](agents.md).
|
|||||||
`/api/v1/openapi.json`. That is the authoritative reference; this page is
|
`/api/v1/openapi.json`. That is the authoritative reference; this page is
|
||||||
the tour.
|
the tour.
|
||||||
|
|
||||||
It is closed in production on purpose: the schema enumerates every endpoint
|
It is closed in production: the schema enumerates every endpoint the
|
||||||
the instance serves, including the paths webhook nodes mounted at
|
instance serves, including paths webhook nodes mounted at runtime.
|
||||||
runtime.
|
|
||||||
|
|
||||||
## Flows
|
## Flows
|
||||||
|
|
||||||
@@ -81,8 +80,8 @@ shared sources and which flows use each.
|
|||||||
| `POST` | `/flows/{name}/nodes/{id}/acknowledge` | clear the failure the canvas is marking |
|
| `POST` | `/flows/{name}/nodes/{id}/acknowledge` | clear the failure the canvas is marking |
|
||||||
|
|
||||||
`POST /flows/{name}/run` on a **batch** flow submits a run instead, because
|
`POST /flows/{name}/run` on a **batch** flow submits a run instead, because
|
||||||
that is what running one means — the parameters, the series and the result are
|
that is what running one means: the parameters, the series and the result are
|
||||||
the point, and a call that quietly did something else would be a trap.
|
the point.
|
||||||
|
|
||||||
## Messages
|
## Messages
|
||||||
|
|
||||||
@@ -95,7 +94,7 @@ curl -s $FLUKSIO/messages/house.temperature/history -H "Authorization: Bearer $T
|
|||||||
|
|
||||||
`GET /messages/` is the catalogue: every message any published flow declares,
|
`GET /messages/` is the catalogue: every message any published flow declares,
|
||||||
with its last value. Publishing puts a value into the graph exactly as a
|
with its last value. Publishing puts a value into the graph exactly as a
|
||||||
dashboard control does — which means only a message some flow *declares* can be
|
dashboard control does, which means only a message some flow *declares* can be
|
||||||
published to. Flows own the namespace; everything else is a client of it.
|
published to. Flows own the namespace; everything else is a client of it.
|
||||||
|
|
||||||
## Runs
|
## Runs
|
||||||
@@ -113,8 +112,8 @@ published to. Flows own the namespace; everything else is a client of it.
|
|||||||
| `GET` | `/runs/{id}/metrics?name=&stride=` | one metric's series, in step order; every series of the run without `name` |
|
| `GET` | `/runs/{id}/metrics?name=&stride=` | one metric's series, in step order; every series of the run without `name` |
|
||||||
| `GET` | `/runs/series/compare?ids=a,b,c&metric=&x=` | that metric across several runs. `x` is what to plot against: nothing or `step`, `time` (seconds since each run's own first reading), or another metric's name, joined on the step the two share |
|
| `GET` | `/runs/series/compare?ids=a,b,c&metric=&x=` | that metric across several runs. `x` is what to plot against: nothing or `step`, `time` (seconds since each run's own first reading), or another metric's name, joined on the step the two share |
|
||||||
|
|
||||||
Submitting answers immediately with a `queued` run. Wrong parameters — an
|
Submitting answers immediately with a `queued` run. Wrong parameters (an
|
||||||
undeclared name, a value of the wrong type — come back as a 422 naming the
|
undeclared name, a value of the wrong type) come back as a 422 naming the
|
||||||
problem, before anything executes.
|
problem, before anything executes.
|
||||||
|
|
||||||
`?digest=` filters by the hash of the parameters, which is how you find "every
|
`?digest=` filters by the hash of the parameters, which is how you find "every
|
||||||
@@ -122,13 +121,13 @@ run that used exactly this configuration".
|
|||||||
|
|
||||||
`?before=` is how a long history is paged: rows come newest first, so handing
|
`?before=` is how a long history is paged: rows come newest first, so handing
|
||||||
back the last row's `created_at` reads the next page whatever landed
|
back the last row's `created_at` reads the next page whatever landed
|
||||||
meanwhile — which `?offset=` cannot, since a run submitted between two pages
|
meanwhile, which `?offset=` cannot, since a run submitted between two pages
|
||||||
shifts every row down one. `?since=` is inclusive and bounds the other end.
|
shifts every row down one. `?since=` is inclusive and bounds the other end.
|
||||||
|
|
||||||
Both exports stream `csv` (the default) or `jsonl`, and take the selection the
|
Both exports stream `csv` (the default) or `jsonl`, and take the selection the
|
||||||
history takes plus `?ids=a,b,c`, `?since=` and `?until=`. `export/runs` puts
|
history takes plus `?ids=a,b,c`, `?since=` and `?until=`. `export/runs` puts
|
||||||
the inputs that *vary* across the selection in `param.` columns — the sweep
|
the inputs that *vary* across the selection in `param.` columns (the sweep
|
||||||
axis — unless `?params=` names them, and the run's numbers in `metric.`
|
axis) unless `?params=` names them, and the run's numbers in `metric.`
|
||||||
columns. Both are dotted paths into whatever a node returned, so
|
columns. Both are dotted paths into whatever a node returned, so
|
||||||
`?metrics=final_metrics.train_loss` selects a field of a record and the
|
`?metrics=final_metrics.train_loss` selects a field of a record and the
|
||||||
defaults reach every number inside one. The run id is on every row of both, which is what makes an exported
|
defaults reach every number inside one. The run id is on every row of both, which is what makes an exported
|
||||||
@@ -239,4 +238,4 @@ stored version moved past the one you were editing. Re-read, merge, save again.
|
|||||||
|
|
||||||
The frontend's TypeScript client is generated from the OpenAPI schema
|
The frontend's TypeScript client is generated from the OpenAPI schema
|
||||||
(`make generate-client`). Any OpenAPI generator will do the same for your
|
(`make generate-client`). Any OpenAPI generator will do the same for your
|
||||||
language — point it at `/api/v1/openapi.json` on a non-production instance.
|
language: point it at `/api/v1/openapi.json` on a non-production instance.
|
||||||
|
|||||||
+62
-65
@@ -8,12 +8,12 @@ Installs the engine and the `fluksio` command. Python 3.12 or newer, Linux or
|
|||||||
macOS.
|
macOS.
|
||||||
|
|
||||||
The MQTT and InfluxDB connectors, outbound mail and error reporting are
|
The MQTT and InfluxDB connectors, outbound mail and error reporting are
|
||||||
`pip install 'fluksio[server]'` — a deployment talking to devices wants them,
|
`pip install 'fluksio[server]'`: a deployment talking to devices wants them,
|
||||||
and a laptop waiting on them to install does not. Everything else, the engine
|
and a laptop waiting on them to install does not. Everything else, the engine
|
||||||
and every python node included, is in the plain install; a node type whose
|
and every python node included, is in the plain install; a node type whose
|
||||||
library is missing says which extra to add when one is actually built.
|
library is missing says which extra to add when one is actually built.
|
||||||
|
|
||||||
There is a second, smaller distribution — `fluksio-worker` — for a machine that
|
There is a second, smaller distribution, `fluksio-worker`, for a machine that
|
||||||
should only *run nodes* for an engine elsewhere. It has none of the engine in
|
should only *run nodes* for an engine elsewhere. It has none of the engine in
|
||||||
it. See [Remote workers](workers.md).
|
it. See [Remote workers](workers.md).
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ instance, while `login`, `sync`, `run`, `runs`, `artifacts`, `sweep` and
|
|||||||
`.fluksio` beside your code, found the way `.git` is: from the working
|
`.fluksio` beside your code, found the way `.git` is: from the working
|
||||||
directory, or any directory above it. Two repositories on one machine are
|
directory, or any directory above it. Two repositories on one machine are
|
||||||
therefore two engines, with their own flows, runs and token. `fluksio serve`
|
therefore two engines, with their own flows, runs and token. `fluksio serve`
|
||||||
makes one where there is none, and it ignores itself from within — a
|
makes one where there is none, and it ignores itself from within with a
|
||||||
`.gitignore` of `*`, so a database and a credential cannot be committed by
|
`.gitignore` of `*`, so a database and a credential cannot be committed by
|
||||||
accident.
|
accident.
|
||||||
|
|
||||||
@@ -47,17 +47,17 @@ Docker.
|
|||||||
|
|
||||||
At a terminal this opens a dashboard with the engine running under it; see
|
At a terminal this opens a dashboard with the engine running under it; see
|
||||||
[below](#the-dashboard). `--plain` prints the log stream instead, which is
|
[below](#the-dashboard). `--plain` prints the log stream instead, which is
|
||||||
also what happens with no terminal — in a container, under systemd, or in CI.
|
also what happens with no terminal: in a container, under systemd, or in CI.
|
||||||
|
|
||||||
The default port moves out of the way when something already has it — 8001,
|
The default port moves out of the way when something already has it (8001,
|
||||||
8002, and so on — and says which one it took; the URL written to
|
8002, and so on) and says which one it took; the URL written to
|
||||||
`client.json` is the one it is actually on. A port you *asked* for is never
|
`client.json` is the one it is actually on. A port you *asked* for is never
|
||||||
moved off: `--port 9000` on a taken 9000 fails, because something else is
|
moved off: `--port 9000` on a taken 9000 fails, because something else is
|
||||||
there and you named it.
|
there and you named it.
|
||||||
|
|
||||||
What it will *not* do is start a second engine for the same instance. If
|
What it will *not* do is start a second engine for the same instance. If
|
||||||
the port is held by an engine already serving this directory, it says so and
|
the port is held by an engine already serving this directory, it says so and
|
||||||
stops — one SQLite database wants one engine. Another instance's Fluksio
|
stops, since one SQLite database wants one engine. Another instance's Fluksio
|
||||||
on that port is named, and the move happens as usual.
|
on that port is named, and the move happens as usual.
|
||||||
|
|
||||||
| Option | Default | What it does |
|
| Option | Default | What it does |
|
||||||
@@ -76,8 +76,8 @@ on that port is named, and the move happens as usual.
|
|||||||
| `--max-workers N` | 4 | python worker processes (`FLOW_MAX_WORKERS`) |
|
| `--max-workers N` | 4 | python worker processes (`FLOW_MAX_WORKERS`) |
|
||||||
| `--gpus N` | 0 | GPUs on this machine a node may be given (`FLOW_GPUS`) |
|
| `--gpus N` | 0 | GPUs on this machine a node may be given (`FLOW_GPUS`) |
|
||||||
|
|
||||||
Cards are declared rather than detected — asking a vendor's tooling would make
|
Cards are declared rather than detected, since asking a vendor's tooling would
|
||||||
one dependency two — so a machine with a GPU reports none until `--gpus` says
|
make one dependency two, so a machine with a GPU reports none until `--gpus` says
|
||||||
otherwise, and a node asking for one is clamped to zero and runs alongside
|
otherwise, and a node asking for one is clamped to zero and runs alongside
|
||||||
every other. `--gpus 1` is what serialises them.
|
every other. `--gpus 1` is what serialises them.
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ restart. It is skipped if the instance is already enrolled.
|
|||||||
|
|
||||||
!!! warning "One process"
|
!!! warning "One process"
|
||||||
|
|
||||||
`fluksio serve` holds the flow engine. A second one is a *second engine* —
|
`fluksio serve` holds the flow engine. A second one is a *second engine*:
|
||||||
duplicated subscriptions, duplicated cron ticks, two webhooks answering the
|
duplicated subscriptions, duplicated cron ticks, two webhooks answering the
|
||||||
same path. Run one, and distribute work with
|
same path. Run one, and distribute work with
|
||||||
[workers](workers.md) instead.
|
[workers](workers.md) instead.
|
||||||
@@ -95,7 +95,7 @@ restart. It is skipped if the instance is already enrolled.
|
|||||||
!!! note "`$HOME` on a cluster"
|
!!! note "`$HOME` on a cluster"
|
||||||
|
|
||||||
A login node's home directory is often NFS, where SQLite's write-ahead log
|
A login node's home directory is often NFS, where SQLite's write-ahead log
|
||||||
does not work — the database would be locked or corrupt. `fluksio serve`
|
does not work, since the database would be locked or corrupt. `fluksio serve`
|
||||||
warns when it notices; point `--data-dir` at local disk.
|
warns when it notices; point `--data-dir` at local disk.
|
||||||
|
|
||||||
### What it prints
|
### What it prints
|
||||||
@@ -116,8 +116,8 @@ dashboard is served from there rather than here.
|
|||||||
### The dashboard
|
### The dashboard
|
||||||
|
|
||||||
At a terminal, `serve` draws the health overview, the recent runs, and the
|
At a terminal, `serve` draws the health overview, the recent runs, and the
|
||||||
engine's own log in a pane below — the output above is in there, not replaced
|
engine's own log in a pane below, so the output above is in there rather than
|
||||||
by it.
|
replaced by it.
|
||||||
|
|
||||||
| Key | What it does |
|
| Key | What it does |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -131,7 +131,7 @@ by it.
|
|||||||
| `e` | pair with a portal, without leaving the screen |
|
| `e` | pair with a portal, without leaving the screen |
|
||||||
|
|
||||||
The engine is a child process rather than a thread, which is what makes those
|
The engine is a child process rather than a thread, which is what makes those
|
||||||
possible — and what makes `q` a way out of the screen rather than a way to
|
possible, and what makes `q` a way out of the screen rather than a way to
|
||||||
stop the engine. Running `fluksio serve` again reattaches to it.
|
stop the engine. Running `fluksio serve` again reattaches to it.
|
||||||
|
|
||||||
An engine started elsewhere is adopted rather than duplicated, and can be
|
An engine started elsewhere is adopted rather than duplicated, and can be
|
||||||
@@ -140,30 +140,27 @@ beside the data and a token this directory's key signed have to agree. Another
|
|||||||
instance's engine is named and left alone.
|
instance's engine is named and left alone.
|
||||||
|
|
||||||
The screen subscribes to the engine's event bus over the same websocket a
|
The screen subscribes to the engine's event bus over the same websocket a
|
||||||
browser uses, so a run appears the moment it starts rather than at the next
|
browser uses, so a run appears the moment it starts. It reconnects on its own,
|
||||||
poll — which is what used to make a run that started and finished inside five
|
and an engine stopped from this screen is a normal state rather than an error.
|
||||||
seconds visible only in the history. It reconnects on its own, quietly: an
|
|
||||||
engine stopped from this screen is a normal state, not an error to fill the
|
|
||||||
log pane with.
|
|
||||||
|
|
||||||
#### Comparing runs
|
#### Comparing runs
|
||||||
|
|
||||||
`enter` opens a comparison of the ticked runs: one metric's curve for each,
|
`enter` opens a comparison of the ticked runs: one metric's curve for each,
|
||||||
over a table of what differs between them.
|
over a table of what differs between them.
|
||||||
|
|
||||||
The curves are drawn in braille, five to a chart, in five distinct colours —
|
The curves are drawn in braille, five to a chart, in five distinct colours: a
|
||||||
a deliberate departure from the browser's chart palette, where the five series
|
departure from the browser's chart palette, where the five series
|
||||||
step one hue by lightness. Hue is what survives a terminal. Every curve is
|
step one hue by lightness. Hue is what survives a terminal. Every curve is
|
||||||
named in the legend either way.
|
named in the legend either way.
|
||||||
|
|
||||||
Two pickers sit above the chart. The first is the metric, named as the run
|
Two pickers sit above the chart. The first is the metric, named as the run
|
||||||
records it — `train.loss` rather than `loss`, since a name is qualified by the
|
records it (`train.loss` rather than `loss`, since a name is qualified by the
|
||||||
node that published it. The second is what it is plotted against: the step,
|
node that published it). The second is what it is plotted against: the step,
|
||||||
`time (s)` measured from each run's own first reading so runs started hours
|
`time (s)` measured from each run's own first reading so runs started hours
|
||||||
apart lie on top of each other, or another metric of the same runs.
|
apart lie on top of each other, or another metric of the same runs.
|
||||||
|
|
||||||
The table below keeps only what actually differs — the inputs whose values are
|
The table below keeps only what actually differs (the inputs whose values are
|
||||||
not the same in every run, and the seed when it varies — beside each run's
|
not the same in every run, and the seed when it varies) beside each run's
|
||||||
status, duration and final numbers. A parameter every run shared is not a
|
status, duration and final numbers. A parameter every run shared is not a
|
||||||
column worth scanning.
|
column worth scanning.
|
||||||
|
|
||||||
@@ -186,7 +183,7 @@ fluksio enroll ABCD-1234
|
|||||||
|
|
||||||
Get the code from the portal under **Instances → Add instance**. It is
|
Get the code from the portal under **Instances → Add instance**. It is
|
||||||
single-use and expires in fifteen minutes. `--as` matters when the instance
|
single-use and expires in fifteen minutes. `--as` matters when the instance
|
||||||
has several superusers — without it, enrolment refuses rather than guessing.
|
has several superusers. Without it, enrolment refuses rather than guessing.
|
||||||
|
|
||||||
Afterwards, `fluksio serve` dials the portal as it comes up, and keeps dialling:
|
Afterwards, `fluksio serve` dials the portal as it comes up, and keeps dialling:
|
||||||
a portal that restarts, a wifi that changes, a laptop that suspends and wakes
|
a portal that restarts, a wifi that changes, a laptop that suspends and wakes
|
||||||
@@ -198,11 +195,11 @@ minute. See [Accounts and the portal](../interface/portal.md).
|
|||||||
## `fluksio worker`
|
## `fluksio worker`
|
||||||
|
|
||||||
Runs nodes for an engine elsewhere. Everything after `worker` belongs to the
|
Runs nodes for an engine elsewhere. Everything after `worker` belongs to the
|
||||||
agent's own parser — it is the same program `fluksio-worker` installs, so the
|
agent's own parser: it is the same program `fluksio-worker` installs, so the
|
||||||
two are interchangeable:
|
two are interchangeable:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fluksio worker --url wss://api.example.com/api/v1/workers/attach \
|
fluksio worker --url wss://api.fluksio.com/api/v1/workers/attach \
|
||||||
--token "$FLUKSIO_WORKER_TOKEN" --labels gpu
|
--token "$FLUKSIO_WORKER_TOKEN" --labels gpu
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -211,21 +208,21 @@ See [Remote workers](workers.md).
|
|||||||
## Talking to an engine
|
## Talking to an engine
|
||||||
|
|
||||||
The commands below are the client half: they run wherever you work, and
|
The commands below are the client half: they run wherever you work, and
|
||||||
address an engine over its API rather than being one — except under `--local`,
|
address an engine over its API rather than being one, except under `--local`,
|
||||||
which boots one inside the command instead.
|
which boots one inside the command instead.
|
||||||
|
|
||||||
### `fluksio login`
|
### `fluksio login`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fluksio login --url https://api.example.com
|
fluksio login --url https://api.fluksio.com
|
||||||
```
|
```
|
||||||
|
|
||||||
For an engine somewhere *else*. One you started yourself needs no login:
|
For an engine somewhere *else*. One you started yourself needs no login:
|
||||||
`fluksio serve` writes the token as it comes up and says where it put it.
|
`fluksio serve` writes the token as it comes up and says where it put it.
|
||||||
|
|
||||||
The token goes in this project's `.fluksio/client.json`, or with `--global` in
|
The token goes in this project's `.fluksio/client.json`, or with `--global` in
|
||||||
`~/.fluksio/client.json`. Every command below reads it from there — nearest
|
`~/.fluksio/client.json`. Every command below reads it from there, nearest
|
||||||
first, walking up from the working directory — or from `FLUKSIO_URL` and
|
first walking up from the working directory, or from `FLUKSIO_URL` and
|
||||||
`FLUKSIO_TOKEN`, or from its own `--url` and `--token`. A token an older
|
`FLUKSIO_TOKEN`, or from its own `--url` and `--token`. A token an older
|
||||||
version wrote to `~/.config/fluksio/client.json` is still read.
|
version wrote to `~/.config/fluksio/client.json` is still read.
|
||||||
|
|
||||||
@@ -240,14 +237,14 @@ each one with a generated import shim per node. A directory that is a package
|
|||||||
is walked; a dotted name is imported as it stands; nothing is loaded from a
|
is walked; a dotted name is imported as it stands; nothing is loaded from a
|
||||||
file path, because the shim has to import the same way.
|
file path, because the shim has to import the same way.
|
||||||
|
|
||||||
A plain directory is walked all the way down, so one folder per study —
|
A plain directory is walked all the way down, so one folder per study
|
||||||
`fluksio sync dev` over `dev/s1_baseline/study.py` — needs no naming. Hidden
|
(`fluksio sync dev` over `dev/s1_baseline/study.py`) needs no naming. Hidden
|
||||||
directories, `__pycache__`, `node_modules` and virtualenvs are left alone.
|
directories, `__pycache__`, `node_modules` and virtualenvs are left alone.
|
||||||
|
|
||||||
Each file is imported under the name its path spells beneath the directory
|
Each file is imported under the name its path spells beneath the directory
|
||||||
being synced, so `dev/s1/study.py` and `dev/s2/study.py` are `s1.study` and
|
being synced, so `dev/s1/study.py` and `dev/s2/study.py` are `s1.study` and
|
||||||
`s2.study` and a `study.py` per study collides with nothing. No `__init__.py`
|
`s2.study` and a `study.py` per study collides with nothing. No `__init__.py`
|
||||||
is needed — the directories in between are namespace packages — which leaves a
|
is needed, since the directories in between are namespace packages, which leaves a
|
||||||
bare `from study import ...` in a test beside it working. A file at the top of
|
bare `from study import ...` in a test beside it working. A file at the top of
|
||||||
what is synced keeps its plain name.
|
what is synced keeps its plain name.
|
||||||
|
|
||||||
@@ -258,18 +255,18 @@ what is synced keeps its plain name.
|
|||||||
| `--force` | overwrite a flow, or a node body, that was edited on the canvas |
|
| `--force` | overwrite a flow, or a node body, that was edited on the canvas |
|
||||||
|
|
||||||
Every sync retires the engine's workers, including one that had nothing to
|
Every sync retires the engine's workers, including one that had nothing to
|
||||||
upload — a worker holds your package in memory, so an edit to it is invisible
|
upload: a worker holds your package in memory, so an edit to it is invisible
|
||||||
until the process goes.
|
until the process goes.
|
||||||
|
|
||||||
It also records, per node, which of your modules that node's function imports
|
It also records, per node, which of your modules that node's function imports
|
||||||
its way to, and what they hash to. That is what the
|
its way to, and what they hash to. That is what the
|
||||||
[stage cache](../concepts/runs.md#stage-caching) keys on, so editing a
|
[stage cache](../concepts/runs.md#stage-caching) keys on, so editing a
|
||||||
helper a node calls into is reported as that node changing — `train: updated
|
helper a node calls into is reported as that node changing (`train: updated
|
||||||
(flow, fit)` — and re-runs it, while editing something the node never reaches
|
(flow, fit)`) and re-runs it, while editing something the node never reaches
|
||||||
is left alone.
|
is left alone.
|
||||||
|
|
||||||
A sync that changed nothing says `unchanged`, which is the answer worth
|
A sync that changed nothing says `unchanged`, which is the answer worth
|
||||||
having. `— published` and `— draft` are said only when there was something to
|
having. `published` and `draft` are said only when there was something to
|
||||||
publish or a draft was genuinely left behind. An engine too old to store what
|
publish or a draft was genuinely left behind. An engine too old to store what
|
||||||
a node's code reaches says so in a line naming both versions; until it is
|
a node's code reaches says so in a line naming both versions; until it is
|
||||||
upgraded its cache is keyed on the whole repository, as it was before. See
|
upgraded its cache is keyed on the whole repository, as it was before. See
|
||||||
@@ -281,7 +278,7 @@ upgraded its cache is keyed on the whole repository, as it was before. See
|
|||||||
fluksio run train --lr 0.05 --seed 7 [--wait]
|
fluksio run train --lr 0.05 --seed 7 [--wait]
|
||||||
```
|
```
|
||||||
|
|
||||||
Syncs the working directory *and everything under it*, then submits a run — so
|
Syncs the working directory *and everything under it*, then submits a run, so
|
||||||
the command after an edit is this one and nothing else, from the repository
|
the command after an edit is this one and nothing else, from the repository
|
||||||
root as readily as from the study's own folder. A study that will not import
|
root as readily as from the study's own folder. A study that will not import
|
||||||
is a warning rather than a stopped run; the upload is already a no-op for a
|
is a warning rather than a stopped run; the upload is already a no-op for a
|
||||||
@@ -290,7 +287,7 @@ flow nothing changed in, so what the walk costs is importing the others.
|
|||||||
is not free, and `--no-sync` skips it entirely.
|
is not free, and `--no-sync` skips it entirely.
|
||||||
|
|
||||||
Flags that are not its own are the flow's inputs, typed by what the flow
|
Flags that are not its own are the flow's inputs, typed by what the flow
|
||||||
declares them as — so a name the flow has not got is refused by name, and
|
declares them as, so a name the flow has not got is refused by name, and
|
||||||
`--param lr=0.002` is told that one value is `--lr 0.002` and several is a
|
`--param lr=0.002` is told that one value is `--lr 0.002` and several is a
|
||||||
[sweep](#fluksio-sweep). `--wait` blocks until the run
|
[sweep](#fluksio-sweep). `--wait` blocks until the run
|
||||||
finishes and exits non-zero if it failed. `--follow` waits as well, and prints
|
finishes and exits non-zero if it failed. `--follow` waits as well, and prints
|
||||||
@@ -309,13 +306,13 @@ timeout.
|
|||||||
on the run, so what a result came from is answerable later. It goes into the
|
on the run, so what a result came from is answerable later. It goes into the
|
||||||
digest that identifies a run's inputs, so two runs of one configuration with
|
digest that identifies a run's inputs, so two runs of one configuration with
|
||||||
different seeds are different runs rather than a cache hit. And if the flow
|
different seeds are different runs rather than a cache hit. And if the flow
|
||||||
declares an input named `seed`, that is what fills it — so the number the run
|
declares an input named `seed`, that is what fills it, so the number the run
|
||||||
is labelled with is the number your code actually drew from, instead of merely
|
is labelled with is the number your code actually drew from, instead of merely
|
||||||
looking like it. A flow that declares no such input still records it, and
|
looking like it. A flow that declares no such input still records it, and
|
||||||
nothing reads it. Sweep over seeds with `--param seed=1,2,3`.
|
nothing reads it. Sweep over seeds with `--param seed=1,2,3`.
|
||||||
|
|
||||||
Any input takes what a previous run produced, named rather than typed out —
|
Any input takes what a previous run produced, named rather than typed out: a
|
||||||
a checkpoint, but equally a config object nobody wants to paste into a shell:
|
checkpoint, but equally a config object nobody wants to paste into a shell:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fluksio run evaluate --dataset @run:1758042000123-9f2ab41c.dataset
|
fluksio run evaluate --dataset @run:1758042000123-9f2ab41c.dataset
|
||||||
@@ -327,7 +324,7 @@ fluksio run train --meta @run:1758042000123-9f2ab41c.dataset_meta
|
|||||||
type; a bare digest is the content itself, resolved into a reference. Both
|
type; a bare digest is the content itself, resolved into a reference. Both
|
||||||
spellings are reserved on every input, `str` included, so an input that has to
|
spellings are reserved on every input, `str` included, so an input that has to
|
||||||
carry one of them literally cannot. Passing the value as JSON still works and
|
carry one of them literally cannot. Passing the value as JSON still works and
|
||||||
is what a script that already holds one does — the same thing
|
is what a script that already holds one does, the same thing
|
||||||
`flow.submit(dataset=run.result["dataset"])` does from Python.
|
`flow.submit(dataset=run.result["dataset"])` does from Python.
|
||||||
|
|
||||||
Run a flow with no parameters at a terminal and it asks for them, one line per
|
Run a flow with no parameters at a terminal and it asks for them, one line per
|
||||||
@@ -346,15 +343,15 @@ the command, skips the questions, and `--defaults` skips them explicitly.
|
|||||||
`--no-sync` runs what is already on the engine. Worth it in a tight loop where
|
`--no-sync` runs what is already on the engine. Worth it in a tight loop where
|
||||||
you know nothing changed, since syncing retires the workers and the next call
|
you know nothing changed, since syncing retires the workers and the next call
|
||||||
pays its imports again. A directory that declares no flows syncs nothing and
|
pays its imports again. A directory that declares no flows syncs nothing and
|
||||||
says nothing — a flow drawn on the canvas is run the same way.
|
says nothing: a flow drawn on the canvas is run the same way.
|
||||||
|
|
||||||
`--no-cache` executes every node, including one an earlier run already
|
`--no-cache` executes every node, including one an earlier run already
|
||||||
answered. See [Stage caching](../concepts/runs.md#stage-caching).
|
answered. See [Stage caching](../concepts/runs.md#stage-caching).
|
||||||
|
|
||||||
`--local` boots the engine inside this process instead of talking to a served
|
`--local` boots the engine inside this process instead of talking to a served
|
||||||
one, so there is no `fluksio serve` terminal to keep open. It is the same
|
one, so there is no `fluksio serve` terminal to keep open. It is the same
|
||||||
instance either way — the same `.fluksio`, the same database, artifacts
|
instance either way (the same `.fluksio`, the same database, artifacts and run
|
||||||
and run history — so a run made this way and a run made through a served
|
history) so a run made this way and a run made through a served
|
||||||
engine cache against each other. It always waits, because the engine it starts
|
engine cache against each other. It always waits, because the engine it starts
|
||||||
lives exactly as long as the command. Starting one costs a few seconds of
|
lives exactly as long as the command. Starting one costs a few seconds of
|
||||||
worker pool and module reconcile, against the ~15 ms of submitting to an
|
worker pool and module reconcile, against the ~15 ms of submitting to an
|
||||||
@@ -378,11 +375,11 @@ engine that accounts for nothing.
|
|||||||
|
|
||||||
The portal reads one of three ways. `no portal` means this instance was
|
The portal reads one of three ways. `no portal` means this instance was
|
||||||
never enrolled. `portal hub.fluksio.com` means the link is up. `portal
|
never enrolled. `portal hub.fluksio.com` means the link is up. `portal
|
||||||
unreachable` names the error, and is the one worth acting on — the dashboard is
|
unreachable` names the error, and is the one worth acting on: the dashboard is
|
||||||
served from the other end, so nobody can reach it while that is showing.
|
served from the other end, so nobody can reach it while that is showing.
|
||||||
|
|
||||||
`--watch` keeps it on screen and refreshes every five seconds until Ctrl-C —
|
`--watch` keeps it on screen and refreshes every five seconds until Ctrl-C, the
|
||||||
the cadence the dashboard polls at, since nothing here moves faster. It needs a
|
cadence the dashboard polls at, since nothing here moves faster. It needs a
|
||||||
terminal; without one, run it without `--watch` and the output pipes cleanly.
|
terminal; without one, run it without `--watch` and the output pipes cleanly.
|
||||||
|
|
||||||
`--local` reads the flows and history out of this directory with no engine
|
`--local` reads the flows and history out of this directory with no engine
|
||||||
@@ -398,10 +395,10 @@ fluksio runs [--flow train] [--limit 20]
|
|||||||
The runs an engine has recorded, newest first: id, status, flow, duration, how
|
The runs an engine has recorded, newest first: id, status, flow, duration, how
|
||||||
long ago it was submitted, the commit of the repository it came from, and the
|
long ago it was submitted, the commit of the repository it came from, and the
|
||||||
inputs it was given. Statuses are coloured when a terminal is reading the
|
inputs it was given. Statuses are coloured when a terminal is reading the
|
||||||
output — `ok` green, `error` red, `cached` cyan.
|
output: `ok` green, `error` red, `cached` cyan.
|
||||||
|
|
||||||
Only the inputs that *differ from what the flow declares* are shown, and they
|
Only the inputs that *differ from what the flow declares* are shown, and they
|
||||||
are clamped to what is left of the terminal's width — a run that took the
|
are clamped to what is left of the terminal's width. A run that took the
|
||||||
defaults lists none at all, and a flow taking a few kB of JSON does not push
|
defaults lists none at all, and a flow taking a few kB of JSON does not push
|
||||||
everything else off the line. `Client.runs()` and
|
everything else off the line. `Client.runs()` and
|
||||||
[`fluksio export runs`](#fluksio-export) are where the whole value is read.
|
[`fluksio export runs`](#fluksio-export) are where the whole value is read.
|
||||||
@@ -414,7 +411,7 @@ to be served.
|
|||||||
fluksio flavors
|
fluksio flavors
|
||||||
```
|
```
|
||||||
|
|
||||||
The named sizes a node can ask for — `@node(resources="gpu-small")` — with the
|
The named sizes a node can ask for, as in `@node(resources="gpu-small")`, with the
|
||||||
cores, memory and cards each stands for. Editing them is the Workers screen or
|
cores, memory and cards each stands for. Editing them is the Workers screen or
|
||||||
`POST /api/v1/flavors`; this is the read.
|
`POST /api/v1/flavors`; this is the read.
|
||||||
|
|
||||||
@@ -424,7 +421,7 @@ cores, memory and cards each stands for. Editing them is the Workers screen or
|
|||||||
fluksio sweep train --param lr=0.1,0.01 --param epochs=10,50 --wait
|
fluksio sweep train --param lr=0.1,0.01 --param epochs=10,50 --wait
|
||||||
```
|
```
|
||||||
|
|
||||||
Every combination of the parameter lists, submitted as one group — four runs
|
Every combination of the parameter lists, submitted as one group: four runs
|
||||||
above, sharing a `group_id` and executing in parallel. Values are typed by the
|
above, sharing a `group_id` and executing in parallel. Values are typed by the
|
||||||
flow's inputs, the same as `run`'s are, and `--seed`, `--no-sync`,
|
flow's inputs, the same as `run`'s are, and `--seed`, `--no-sync`,
|
||||||
`--no-cache` and `--local` mean what they do there. `--wait` blocks until all
|
`--no-cache` and `--local` mean what they do there. `--wait` blocks until all
|
||||||
@@ -438,8 +435,8 @@ fluksio export metrics --flow train --name train.train_loss --stride 10 -o curve
|
|||||||
fluksio export runs --flow train --status ok > arms.csv
|
fluksio export runs --flow train --status ok > arms.csv
|
||||||
```
|
```
|
||||||
|
|
||||||
The two tables an analysis reads. `export metrics` is the long one — a row per
|
The two tables an analysis reads. `export metrics` is the long one, a row per
|
||||||
run, metric and step — which is what a plotting library takes without
|
run, metric and step, which is what a plotting library takes without
|
||||||
reshaping; `--name` keeps the metrics it lists and `--stride` keeps every Nth
|
reshaping; `--name` keeps the metrics it lists and `--stride` keeps every Nth
|
||||||
point of *each* curve. `export runs` is the wide one: a row per run with its
|
point of *each* curve. `export runs` is the wide one: a row per run with its
|
||||||
inputs as columns, its final numbers, its status, its duration and the commit
|
inputs as columns, its final numbers, its status, its duration and the commit
|
||||||
@@ -457,12 +454,12 @@ cell blank.
|
|||||||
A node usually returns a record rather than a scalar, so both sides take
|
A node usually returns a record rather than a scalar, so both sides take
|
||||||
dotted paths into one: `--metrics final_metrics.train_loss,test_metrics.known.perfect`
|
dotted paths into one: `--metrics final_metrics.train_loss,test_metrics.known.perfect`
|
||||||
selects three fields rather than two blobs, and `--params model.ansatz` does
|
selects three fields rather than two blobs, and `--params model.ansatz` does
|
||||||
the same for an input. The defaults reach the same depth — every number a
|
the same for an input. The defaults reach the same depth: every number a
|
||||||
result carries becomes a column wherever it sits, and a record's inputs are
|
result carries becomes a column wherever it sits, and a record's inputs are
|
||||||
taken leaf by leaf rather than as one blob.
|
taken leaf by leaf rather than as one blob.
|
||||||
|
|
||||||
Metric names are flow-qualified — a node of `train` writing `train_loss`
|
Metric names are flow-qualified (a node of `train` writing `train_loss` records
|
||||||
records `train.train_loss` — so `--list` prints the names the selected runs
|
`train.train_loss`) so `--list` prints the names the selected runs
|
||||||
carry when the spelling is not obvious.
|
carry when the spelling is not obvious.
|
||||||
|
|
||||||
Both take `--flow`, `--run ID` (repeat it), `--group`, `--status`, `--since`,
|
Both take `--flow`, `--run ID` (repeat it), `--group`, `--status`, `--since`,
|
||||||
@@ -470,8 +467,8 @@ Both take `--flow`, `--run ID` (repeat it), `--group`, `--status`, `--since`,
|
|||||||
back to the run page and to what the run made.
|
back to the run page and to what the run made.
|
||||||
|
|
||||||
`--format` is `csv` (the default), `jsonl` or `parquet`; output goes to stdout
|
`--format` is `csv` (the default), `jsonl` or `parquet`; output goes to stdout
|
||||||
unless `-o FILE` names somewhere. Parquet keeps the types and needs pyarrow —
|
unless `-o FILE` names somewhere. Parquet keeps the types, needs pyarrow
|
||||||
`pip install 'fluksio[parquet]'` — and a file to write, since it is not a
|
(`pip install 'fluksio[parquet]'`) and needs a file to write, since it is not a
|
||||||
stream. In a notebook, `Client.export_metrics()` and `Client.export_runs()`
|
stream. In a notebook, `Client.export_metrics()` and `Client.export_runs()`
|
||||||
answer the same rows as a list of dicts, which `pandas.DataFrame` takes
|
answer the same rows as a list of dicts, which `pandas.DataFrame` takes
|
||||||
directly.
|
directly.
|
||||||
@@ -483,7 +480,7 @@ fluksio artifacts 1758042000123-9f2ab41c
|
|||||||
fluksio artifacts 1758042000123-9f2ab41c weights -o model.npz
|
fluksio artifacts 1758042000123-9f2ab41c weights -o model.npz
|
||||||
```
|
```
|
||||||
|
|
||||||
The files a run produced — what `fluksio.save_artifact(...)` wrote, and any
|
The files a run produced: what `fluksio.save_artifact(...)` wrote, and any
|
||||||
artifact a node returned. Named alone it lists them: the message each left on,
|
artifact a node returned. Named alone it lists them: the message each left on,
|
||||||
its size, its media type and the filename the node gave it. Name one and it is
|
its size, its media type and the filename the node gave it. Name one and it is
|
||||||
written here, under that filename unless `-o` says otherwise.
|
written here, under that filename unless `-o` says otherwise.
|
||||||
@@ -496,9 +493,9 @@ The message name is the one to pass, since it is what addresses the bytes;
|
|||||||
```text
|
```text
|
||||||
.fluksio/ (or ~/.fluksio, with `--global`)
|
.fluksio/ (or ~/.fluksio, with `--global`)
|
||||||
├── client.json the token `serve` wrote, mode 600
|
├── client.json the token `serve` wrote, mode 600
|
||||||
├── .gitignore `*` — a database and a credential, ignored from within
|
├── .gitignore `*`, so a database and a credential are ignored from within
|
||||||
├── fluksio.db SQLite: users, runs, metrics, observability, agents
|
├── fluksio.db SQLite: users, runs, metrics, observability, agents
|
||||||
├── flows/ a git repository — one directory per flow
|
├── flows/ a git repository, one directory per flow
|
||||||
│ ├── house/
|
│ ├── house/
|
||||||
│ │ ├── flow.json the published structure
|
│ │ ├── flow.json the published structure
|
||||||
│ │ ├── nodes/*.py the published node code
|
│ │ ├── nodes/*.py the published node code
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ uv add --editable ../../fluksio-connector-mydevice
|
|||||||
```
|
```
|
||||||
|
|
||||||
Editing your connector's **code** then takes effect on the next engine restart,
|
Editing your connector's **code** then takes effect on the next engine restart,
|
||||||
with no reinstall. Changing its **metadata** — the entry point, the version,
|
with no reinstall. Changing its **metadata** (the entry point, the version,
|
||||||
the package name — needs `uv sync` to regenerate the installed metadata before
|
the package name) needs `uv sync` to regenerate the installed metadata before
|
||||||
a restart picks it up.
|
a restart picks it up.
|
||||||
|
|
||||||
Two things to know about this:
|
Two things to know about this:
|
||||||
@@ -84,7 +84,7 @@ class MyDevice(ConnectorNode):
|
|||||||
|
|
||||||
Then, on the canvas: place the node, set `poll_interval` to how often the
|
Then, on the canvas: place the node, set `poll_interval` to how often the
|
||||||
device should be read, pick the API key from the secrets store, and add output
|
device should be read, pick the API key from the secrets store, and add output
|
||||||
ports named `temperature` and `humidity`. The port names are the wiring — any
|
ports named `temperature` and `humidity`. The port names are the wiring: any
|
||||||
node consuming those messages is downstream of this one.
|
node consuming those messages is downstream of this one.
|
||||||
|
|
||||||
## If the device is told rather than asked
|
## If the device is told rather than asked
|
||||||
@@ -102,7 +102,7 @@ message drives the device. Set `idempotent = False` on the class: a write is a
|
|||||||
command, and a redelivery after a crash should not undo a newer one.
|
command, and a redelivery after a crash should not undo a newer one.
|
||||||
|
|
||||||
Keep the first version off the wire. A boolean setting the code checks before
|
Keep the first version off the wire. A boolean setting the code checks before
|
||||||
it sends — `artnet`'s `transmit` is the example — lets a flow be built and
|
it sends (`artnet`'s `transmit` is the example) lets a flow be built and
|
||||||
watched in the logs before anything physically moves.
|
watched in the logs before anything physically moves.
|
||||||
|
|
||||||
## If the reading is bytes
|
## If the reading is bytes
|
||||||
@@ -117,8 +117,8 @@ A camera or a microphone publishes a reference rather than the bytes:
|
|||||||
|
|
||||||
Place the node with an `image`-typed output port and a Media widget draws each
|
Place the node with an `image`-typed output port and a Media widget draws each
|
||||||
frame as it lands. `fluksio-connector-test-media` publishes test frames and
|
frame as it lands. `fluksio-connector-test-media` publishes test frames and
|
||||||
tones this way, so the whole path can be wired up with no camera in the room —
|
tones this way, so the whole path can be wired up with no camera in the room.
|
||||||
copy it if yours is a media device.
|
Copy it if yours is a media device.
|
||||||
|
|
||||||
## Try it without a device
|
## Try it without a device
|
||||||
|
|
||||||
|
|||||||
+30
-31
@@ -1,7 +1,7 @@
|
|||||||
# Writing node code
|
# Writing node code
|
||||||
|
|
||||||
A Function node is a Python file. That is all it is — no base class, no
|
A Function node is a Python file: no base class, no decorator, no framework
|
||||||
decorator, no framework import unless you want one.
|
import unless you want one.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def process(temperature, setpoint=21.0):
|
def process(temperature, setpoint=21.0):
|
||||||
@@ -22,18 +22,18 @@ See [Where a node's values come from](../concepts/values.md).
|
|||||||
|
|
||||||
**The return value is a dict keyed by output ports.** Every value is checked
|
**The return value is a dict keyed by output ports.** Every value is checked
|
||||||
against the port's declared type before it is published. A key that is not a
|
against the port's declared type before it is published. A key that is not a
|
||||||
declared port is an error, not a silent drop — nothing leaves a node except
|
declared port is an error, not a silent drop. Nothing leaves a node except
|
||||||
through a port it declared.
|
through a port it declared.
|
||||||
|
|
||||||
**Nothing else is importable from the engine.** Node code runs in a separate
|
**Nothing else is importable from the engine.** Node code runs in a separate
|
||||||
process, on a separate interpreter, with none of Fluksio's own modules on its
|
process, on a separate interpreter, with none of Fluksio's own modules on its
|
||||||
path. What it can import is what the [Modules](../interface/operations.md)
|
path. What it can import is what the [Modules](../interface/operations.md)
|
||||||
screen installed — which can include [your own project](#your-own-code-as-a-package),
|
screen installed, which can include [your own project](#your-own-code-as-a-package),
|
||||||
so a node need not be a self-contained file.
|
so a node need not be a self-contained file.
|
||||||
|
|
||||||
**A node is a pure function of its inputs.** No context object, no global
|
**A node is a pure function of its inputs.** No context object, no global
|
||||||
store, no handle to reach for. A running total or a debounce timer has a
|
store, no handle to reach for. A running total or a debounce timer has a
|
||||||
specific shape — see [Keeping state in a flow](../concepts/state.md).
|
specific shape; see [Keeping state in a flow](../concepts/state.md).
|
||||||
|
|
||||||
## Producing values over time
|
## Producing values over time
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ def process(lr, steps):
|
|||||||
return {"final_loss": loss}
|
return {"final_loss": loss}
|
||||||
```
|
```
|
||||||
|
|
||||||
Whatever the generator `return`s at the end is the node's result — what
|
Whatever the generator `return`s at the end is the node's result, which is what
|
||||||
downstream nodes read. If you never `return`, the last thing you yield is the
|
downstream nodes read. If you never `return`, the last thing you yield is the
|
||||||
result instead.
|
result instead.
|
||||||
|
|
||||||
@@ -60,15 +60,15 @@ Mark the port so the flow says what it does:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Two consequences. In a [run](../concepts/runs.md), the whole series is kept as
|
Two consequences. In a [run](../concepts/runs.md), the whole series is kept as
|
||||||
that run's metrics — this is why there is no `log_metric()` anywhere in the
|
that run's metrics, which is why there is no `log_metric()` anywhere in the
|
||||||
API. And **the node's timeout starts measuring silence rather than duration**:
|
API. And **the node's timeout starts measuring silence rather than duration**:
|
||||||
each emission resets the deadline, so a node yielding every few seconds can run
|
each emission resets the deadline, so a node yielding every few seconds can run
|
||||||
for hours under a timeout of 300.
|
for hours under a timeout of 300.
|
||||||
|
|
||||||
### `fluksio.emit`
|
### `fluksio.emit`
|
||||||
|
|
||||||
Where a `yield` cannot reach — the value comes from inside somebody else's
|
Where a `yield` cannot reach, because the value comes from inside somebody
|
||||||
callback, and they call you rather than the other way round:
|
else's callback and they call you rather than the other way round:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import fluksio
|
import fluksio
|
||||||
@@ -103,19 +103,19 @@ def process(dataset):
|
|||||||
```
|
```
|
||||||
|
|
||||||
`save_artifact` takes bytes or a path, stores them by their SHA-256 digest, and
|
`save_artifact` takes bytes or a path, stores them by their SHA-256 digest, and
|
||||||
returns a small reference — digest, size, media type, name — which is what an
|
returns a small reference (digest, size, media type, name) which is what an
|
||||||
`artifact`-typed port carries.
|
`artifact`-typed port carries.
|
||||||
|
|
||||||
Because the address is the content's hash, a sweep whose fifty configs share
|
Because the address is the content's hash, a sweep whose fifty configs share
|
||||||
one preprocessed input stores it once, and a reference stays valid wherever the
|
one preprocessed input stores it once, and a reference stays valid wherever the
|
||||||
store is reachable from — including on another machine.
|
store is reachable from, including on another machine.
|
||||||
|
|
||||||
`fluksio artifacts RUN NAME` is how one comes back out at a terminal.
|
`fluksio artifacts RUN NAME` is how one comes back out at a terminal.
|
||||||
|
|
||||||
### `fluksio.logger`
|
### `fluksio.logger`
|
||||||
|
|
||||||
A node's `print` is kept as that node's logs, and so is anything on
|
A node's `print` is kept as that node's logs, and so is anything on
|
||||||
`fluksio.logger` — the same logger the SDK exports at top level, so code that
|
`fluksio.logger` is the same logger the SDK exports at top level, so code that
|
||||||
runs both inside a node and outside one says it the same way:
|
runs both inside a node and outside one says it the same way:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -152,7 +152,7 @@ each carries and what rates are realistic.
|
|||||||
|
|
||||||
Only what a node *returns* is recorded against its run. Frames yielded
|
Only what a node *returns* is recorded against its run. Frames yielded
|
||||||
along the way are replaced in state by the next one, and the artifact sweep
|
along the way are replaced in state by the next one, and the artifact sweep
|
||||||
removes bytes nothing refers to any more — which is what stops a camera
|
removes bytes nothing refers to any more, which is what stops a camera
|
||||||
filling the disk. If a particular frame matters, return it.
|
filling the disk. If a particular frame matters, return it.
|
||||||
|
|
||||||
## Printing
|
## Printing
|
||||||
@@ -161,14 +161,13 @@ each carries and what rates are realistic.
|
|||||||
the flow editor's log panel and on the run's per-node record; the rest is
|
the flow editor's log panel and on the run's per-node record; the rest is
|
||||||
dropped, so a node printing in a loop cannot fill anything up.
|
dropped, so a node printing in a loop cannot fill anything up.
|
||||||
|
|
||||||
Use it to debug. Do not use it to record results — a number worth keeping is an
|
Use it to debug, not to record results. A number worth keeping is an output
|
||||||
output port, not a line of text.
|
port, not a line of text.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
An exception fails that node's execution, not the flow. The message you see is
|
An exception fails that node's execution, not the flow. The message you see is
|
||||||
one line from the frame in *your* code, not a stack through the engine — that
|
one line from the frame in *your* code, not a stack through the engine.
|
||||||
is a deliberate choice about what is actionable.
|
|
||||||
|
|
||||||
The node keeps its last error visible after it recovers, so a failure that
|
The node keeps its last error visible after it recovers, so a failure that
|
||||||
fired an alert at 03:00 still says what it was at 09:00. It can also be
|
fired an alert at 03:00 still says what it was at 09:00. It can also be
|
||||||
@@ -178,13 +177,13 @@ acknowledged from the canvas.
|
|||||||
|
|
||||||
`timeout` on a node is how many seconds its code may be *silent* before it is
|
`timeout` on a node is how many seconds its code may be *silent* before it is
|
||||||
stopped. A yield or an `emit` resets the clock, and the first call's imports are
|
stopped. A yield or an `emit` resets the clock, and the first call's imports are
|
||||||
not charged to it — a node importing torch is not being slow, it is loading.
|
not charged to it: a node importing torch is not being slow, it is loading.
|
||||||
|
|
||||||
There is no timeout by default. Training runs for hours and a node that reports
|
There is no timeout by default. Training runs for hours and a node that reports
|
||||||
nothing is usually working, so the engine waits: what fails a call is the worker
|
nothing is usually working, so the engine waits: what fails a call is the worker
|
||||||
dying, which arrives at once rather than after a deadline. Set a timeout on the
|
dying, which arrives at once rather than after a deadline. Set a timeout on the
|
||||||
nodes where silence means stuck — an HTTP call that should answer in seconds,
|
nodes where silence means stuck (an HTTP call that should answer in seconds, a
|
||||||
a loop that can spin — either per node or, for all of them,
|
loop that can spin) either per node or, for all of them,
|
||||||
with `FLOW_NODE_TIMEOUT`. `timeout = 0` says explicitly that this node has none.
|
with `FLOW_NODE_TIMEOUT`. `timeout = 0` says explicitly that this node has none.
|
||||||
|
|
||||||
## Running a node somewhere else
|
## Running a node somewhere else
|
||||||
@@ -197,7 +196,7 @@ A node declares the label of the machine it needs:
|
|||||||
|
|
||||||
`require` (the default) waits for a worker carrying that label; `prefer` runs
|
`require` (the default) waits for a worker carrying that label; `prefer` runs
|
||||||
locally when none is attached. A node bound to a device is compiled *on that
|
locally when none is attached. A node bound to a device is compiled *on that
|
||||||
machine* — a node importing `torch` is correct on the GPU box and a missing
|
machine*: a node importing `torch` is correct on the GPU box and a missing
|
||||||
module on the engine, so checking it here would fail something that is fine.
|
module on the engine, so checking it here would fail something that is fine.
|
||||||
|
|
||||||
See [Remote workers](workers.md).
|
See [Remote workers](workers.md).
|
||||||
@@ -205,8 +204,8 @@ See [Remote workers](workers.md).
|
|||||||
## Sharing code between flows
|
## Sharing code between flows
|
||||||
|
|
||||||
A node's source can be promoted to the shared library from its panel, and other
|
A node's source can be promoted to the shared library from its panel, and other
|
||||||
flows can then use it by reference. One copy, one place to edit — and every
|
flows can then use it by reference. There is one copy and one place to edit, and
|
||||||
flow using it runs the edit, which is the point and also the caution.
|
every flow using it runs the edit.
|
||||||
|
|
||||||
Shared sources live in `_lib/` in the flow repository, so they are versioned
|
Shared sources live in `_lib/` in the flow repository, so they are versioned
|
||||||
with everything else.
|
with everything else.
|
||||||
@@ -214,7 +213,7 @@ with everything else.
|
|||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
Node code runs in a virtual environment of its own, on the instance's data
|
Node code runs in a virtual environment of its own, on the instance's data
|
||||||
volume — deliberately separate from the one Fluksio itself runs on.
|
volume, separate from the one Fluksio itself runs on.
|
||||||
|
|
||||||
Declare what you import in [Modules](../interface/operations.md), or over the
|
Declare what you import in [Modules](../interface/operations.md), or over the
|
||||||
API:
|
API:
|
||||||
@@ -233,7 +232,7 @@ flows. An install takes effect immediately; nothing restarts.
|
|||||||
!!! tip "If Fluksio is installed in the venv you work in, skip this"
|
!!! tip "If Fluksio is installed in the venv you work in, skip this"
|
||||||
|
|
||||||
Node code then runs on that environment, so your project and everything it
|
Node code then runs on that environment, so your project and everything it
|
||||||
imports are already importable — see
|
imports are already importable; see
|
||||||
[Getting started: data science](../getting-started/data-science.md). What
|
[Getting started: data science](../getting-started/data-science.md). What
|
||||||
follows is for a Fluksio with a venv of its own, which is what a container
|
follows is for a Fluksio with a venv of its own, which is what a container
|
||||||
always has.
|
always has.
|
||||||
@@ -246,7 +245,7 @@ installable like any other dependency:
|
|||||||
numpy>=2
|
numpy>=2
|
||||||
```
|
```
|
||||||
|
|
||||||
A node body then imports it, and the logic stays where it already lives — in
|
A node body then imports it, and the logic stays where it already lives: in
|
||||||
your repository, under your own version control, importing its own siblings:
|
your repository, under your own version control, importing its own siblings:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -263,7 +262,7 @@ modules as it likes, and nothing was copied.
|
|||||||
!!! tip "You can have those three lines written for you"
|
!!! tip "You can have those three lines written for you"
|
||||||
|
|
||||||
Decorate `fit` with `@node(...)` where it is defined, say which nodes make
|
Decorate `fit` with `@node(...)` where it is defined, say which nodes make
|
||||||
a flow with `Flow(...)`, and `fluksio sync` generates the body above —
|
a flow with `Flow(...)`, and `fluksio sync` generates the body above,
|
||||||
along with the flow document, so there is nothing to PUT by hand. The
|
along with the flow document, so there is nothing to PUT by hand. The
|
||||||
declaration lives beside the function it describes and is checked against
|
declaration lives beside the function it describes and is checked against
|
||||||
its signature. See
|
its signature. See
|
||||||
@@ -271,7 +270,7 @@ modules as it likes, and nothing was copied.
|
|||||||
|
|
||||||
!!! warning "Editable, but not live"
|
!!! warning "Editable, but not live"
|
||||||
|
|
||||||
`-e` means edits reach the venv without reinstalling — but a node's process
|
`-e` means edits reach the venv without reinstalling, but a node's process
|
||||||
already holds the imported module in memory. The engine's workers are
|
already holds the imported module in memory. The engine's workers are
|
||||||
long-lived, so a change to your code is picked up when they are retired,
|
long-lived, so a change to your code is picked up when they are retired,
|
||||||
which is what **Apply** does. Pressing it after an edit is the loop.
|
which is what **Apply** does. Pressing it after an edit is the loop.
|
||||||
@@ -279,20 +278,20 @@ modules as it likes, and nothing was copied.
|
|||||||
A [worker](workers.md) you attach yourself is the exception: it starts a
|
A [worker](workers.md) you attach yourself is the exception: it starts a
|
||||||
process per call, so it reads your code fresh every run. If you are
|
process per call, so it reads your code fresh every run. If you are
|
||||||
iterating on the code many times an hour, point one at your own
|
iterating on the code many times an hour, point one at your own
|
||||||
interpreter — `fluksio-worker --python "$(which python)"` — and mark the
|
interpreter (`fluksio-worker --python "$(which python)"`) and mark the
|
||||||
node with its label.
|
node with its label.
|
||||||
|
|
||||||
!!! note "The path is a deployment detail"
|
!!! note "The path is a deployment detail"
|
||||||
|
|
||||||
It is resolved on whichever machine runs the node, and the manifest is
|
It is resolved on whichever machine runs the node, and the manifest is
|
||||||
committed to the flow repository — so an absolute path from your laptop
|
committed to the flow repository, so an absolute path from your laptop
|
||||||
means nothing inside a container or on a GPU box. Those need their own
|
means nothing inside a container or on a GPU box. Those need their own
|
||||||
install of the same project; a VCS requirement
|
install of the same project; a VCS requirement
|
||||||
(`myresearch @ git+ssh://…@a1b2c3d`) travels where a path does not.
|
(`myresearch @ git+ssh://…@a1b2c3d`) travels where a path does not.
|
||||||
|
|
||||||
## A worked example
|
## A worked example
|
||||||
|
|
||||||
The repository ships a small supervised fit as a seedable demo — three nodes,
|
The repository ships a small supervised fit as a seedable demo: three nodes,
|
||||||
a batch flow, streaming metrics, artifacts between stages, and a GPU-labelled
|
a batch flow, streaming metrics, artifacts between stages, and a GPU-labelled
|
||||||
node that falls back to the engine when no worker is attached. It is the
|
node that falls back to the engine when no worker is attached. It is the
|
||||||
shortest complete thing to read:
|
shortest complete thing to read:
|
||||||
|
|||||||
+28
-30
@@ -6,7 +6,7 @@ network as the other.
|
|||||||
|
|
||||||
A **worker** is a process that runs the code of nodes marked for it. It dials
|
A **worker** is a process that runs the code of nodes marked for it. It dials
|
||||||
*out* to the engine over one authenticated websocket, so nothing on that
|
*out* to the engine over one authenticated websocket, so nothing on that
|
||||||
machine has to be reachable — and nothing has to expose the engine's state
|
machine has to be reachable, and nothing has to expose the engine's state
|
||||||
backend across hosts, which it never should.
|
backend across hosts, which it never should.
|
||||||
|
|
||||||
## Install and attach
|
## Install and attach
|
||||||
@@ -15,13 +15,13 @@ backend across hosts, which it never should.
|
|||||||
pip install fluksio-worker
|
pip install fluksio-worker
|
||||||
|
|
||||||
fluksio-worker \
|
fluksio-worker \
|
||||||
--url wss://api.example.com/api/v1/workers/attach \
|
--url wss://api.fluksio.com/api/v1/workers/attach \
|
||||||
--token "$FLUKSIO_WORKER_TOKEN" \
|
--token "$FLUKSIO_WORKER_TOKEN" \
|
||||||
--labels gpu,cuda12 \
|
--labels gpu,cuda12 \
|
||||||
--python /opt/torch-venv/bin/python
|
--python /opt/torch-venv/bin/python
|
||||||
```
|
```
|
||||||
|
|
||||||
`fluksio-worker` is its own distribution — the agent, the node runner, and
|
`fluksio-worker` is its own distribution: the agent, the node runner, and
|
||||||
`websockets`. Nothing of the engine, so a GPU box does not install a database
|
`websockets`. Nothing of the engine, so a GPU box does not install a database
|
||||||
driver in order to run a training step. An engine host already has it, and
|
driver in order to run a training step. An engine host already has it, and
|
||||||
`fluksio worker …` is the same program.
|
`fluksio worker …` is the same program.
|
||||||
@@ -40,13 +40,13 @@ driver in order to run a training step. An engine host already has it, and
|
|||||||
| `--ram-mb` | what the job or the machine has | memory to advertise, in MB |
|
| `--ram-mb` | what the job or the machine has | memory to advertise, in MB |
|
||||||
| `--max-idle` | never | stop after this many seconds with nothing running |
|
| `--max-idle` | never | stop after this many seconds with nothing running |
|
||||||
|
|
||||||
`--python` is the important one. It is how this machine keeps its own wheels —
|
`--python` is the important one. It is how this machine keeps its own wheels
|
||||||
the CUDA build, the vendor SDK, the thing that will not install anywhere else —
|
(the CUDA build, the vendor SDK, the thing that will not install anywhere else)
|
||||||
without the engine ever installing them or knowing about them.
|
without the engine ever installing them or knowing about them.
|
||||||
|
|
||||||
### What it says it has
|
### What it says it has
|
||||||
|
|
||||||
A worker reports its inventory when it attaches — cores, GPUs and memory — and
|
A worker reports its inventory when it attaches (cores, GPUs and memory) and
|
||||||
the engine schedules against it: a node asking for two cores and a GPU goes to
|
the engine schedules against it: a node asking for two cores and a GPU goes to
|
||||||
a machine that has them free, not merely to one carrying the right label.
|
a machine that has them free, not merely to one carrying the right label.
|
||||||
|
|
||||||
@@ -58,12 +58,12 @@ or `FLUKSIO_WORKER_GPUS`) or something you say with `--gpus`. A worker that
|
|||||||
reports nothing still attaches and is scheduled by its label alone, as every
|
reports nothing still attaches and is scheduled by its label alone, as every
|
||||||
worker was before any of them reported anything.
|
worker was before any of them reported anything.
|
||||||
|
|
||||||
The engine tells each call what it may use — thread caps, and the devices it
|
The engine tells each call what it may use: thread caps, and the devices it
|
||||||
may see. The worker starts a process per call, so it applies them at the only
|
may see. The worker starts a process per call, so it applies them at the only
|
||||||
moment a numerical library still reads them: before the import.
|
moment a numerical library still reads them: before the import.
|
||||||
|
|
||||||
`--max-idle` is for a worker something else started for one job — a batch
|
`--max-idle` is for a worker something else started for one job, a batch
|
||||||
scheduler, say. It exits when nothing has run for that long, so the allocation
|
scheduler say. It exits when nothing has run for that long, so the allocation
|
||||||
goes back rather than idling until its walltime.
|
goes back rather than idling until its walltime.
|
||||||
|
|
||||||
## Mint the token
|
## Mint the token
|
||||||
@@ -75,15 +75,15 @@ curl -X POST $FLUKSIO/workers/tokens -H "Authorization: Bearer $TOKEN" \
|
|||||||
-H 'Content-Type: application/json' -d '{"name": "gpu-dev"}'
|
-H 'Content-Type: application/json' -d '{"name": "gpu-dev"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Shown once, valid for a year — a worker is a machine somebody sets up and
|
Shown once, valid for a year, since a worker is a machine somebody sets up and
|
||||||
leaves running. It is signed with the same keypair agent tokens use, so
|
leaves running. It is signed with the same keypair agent tokens use, so
|
||||||
rotating that key revokes every worker along with them.
|
rotating that key revokes every worker along with them.
|
||||||
|
|
||||||
??? note "A host where pip is not an option"
|
??? note "A host where pip is not an option"
|
||||||
|
|
||||||
The two files work copied into one directory and run with `python agent.py
|
The two files work copied into one directory and run with `python agent.py
|
||||||
…`. The engine serves the runner itself at `GET /api/v1/workers/runtime` —
|
…`. The engine serves the runner itself at `GET /api/v1/workers/runtime`.
|
||||||
it is the same module its own local workers run, deliberately standard
|
It is the same module its own local workers run, deliberately standard
|
||||||
library only.
|
library only.
|
||||||
|
|
||||||
## Send a node to it
|
## Send a node to it
|
||||||
@@ -107,10 +107,10 @@ A node declares the label of the machine it needs:
|
|||||||
`prefer` is what makes a flow work before the GPU box exists. `require` is what
|
`prefer` is what makes a flow work before the GPU box exists. `require` is what
|
||||||
you want once it does.
|
you want once it does.
|
||||||
|
|
||||||
!!! note "Set from the API"
|
!!! note "Not in the panel yet"
|
||||||
|
|
||||||
`device` and `device_policy` are not yet fields in the node panel. Set them
|
`device` and `device_policy`, which machine a node's code runs on, are set
|
||||||
with `PUT /flows/{name}`.
|
through the API rather than the panel, with `PUT /flows/{name}`.
|
||||||
|
|
||||||
## What follows from this
|
## What follows from this
|
||||||
|
|
||||||
@@ -120,15 +120,15 @@ you want once it does.
|
|||||||
`torch` is correct on the GPU box and a missing module on the engine, so
|
`torch` is correct on the GPU box and a missing module on the engine, so
|
||||||
checking it here would fail something that is fine.
|
checking it here would fail something that is fine.
|
||||||
- **`import fluksio` inside a node is the worker's own reporter.** `emit`,
|
- **`import fluksio` inside a node is the worker's own reporter.** `emit`,
|
||||||
`save_artifact`, `load_artifact` — installed before your code runs, so an
|
`save_artifact`, `load_artifact`, installed before your code runs, so an
|
||||||
installed `fluksio` package on that box never shadows it.
|
installed `fluksio` package on that box never shadows it.
|
||||||
- **Cancelling a run kills what it is executing**, there or here, and leaves
|
- **Cancelling a run kills what it is executing**, there or here, and leaves
|
||||||
other runs of the same node alone.
|
other runs of the same node alone.
|
||||||
- **If the worker disappears mid-call**, the run fails in seconds with
|
- **If the worker disappears mid-call**, the run fails in seconds with
|
||||||
`worker went away mid-call` rather than waiting out its timeout.
|
`worker went away mid-call` rather than waiting out its timeout.
|
||||||
- **A worker sends a heartbeat every ten seconds while it executes**, so a long
|
- **A worker sends a heartbeat every ten seconds while it executes**, so a long
|
||||||
node is distinguishable from a dead socket. Ninety seconds of nothing at all —
|
node is distinguishable from a dead socket. Ninety seconds of nothing at all,
|
||||||
not even a heartbeat — fails the call as gone. A heartbeat says the *agent* is
|
not even a heartbeat, fails the call as gone. A heartbeat says the *agent* is
|
||||||
alive and nothing about the node, so it never satisfies a node's own timeout:
|
alive and nothing about the node, so it never satisfies a node's own timeout:
|
||||||
one set to thirty seconds fires after thirty seconds of the node reporting
|
one set to thirty seconds fires after thirty seconds of the node reporting
|
||||||
nothing, wherever it runs.
|
nothing, wherever it runs.
|
||||||
@@ -154,8 +154,8 @@ environment, and what it says it has: cores, GPUs and memory.
|
|||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
The engine and the worker speak a version-matched protocol, and a worker
|
The engine and the worker speak a version-matched protocol, and a worker
|
||||||
announcing anything else is refused rather than half-understood. Protocol 2 —
|
announcing anything else is refused rather than half-understood. Protocol 2,
|
||||||
the one that carries inventory — is `fluksio-worker` 0.2.0. An older agent is
|
the one that carries inventory, is `fluksio-worker` 0.2.0. An older agent is
|
||||||
told so on the socket and stops, rather than retrying against an engine that
|
told so on the socket and stops, rather than retrying against an engine that
|
||||||
will never accept it; `pip install -U fluksio-worker` on that host is the whole
|
will never accept it; `pip install -U fluksio-worker` on that host is the whole
|
||||||
upgrade. Nothing changed in the runner served at `GET /api/v1/workers/runtime`,
|
upgrade. Nothing changed in the runner served at `GET /api/v1/workers/runtime`,
|
||||||
@@ -163,7 +163,7 @@ so a host that copies its two files copies the same one as before.
|
|||||||
|
|
||||||
## Machines from a batch scheduler
|
## Machines from a batch scheduler
|
||||||
|
|
||||||
A cluster is not a machine that attaches and stays — it is a queue somebody else
|
A cluster is not a machine that attaches and stays; it is a queue somebody else
|
||||||
owns. So Fluksio does not submit *nodes* to Slurm. It submits a job whose payload
|
owns. So Fluksio does not submit *nodes* to Slurm. It submits a job whose payload
|
||||||
is an ordinary worker dialling back in, and from there everything works the way
|
is an ordinary worker dialling back in, and from there everything works the way
|
||||||
it already does: the same protocol, the same artifacts, the same cancellation.
|
it already does: the same protocol, the same artifacts, the same cancellation.
|
||||||
@@ -176,7 +176,7 @@ Write the clusters into `provisioners.json` beside the flows:
|
|||||||
"name": "hpc",
|
"name": "hpc",
|
||||||
"login": "me@login.cluster",
|
"login": "me@login.cluster",
|
||||||
"ssh_key": "/secrets/hpc_ed25519",
|
"ssh_key": "/secrets/hpc_ed25519",
|
||||||
"engine_url": "wss://api.example.com/api/v1/workers/attach",
|
"engine_url": "wss://api.fluksio.com/api/v1/workers/attach",
|
||||||
"max_idle_s": 300,
|
"max_idle_s": 300,
|
||||||
"provision_timeout_s": 900,
|
"provision_timeout_s": 900,
|
||||||
"profiles": [{
|
"profiles": [{
|
||||||
@@ -192,12 +192,10 @@ Write the clusters into `provisioners.json` beside the flows:
|
|||||||
A **profile** is what the scheduler is asked for, where a flavor is what a node
|
A **profile** is what the scheduler is asked for, where a flavor is what a node
|
||||||
asks for. They are separate on purpose, and agree when you set them up to.
|
asks for. They are separate on purpose, and agree when you set them up to.
|
||||||
|
|
||||||
When a node needs a machine nothing attached can give, and a profile would fit,
|
When a node needs a machine nothing attached can give, and a profile fits, the
|
||||||
the engine `sbatch`es one over ssh — the system `ssh`, so nothing new is
|
engine `sbatch`es one over the system `ssh`, and the run waits meanwhile, saying
|
||||||
installed — and the run waits meanwhile, saying so. `prerun` owns the
|
so. `prerun` owns the environment: a `module load`, or a venv with
|
||||||
environment: a `module load`, a venv with `fluksio-worker` already in it. There
|
`fluksio-worker` already in it. The generated script runs no `pip install`.
|
||||||
is deliberately no `pip install` in the generated script, because what is
|
|
||||||
installed on a cluster is somebody's decision and not this program's.
|
|
||||||
|
|
||||||
One outstanding request per profile, however often it is asked for. A job that
|
One outstanding request per profile, however often it is asked for. A job that
|
||||||
never attaches within `provision_timeout_s` is `scancel`led, as is anything
|
never attaches within `provision_timeout_s` is `scancel`led, as is anything
|
||||||
@@ -219,8 +217,8 @@ queue does, which is what a cluster that queues overnight needs, and
|
|||||||
## What a worker is not
|
## What a worker is not
|
||||||
|
|
||||||
It is not a second engine. Subscriptions, schedules, webhooks, the dashboards
|
It is not a second engine. Subscriptions, schedules, webhooks, the dashboards
|
||||||
and the run queue all stay in one process — that is what keeps a message having
|
and the run queue all stay in one process, which keeps a message having one
|
||||||
one definition and a cron tick happening once. A worker executes node bodies.
|
definition and a cron tick happening once. A worker executes node bodies.
|
||||||
|
|
||||||
Running two engines against one data directory is not supported. Distribute
|
Running two engines against one data directory is not supported. Distribute
|
||||||
work with workers.
|
work with workers.
|
||||||
|
|||||||
+12
-14
@@ -1,7 +1,6 @@
|
|||||||
# Flows, nodes and messages
|
# Flows, nodes and messages
|
||||||
|
|
||||||
Three ideas hold the whole system up. They are worth twenty minutes, because
|
Three ideas hold the system up. Almost everything else follows from them.
|
||||||
almost everything else follows from them.
|
|
||||||
|
|
||||||
## A flow is a graph you did not draw
|
## A flow is a graph you did not draw
|
||||||
|
|
||||||
@@ -22,21 +21,20 @@ def process(temperature, setpoint=21.0):
|
|||||||
`decide` is downstream of `read` because it needs `temperature` and `read`
|
`decide` is downstream of `read` because it needs `temperature` and `read`
|
||||||
produces it. Nobody drew a wire.
|
produces it. Nobody drew a wire.
|
||||||
|
|
||||||
This is the one structural decision everything else rests on, so it is worth
|
The consequences:
|
||||||
being explicit about the consequences:
|
|
||||||
|
|
||||||
- **Fan-in is free.** Two nodes providing `temperature` are two producers of
|
- **Fan-in is free.** Two nodes providing `temperature` are two producers of
|
||||||
one message. The consumer does not change.
|
one message. The consumer does not change.
|
||||||
- **A node runs when something it reads was published.** Not merely when
|
- **A node runs when something it reads was published.** Not merely when
|
||||||
something upstream of it ran: a node that produced nothing this time — held
|
something upstream of it ran: a node that produced nothing this time (held
|
||||||
back by a rate limit, say — leaves what reads it on the value it already
|
back by a rate limit, say) leaves what reads it on the value it already
|
||||||
has, and so does everything behind that.
|
has, and so does everything behind that.
|
||||||
- **A wire cannot be wrong.** There is no wire. There is a name that either
|
- **A wire cannot be wrong.** There is no wire. There is a name that either
|
||||||
matches or does not, and the canvas tells you at edit time which it is.
|
matches or does not, and the canvas tells you at edit time which it is.
|
||||||
- **Layout is not a document.** The canvas computes the arrangement, so a flow
|
- **Layout is not a document.** The canvas computes the arrangement, so a flow
|
||||||
has no stored positions to maintain, merge or fight over.
|
has no stored positions to maintain, merge or fight over.
|
||||||
- **Flows stay small.** A graph nobody can hand-arrange is one worth keeping
|
- **Flows stay small.** A graph nobody can hand-arrange is one worth keeping
|
||||||
small — which is the intent. Several atomic flows that name each other beat
|
small, which is the intent. Several atomic flows that name each other beat
|
||||||
one flow with sixty nodes in it.
|
one flow with sixty nodes in it.
|
||||||
|
|
||||||
### Message names are namespaced
|
### Message names are namespaced
|
||||||
@@ -65,7 +63,7 @@ def process(reading, unit="C"):
|
|||||||
return {"shown": reading if unit == "C" else reading * 1.8 + 32}
|
return {"shown": reading if unit == "C" else reading * 1.8 + 32}
|
||||||
```
|
```
|
||||||
|
|
||||||
`reading` is a port. `unit` is a **setting** — a constant of this node's code,
|
`reading` is a port. `unit` is a **setting**: a constant of this node's code,
|
||||||
typed into its panel and stored with the flow. Both arrive as arguments, which
|
typed into its panel and stored with the flow. Both arrive as arguments, which
|
||||||
is why a setting may not share a name with a port. See
|
is why a setting may not share a name with a port. See
|
||||||
[Where a node's values come from](values.md).
|
[Where a node's values come from](values.md).
|
||||||
@@ -86,7 +84,7 @@ Types are not decoration. They are what lets the dashboard editor offer you
|
|||||||
only the messages a gauge can actually draw, and what lets the canvas refuse a
|
only the messages a gauge can actually draw, and what lets the canvas refuse a
|
||||||
binding before anything runs. See [Payload types](../reference/payload-types.md).
|
binding before anything runs. See [Payload types](../reference/payload-types.md).
|
||||||
|
|
||||||
Everything on the wire is JSON. Bytes — a checkpoint, an image, a model —
|
Everything on the wire is JSON. Bytes (a checkpoint, an image, a model)
|
||||||
travel as an `artifact`: the bytes go to a content-addressed store and the
|
travel as an `artifact`: the bytes go to a content-addressed store and the
|
||||||
message carries a small reference to them. The media types are that same
|
message carries a small reference to them. The media types are that same
|
||||||
reference, saying what kind of bytes are behind it.
|
reference, saying what kind of bytes are behind it.
|
||||||
@@ -98,7 +96,7 @@ once at the end: a training loss, a progress fraction, a frame from a camera, a
|
|||||||
second of speech. A node publishes on one by being a generator and yielding, or
|
second of speech. A node publishes on one by being a generator and yielding, or
|
||||||
by calling `fluksio.emit`.
|
by calling `fluksio.emit`.
|
||||||
|
|
||||||
Each value is delivered to the nodes reading it, in the order it was produced —
|
Each value is delivered to the nodes reading it, in the order it was produced,
|
||||||
so a recogniser slower than the microphone in front of it still sees every
|
so a recogniser slower than the microphone in front of it still sees every
|
||||||
chunk rather than only the newest. What is in state remains the latest value,
|
chunk rather than only the newest. What is in state remains the latest value,
|
||||||
which is what everything else reads, and what a run keeps is the whole series.
|
which is what everything else reads, and what a run keeps is the whole series.
|
||||||
@@ -115,7 +113,7 @@ store, no handle to reach for.
|
|||||||
That is deliberate: a node with hidden state cannot run twice in parallel,
|
That is deliberate: a node with hidden state cannot run twice in parallel,
|
||||||
cannot be replayed, and cannot be moved to another machine. Plenty of real
|
cannot be replayed, and cannot be moved to another machine. Plenty of real
|
||||||
automations do need to remember something, and there is a specific way to say
|
automations do need to remember something, and there is a specific way to say
|
||||||
so — see [Keeping state in a flow](state.md).
|
so; see [Keeping state in a flow](state.md).
|
||||||
|
|
||||||
## Two shapes of flow
|
## Two shapes of flow
|
||||||
|
|
||||||
@@ -142,8 +140,8 @@ Every flow has a published version and, while you are working, a draft.
|
|||||||
- **Publishing** promotes the draft. The engine reloads and picks it up.
|
- **Publishing** promotes the draft. The engine reloads and picks it up.
|
||||||
- **Discarding** throws the draft away.
|
- **Discarding** throws the draft away.
|
||||||
|
|
||||||
The store is a git repository — `flow.json` for the structure, `nodes/*.py` for
|
The store is a git repository (`flow.json` for the structure, `nodes/*.py` for
|
||||||
the code — and each save is a commit. So a flow's history is readable with
|
the code) and each save is a commit. So a flow's history is readable with
|
||||||
ordinary git tooling, and copying a flow between instances is copying a
|
ordinary git tooling, and copying a flow between instances is copying a
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
@@ -166,7 +164,7 @@ to:
|
|||||||
| `node_unhealthy` | the node loaded but is not working: a connector that cannot reach its device, or whose last publication failed |
|
| `node_unhealthy` | the node loaded but is not working: a connector that cannot reach its device, or whose last publication failed |
|
||||||
|
|
||||||
A flow with any of these except the advisory one and `node_unhealthy` does not
|
A flow with any of these except the advisory one and `node_unhealthy` does not
|
||||||
run — a node reporting itself down is a live condition, not a build error, so
|
run. A node reporting itself down is a live condition, not a build error, so
|
||||||
the rest of the flow keeps going and the issue clears by itself once the node
|
the rest of the flow keeps going and the issue clears by itself once the node
|
||||||
reports well again. The health summary on Home counts them, so "why is nothing
|
reports well again. The health summary on Home counts them, so "why is nothing
|
||||||
happening?" has an answer that does not involve reading logs.
|
happening?" has an answer that does not involve reading logs.
|
||||||
|
|||||||
+49
-52
@@ -1,6 +1,6 @@
|
|||||||
# Runs: pipelines that finish
|
# Runs: pipelines that finish
|
||||||
|
|
||||||
A flow that automates a house never ends — a value arrives, nodes fire, and it
|
A flow that automates a house never ends: a value arrives, nodes fire, and it
|
||||||
waits for the next one. A research pipeline is the other shape: parameters go
|
waits for the next one. A research pipeline is the other shape: parameters go
|
||||||
in, stages execute in order, and at some point it is *done* and has produced
|
in, stages execute in order, and at some point it is *done* and has produced
|
||||||
something worth keeping. Fluksio calls the second one a **run**, and it is the
|
something worth keeping. Fluksio calls the second one a **run**, and it is the
|
||||||
@@ -8,7 +8,7 @@ same engine either way.
|
|||||||
|
|
||||||
This is what makes Fluksio usable where Kedro, MLflow or ClearML would be: a
|
This is what makes Fluksio usable where Kedro, MLflow or ClearML would be: a
|
||||||
run has parameters that identify it, a result, per-step metrics, artifacts and
|
run has parameters that identify it, a result, per-step metrics, artifacts and
|
||||||
a place in a queryable history — without a second server, and without paying a
|
a place in a queryable history, with no second server and no
|
||||||
project bootstrap on every execution.
|
project bootstrap on every execution.
|
||||||
|
|
||||||
## A batch flow
|
## A batch flow
|
||||||
@@ -28,15 +28,15 @@ Set `mode: "batch"` on the flow and name the messages its result should hold:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
A batch flow is built and validated like any other — it appears on the canvas,
|
A batch flow is built and validated like any other (it appears on the canvas,
|
||||||
its ports are type-checked — but it is never *activated*: no subscriptions, no
|
its ports are type-checked) but it is never *activated*: no subscriptions, no
|
||||||
schedules, no webhooks. It runs when a run asks it to, and not otherwise.
|
schedules, no webhooks. It runs when a run asks it to, and not otherwise.
|
||||||
|
|
||||||
Its `inputs` are its parameters. A run supplies values for them; anything it
|
Its `inputs` are its parameters. A run supplies values for them; anything it
|
||||||
does not supply keeps the declared initial value.
|
does not supply keeps the declared initial value.
|
||||||
|
|
||||||
One thing a batch flow may not do is rate-limit a port (`interval`). A rate
|
One thing a batch flow may not do is rate-limit a port (`interval`). A rate
|
||||||
limit holds a value back for a timer to release, and a run has no timer — the
|
limit holds a value back for a timer to release, and a run has no timer, so the
|
||||||
value would be dropped rather than delayed, so submitting is refused instead.
|
value would be dropped rather than delayed, so submitting is refused instead.
|
||||||
|
|
||||||
## Submitting
|
## Submitting
|
||||||
@@ -51,7 +51,7 @@ The answer is immediate and the run is `queued`; a training run is measured in
|
|||||||
hours, so nothing waits for it. Poll `GET /api/v1/runs/{id}` for its status,
|
hours, so nothing waits for it. Poll `GET /api/v1/runs/{id}` for its status,
|
||||||
result, per-node record and artifacts.
|
result, per-node record and artifacts.
|
||||||
|
|
||||||
Wrong parameters are refused before anything executes — an undeclared name, or
|
Wrong parameters are refused before anything executes: an undeclared name, or
|
||||||
a value of the wrong type, comes back as a 422 naming the problem.
|
a value of the wrong type, comes back as a 422 naming the problem.
|
||||||
|
|
||||||
### Sweeps
|
### Sweeps
|
||||||
@@ -98,20 +98,17 @@ Mark the port it streams on, so the flow says what it does:
|
|||||||
{"name": "loss", "dtype": "float", "stream": true}
|
{"name": "loss", "dtype": "float", "stream": true}
|
||||||
```
|
```
|
||||||
|
|
||||||
Every yield is published the instant it happens — same port, same type check,
|
Every yield is published the instant it happens: same port, same type check,
|
||||||
same place on the canvas as any other value. Whatever the generator `return`s
|
same place on the canvas as any other value. Whatever the generator `return`s
|
||||||
is the node's result, and is what downstream nodes read. If you never
|
is the node's result, and is what downstream nodes read. If you never
|
||||||
`return`, the last thing you yield is the result instead.
|
`return`, the last thing you yield is the result instead.
|
||||||
|
|
||||||
This is the whole reason the framework does not have a logging API. A metric
|
There is no `log_metric()`. A metric leaves through a port, which makes it a
|
||||||
that escapes through `log_metric()` is undeclared: invisible to validation,
|
message: a chart binds to it directly, a downstream node can consume it, and the
|
||||||
absent from the canvas, and stored somewhere the graph knows nothing about.
|
run keeps its series.
|
||||||
A metric that leaves through a port is a message — so a chart binds to it
|
|
||||||
directly, a downstream node can consume it, and the run keeps its series
|
|
||||||
without anyone asking.
|
|
||||||
|
|
||||||
**Where a yield cannot reach** — the value comes from inside somebody else's
|
**Where a yield cannot reach** — the value comes from inside somebody else's
|
||||||
callback, and they call you rather than the other way round — `fluksio.emit`
|
callback, and they call you rather than the other way round. `fluksio.emit`
|
||||||
writes the same ports the same way:
|
writes the same ports the same way:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -129,8 +126,8 @@ def process():
|
|||||||
|
|
||||||
Every number a node emits is kept as the run's series, stepped by the count of
|
Every number a node emits is kept as the run's series, stepped by the count of
|
||||||
emissions on that message. Read one back with
|
emissions on that message. Read one back with
|
||||||
`GET /api/v1/runs/{id}/metrics?name=<flow>.loss` — or leave `name` off for
|
`GET /api/v1/runs/{id}/metrics?name=<flow>.loss`, or leave `name` off for
|
||||||
every series the run kept — or compare runs:
|
every series the run kept, or compare runs:
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /api/v1/runs/series/compare?ids=<a>,<b>,<c>&metric=<flow>.loss
|
GET /api/v1/runs/series/compare?ids=<a>,<b>,<c>&metric=<flow>.loss
|
||||||
@@ -141,15 +138,15 @@ training curves side by side is a widget binding. During a run the values also
|
|||||||
arrive live on the flow socket, so a chart bound to the port fills in as the
|
arrive live on the flow socket, so a chart bound to the port fills in as the
|
||||||
training goes.
|
training goes.
|
||||||
|
|
||||||
A streaming port may set `interval` to thin out what reaches the canvas — the
|
A streaming port may set `interval` to thin out what reaches the canvas. The
|
||||||
run's history still keeps every value, because the interval is asking for the
|
run's history still keeps every value, because the interval is asking for the
|
||||||
display not to be flooded, not for the curve to have holes in it.
|
display not to be flooded, not for the curve to have holes in it.
|
||||||
|
|
||||||
Emitting has a second effect: **a node's timeout measures silence, not
|
Emitting has a second effect: **a node's timeout measures silence, not
|
||||||
duration**. A node that yields every few seconds can run for hours under a
|
duration**. A node that yields every few seconds can run for hours under a
|
||||||
`timeout` of 300; one that says nothing for longer than its timeout is killed.
|
`timeout` of 300; one that says nothing for longer than its timeout is killed.
|
||||||
There is no timeout unless one is set — a training node that reports nothing is
|
There is no timeout unless one is set, since a training node that reports
|
||||||
usually working — so set `timeout` where going quiet means stuck, at how long
|
nothing is usually working. Set `timeout` where going quiet means stuck, at how long
|
||||||
the node may plausibly be.
|
the node may plausibly be.
|
||||||
|
|
||||||
In a *live* flow, an emission also wakes whatever is downstream of it, exactly
|
In a *live* flow, an emission also wakes whatever is downstream of it, exactly
|
||||||
@@ -160,8 +157,8 @@ finished" with nothing to mean.
|
|||||||
## Artifacts
|
## Artifacts
|
||||||
|
|
||||||
Bytes never travel as a message. `save_artifact` writes them to a
|
Bytes never travel as a message. `save_artifact` writes them to a
|
||||||
content-addressed store and returns a small reference — digest, size, media
|
content-addressed store and returns a small reference (digest, size, media
|
||||||
type, name — which is what an `artifact`-typed port carries:
|
type, name) which is what an `artifact`-typed port carries:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def process(weights): # requires: weights, dtype "artifact"
|
def process(weights): # requires: weights, dtype "artifact"
|
||||||
@@ -177,8 +174,8 @@ downloadable at `GET /api/v1/artifacts/{digest}`.
|
|||||||
## Stage caching
|
## Stage caching
|
||||||
|
|
||||||
A run mostly does not redo what an earlier one already did. Before a node
|
A run mostly does not redo what an earlier one already did. Before a node
|
||||||
executes it is fingerprinted — a sha256 over its source, its settings, the ports
|
executes it is fingerprinted with a sha256 over its source, its settings, the
|
||||||
it declares and the values it is about to read — and if some earlier run of that
|
ports it declares and the values it is about to read. If some earlier run of that
|
||||||
same fingerprint finished, what that one returned is restored into this run's
|
same fingerprint finished, what that one returned is restored into this run's
|
||||||
state and the node is skipped. It is recorded with the status `cached` and a
|
state and the node is skipped. It is recorded with the status `cached` and a
|
||||||
duration of zero, and its artifacts are listed on the new run as well, so they
|
duration of zero, and its artifacts are listed on the new run as well, so they
|
||||||
@@ -192,14 +189,14 @@ on a run carries the `cache_key` it was looked up by.
|
|||||||
For a [code-defined flow](../getting-started/data-science.md), "its source" is
|
For a [code-defined flow](../getting-started/data-science.md), "its source" is
|
||||||
the generated shim, which imports the real function and does not change when
|
the generated shim, which imports the real function and does not change when
|
||||||
that function does. So the key carries one thing more: a digest of the project
|
that function does. So the key carries one thing more: a digest of the project
|
||||||
modules that node's function reaches, worked out by `fluksio sync` — which is
|
modules that node's function reaches, worked out by `fluksio sync`, which is
|
||||||
the only side that imports your code and can see what it imports — and read
|
the only side that imports your code and can see what it imports, and read
|
||||||
again from those files when the run starts.
|
again from those files when the run starts.
|
||||||
|
|
||||||
Editing a helper three calls down from the node invalidates it, which is the
|
Editing a helper three calls down from the node invalidates it, which is the
|
||||||
point: the alternative is a re-run answering with the previous code's numbers.
|
point: the alternative is a re-run answering with the previous code's numbers.
|
||||||
Editing something the node does not reach leaves the hit standing, which is
|
Editing something the node does not reach leaves the hit standing, which is
|
||||||
the other half — a notebook two directories away is not a reason to retrain.
|
the other half: a notebook two directories away is not a reason to retrain.
|
||||||
The walk follows imports statically and stops at the standard library, at
|
The walk follows imports statically and stops at the standard library, at
|
||||||
anything installed, and at Fluksio itself; a module imported under a name the
|
anything installed, and at Fluksio itself; a module imported under a name the
|
||||||
code computes is not followed. An engine that cannot see the files keeps what
|
code computes is not followed. An engine that cannot see the files keeps what
|
||||||
@@ -207,20 +204,20 @@ sync recorded instead of nothing, so a worker on another machine no longer
|
|||||||
keys every run the same.
|
keys every run the same.
|
||||||
|
|
||||||
The run history *is* the cache; there is no second store. A node's returned
|
The run history *is* the cache; there is no second store. A node's returned
|
||||||
outputs are kept on its run record as canonical JSON, up to 256000 characters —
|
outputs are kept on its run record as canonical JSON, up to 256000 characters.
|
||||||
a node returning more than that is simply not cacheable that run. An entry
|
A node returning more than that is not cacheable that run. An entry
|
||||||
whose artifact bytes have since left the store is a miss, not an error.
|
whose artifact bytes have since left the store is a miss, not an error.
|
||||||
|
|
||||||
Only `python` nodes are cached, and by default all of them are. A built-in node
|
Only `python` nodes are cached, and by default all of them are. A built-in node
|
||||||
type or a connector node has side effects and no source to fingerprint, so
|
type or a connector node has side effects and no source to fingerprint, so
|
||||||
neither is ever a candidate. Turn it off for one node with
|
neither is ever a candidate. Turn it off for one node with
|
||||||
`@node(..., cache=False)` — the flow document carries it as `cache`, so the
|
`@node(..., cache=False)` (the flow document carries it as `cache`, so the
|
||||||
canvas and the API can change it too — or for one run with
|
canvas and the API can change it too) or for one run with
|
||||||
`fluksio run --no-cache`, `fluksio sweep --no-cache`, or `"no_cache": true` in
|
`fluksio run --no-cache`, `fluksio sweep --no-cache`, or `"no_cache": true` in
|
||||||
the submission body.
|
the submission body.
|
||||||
|
|
||||||
A cached node replays no emissions — those values were the story of an
|
A cached node replays no emissions, since those values were the story of an
|
||||||
execution that is not happening this time — so its series is not rewritten
|
execution that is not happening this time, so its series is not rewritten
|
||||||
either. The run it was restored from is recorded instead, and that is where the
|
either. The run it was restored from is recorded instead, and that is where the
|
||||||
curve is read back from: asking the reusing run for its metrics answers with
|
curve is read back from: asking the reusing run for its metrics answers with
|
||||||
the same points, under its own flow's names. The one way to be left with a
|
the same points, under its own flow's names. The one way to be left with a
|
||||||
@@ -229,7 +226,7 @@ deleting its flow does.
|
|||||||
|
|
||||||
## Objects that cannot be serialized
|
## Objects that cannot be serialized
|
||||||
|
|
||||||
A live model, a `DataLoader`, a JAX-compiled function — these do not cross a
|
A live model, a `DataLoader`, a JAX-compiled function: none of these cross a
|
||||||
node boundary, and no framework flag will make them. There are exactly two
|
node boundary, and no framework flag will make them. There are exactly two
|
||||||
patterns, and they are both deliberate:
|
patterns, and they are both deliberate:
|
||||||
|
|
||||||
@@ -249,7 +246,7 @@ A node that needs a GPU declares the label of the machine that has one:
|
|||||||
```
|
```
|
||||||
|
|
||||||
A worker on that machine dials **out** to the engine, because the engine
|
A worker on that machine dials **out** to the engine, because the engine
|
||||||
generally cannot reach it — different network, no inbound route — and because
|
generally cannot reach it (different network, no inbound route) and because
|
||||||
nothing should expose Redis across hosts. Install it on the box, mint it a
|
nothing should expose Redis across hosts. Install it on the box, mint it a
|
||||||
token, and start it:
|
token, and start it:
|
||||||
|
|
||||||
@@ -259,13 +256,13 @@ curl -X POST $FLUKSIO/workers/tokens -d '{"name": "gpu-dev"}' # once, as an ad
|
|||||||
pip install fluksio-worker
|
pip install fluksio-worker
|
||||||
|
|
||||||
fluksio-worker \
|
fluksio-worker \
|
||||||
--url wss://api.example.com/api/v1/workers/attach \
|
--url wss://api.fluksio.com/api/v1/workers/attach \
|
||||||
--token "$FLUKSIO_WORKER_TOKEN" \
|
--token "$FLUKSIO_WORKER_TOKEN" \
|
||||||
--labels gpu,cuda12 \
|
--labels gpu,cuda12 \
|
||||||
--python /opt/torch-venv/bin/python
|
--python /opt/torch-venv/bin/python
|
||||||
```
|
```
|
||||||
|
|
||||||
`fluksio-worker` is its own distribution — the agent, the node runner, and
|
`fluksio-worker` is its own distribution: the agent, the node runner, and
|
||||||
`websockets`. Nothing of the engine, so a GPU box does not install a database
|
`websockets`. Nothing of the engine, so a GPU box does not install a database
|
||||||
driver to run a training step. Where pip is not an option, the two files still
|
driver to run a training step. Where pip is not an option, the two files still
|
||||||
work copied into one directory and run with `python agent.py …`; the engine
|
work copied into one directory and run with `python agent.py …`; the engine
|
||||||
@@ -278,7 +275,7 @@ travels with every call, so nothing has to be deployed there.
|
|||||||
A few consequences worth knowing:
|
A few consequences worth knowing:
|
||||||
|
|
||||||
* `import fluksio` inside a node is the worker's own reporter — `emit`,
|
* `import fluksio` inside a node is the worker's own reporter — `emit`,
|
||||||
`save_artifact`, `load_artifact` — installed before the node's code runs, so
|
`save_artifact`, `load_artifact`, installed before the node's code runs, so
|
||||||
the installed `fluksio` package (if the box has one) never shadows it.
|
the installed `fluksio` package (if the box has one) never shadows it.
|
||||||
* A node bound to a device is **compiled on that machine**. A node importing
|
* A node bound to a device is **compiled on that machine**. A node importing
|
||||||
`torch` is correct on the GPU box and a missing module on the engine, so
|
`torch` is correct on the GPU box and a missing module on the engine, so
|
||||||
@@ -295,32 +292,32 @@ A few consequences worth knowing:
|
|||||||
## Durability
|
## Durability
|
||||||
|
|
||||||
Submitting journals the run to a Redis stream of its own, separate from the
|
Submitting journals the run to a Redis stream of its own, separate from the
|
||||||
one the automations use — a burst of five hundred sweep runs must not stand
|
one the automations use: a burst of five hundred sweep runs must not stand
|
||||||
between a house and its heating. An engine that is down when a run is
|
between a house and its heating. An engine that is down when a run is
|
||||||
submitted picks it up when it starts.
|
submitted picks it up when it starts.
|
||||||
|
|
||||||
From the moment a run is claimed, its database row is the record and the queue
|
From the moment a run is claimed, its database row is the record and the queue
|
||||||
is finished with it. Redelivering two hours of training because an
|
is finished with it. Redelivering two hours of training because an
|
||||||
acknowledgement was late is not recovery; instead a running run refreshes a
|
acknowledgement was late is not recovery; instead a running run refreshes a
|
||||||
lease, and one whose lease goes stale is marked `abandoned` — which is what a
|
lease, and one whose lease goes stale is marked `abandoned`, which is what a
|
||||||
run whose engine was killed mid-training becomes.
|
run whose engine was killed mid-training becomes.
|
||||||
|
|
||||||
## Looking at what ran
|
## Looking at what ran
|
||||||
|
|
||||||
The **Runs** screen is the experiment log: every run newest-first, filtered by
|
The **Runs** screen is the experiment log: every run newest-first, filtered by
|
||||||
flow, by status, or down to one sweep. A sweep is worth filtering to — the
|
flow, by status, or down to one sweep. A sweep is worth filtering to, since the
|
||||||
table then draws a column per parameter that actually varied, which is what
|
table then draws a column per parameter that actually varied, which is what
|
||||||
makes fifty runs of one flow readable.
|
makes fifty runs of one flow readable.
|
||||||
|
|
||||||
Tick two or more and their curves go side by side — shift-click to take a
|
Tick two or more and their curves go side by side. Shift-click to take a
|
||||||
range, or the header box to take everything on screen. That comparison is the
|
range, or the header box to take everything on screen. That comparison is the
|
||||||
address, so a link to it is a link someone else can open.
|
address, so a link to it is a link someone else can open.
|
||||||
|
|
||||||
The curves are drawn against the step by default. They can also be drawn
|
The curves are drawn against the step by default. They can also be drawn
|
||||||
against elapsed seconds, which answers "which one got there sooner" and is
|
against elapsed seconds, which answers "which one got there sooner" and is
|
||||||
measured from each run's own first reading so that runs started hours apart
|
measured from each run's own first reading so that runs started hours apart
|
||||||
still lie on top of each other; or against another metric of the same runs —
|
still lie on top of each other; or against another metric of the same runs (an
|
||||||
an epoch, or samples seen — joined on the step the two share.
|
epoch, or samples seen) joined on the step the two share.
|
||||||
|
|
||||||
One run in full is params, the per-node record with its logs and traceback,
|
One run in full is params, the per-node record with its logs and traceback,
|
||||||
the artifacts it made, its metrics and its result.
|
the artifacts it made, its metrics and its result.
|
||||||
@@ -328,8 +325,8 @@ the artifacts it made, its metrics and its result.
|
|||||||
### Taking it into a dataframe
|
### Taking it into a dataframe
|
||||||
|
|
||||||
An analysis wants a table rather than a screen, and there are two it usually
|
An analysis wants a table rather than a screen, and there are two it usually
|
||||||
wants. `fluksio export metrics` is the long one — a row per run, metric and
|
wants. `fluksio export metrics` is the long one, a row per run, metric and
|
||||||
step — and `fluksio export runs` is the wide one, a row per run with the
|
step; `fluksio export runs` is the wide one, a row per run with the
|
||||||
inputs that varied as columns beside its final numbers:
|
inputs that varied as columns beside its final numbers:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -343,20 +340,20 @@ arms = pd.DataFrame(client.export_runs(flow="train", status="ok"))
|
|||||||
|
|
||||||
The run id is on every row of both, so a curve joins to the arm it came from
|
The run id is on every row of both, so a curve joins to the arm it came from
|
||||||
and to the run page it was recorded on, and the wide table carries the commit
|
and to the run page it was recorded on, and the wide table carries the commit
|
||||||
and the code digest — an exported file says what produced its numbers.
|
and the code digest, so an exported file says what produced its numbers.
|
||||||
[The CLI](../code/cli.md#fluksio-export) writes the same rows as csv, jsonl or
|
[The CLI](../code/cli.md#fluksio-export) writes the same rows as csv, jsonl or
|
||||||
parquet, which is where an export belongs: in the script beside the analysis.
|
parquet, which is where an export belongs: in the script beside the analysis.
|
||||||
|
|
||||||
A node's numbers usually arrive inside a record, and the wide table follows
|
A node's numbers usually arrive inside a record, and the wide table follows
|
||||||
them in: every number a result carries is a column of its own, named by its
|
them in: every number a result carries is a column of its own, named by its
|
||||||
path — `metric.final_metrics.train_loss` — and `metrics="final_metrics.train_loss"`
|
path (`metric.final_metrics.train_loss`) and `metrics="final_metrics.train_loss"`
|
||||||
selects one. Inputs work the same way, and are compared leaf by leaf, so two
|
selects one. Inputs work the same way, and are compared leaf by leaf, so two
|
||||||
configurations differing in one field give that field as the axis.
|
configurations differing in one field give that field as the axis.
|
||||||
|
|
||||||
### A dashboard, read against runs
|
### A dashboard, read against runs
|
||||||
|
|
||||||
A run records values under the same names a dashboard binds to — a run of
|
A run records values under the same names a dashboard binds to (a run of
|
||||||
`study` writes `study.loss` — so a dashboard is already a way of looking at
|
`study` writes `study.loss`) so a dashboard is already a way of looking at
|
||||||
one. **Open in dashboard** from a run or a comparison opens any dashboard with
|
one. **Open in dashboard** from a run or a comparison opens any dashboard with
|
||||||
`?runs=a,b,c` on it, and the widgets resolve from those runs instead of from
|
`?runs=a,b,c` on it, and the widgets resolve from those runs instead of from
|
||||||
the live engine: each chart draws a line per run, the tiles that show one
|
the live engine: each chart draws a line per run, the tiles that show one
|
||||||
@@ -367,7 +364,7 @@ Nothing about the dashboard is specific to runs. The page built to watch a
|
|||||||
training run happen is the page that shows the finished ones.
|
training run happen is the page that shows the finished ones.
|
||||||
|
|
||||||
If a flow has no dashboard yet, the same menu offers to build one from what
|
If a flow has no dashboard yet, the same menu offers to build one from what
|
||||||
the flow declares — a chart per streaming port, a tile per output:
|
the flow declares: a chart per streaming port, a tile per output:
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /api/v1/dashboards/from-flow/study
|
POST /api/v1/dashboards/from-flow/study
|
||||||
@@ -386,8 +383,8 @@ sweep, or specific runs. It re-reads on its own and whenever a run finishes.
|
|||||||
### When a run draws nothing
|
### When a run draws nothing
|
||||||
|
|
||||||
A node restored from the [stage cache](#stage-caching) has its curve read back
|
A node restored from the [stage cache](#stage-caching) has its curve read back
|
||||||
from the run that recorded it. Delete that run — deleting its flow does — and
|
from the run that recorded it. Delete that run, which deleting its flow does,
|
||||||
the reusing run is left with a result and an empty curve, and the chart says so
|
and the reusing run is left with a result and an empty curve, and the chart says so
|
||||||
rather than looking broken.
|
rather than looking broken.
|
||||||
|
|
||||||
## What this costs, compared
|
## What this costs, compared
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Keeping state in a flow
|
# Keeping state in a flow
|
||||||
|
|
||||||
Logic nodes are pure functions of their inputs. There is no `context`, no
|
Logic nodes are pure functions of their inputs. There is no `context`, no
|
||||||
`global`, no handle to a store — a node is called with the values of the
|
`global`, no handle to a store. A node is called with the values of the
|
||||||
messages it declares and returns the values of the messages it provides.
|
messages it declares and returns the values of the messages it provides.
|
||||||
|
|
||||||
That is deliberate: a node with hidden state cannot be run twice in parallel,
|
That is deliberate: a node with hidden state cannot be run twice in parallel,
|
||||||
@@ -10,7 +10,7 @@ automations need to remember something. This is how.
|
|||||||
|
|
||||||
## State is a message the node both reads and writes
|
## State is a message the node both reads and writes
|
||||||
|
|
||||||
A running total, a debounce timer, a last-seen reading — each is a value that
|
A running total, a debounce timer, a last-seen reading: each is a value that
|
||||||
survives between runs. Give it a message name, declare it as both an input and
|
survives between runs. Give it a message name, declare it as both an input and
|
||||||
an output, and it is state:
|
an output, and it is state:
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ first call is well-formed even if the value is missing.
|
|||||||
|
|
||||||
## Feeding a value back between two nodes
|
## Feeding a value back between two nodes
|
||||||
|
|
||||||
Sometimes the value comes from a different node — a controller reading back what
|
Sometimes the value comes from a different node, such as a controller reading back what
|
||||||
an estimator computed from its own last output. Written plainly that is a cycle,
|
an estimator computed from its own last output. Written plainly that is a cycle,
|
||||||
and the validator rejects it, because a graph where A waits for B and B waits
|
and the validator rejects it, because a graph where A waits for B and B waits
|
||||||
for A can never start.
|
for A can never start.
|
||||||
@@ -51,7 +51,7 @@ Say so, by marking the input non-triggering:
|
|||||||
A non-triggering input:
|
A non-triggering input:
|
||||||
|
|
||||||
- creates no dependency, so it cannot form a cycle;
|
- creates no dependency, so it cannot form a cycle;
|
||||||
- never makes the node wait — if the message has no value yet, the port is
|
- never makes the node wait. If the message has no value yet, the port is
|
||||||
simply left out of the call, and the function's default applies;
|
simply left out of the call, and the function's default applies;
|
||||||
- is read fresh from state whenever the node does run, for whatever reason.
|
- is read fresh from state whenever the node does run, for whatever reason.
|
||||||
|
|
||||||
@@ -60,8 +60,8 @@ input, and marking it non-triggering would mean the node never runs at all.
|
|||||||
|
|
||||||
## What still holds engine-side state
|
## What still holds engine-side state
|
||||||
|
|
||||||
Built-in nodes that are *about* time or change — rate limiting, filter-on-change,
|
Built-in nodes that are *about* time or change (rate limiting, filter-on-change,
|
||||||
delay, cron — keep what they need in the engine's own state backend, under keys
|
delay, cron) keep what they need in the engine's own state backend, under keys
|
||||||
that never appear as messages. That is the engine's business, not a flow's: the
|
that never appear as messages. That is the engine's business, not a flow's: the
|
||||||
line is that node *code* you write never reaches for state, while node *types*
|
line is that node *code* you write never reaches for state, while node *types*
|
||||||
the engine ships may.
|
the engine ships may.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def process(reading, unit="C"):
|
|||||||
return {"shown": reading if unit == "C" else reading * 1.8 + 32}
|
return {"shown": reading if unit == "C" else reading * 1.8 + 32}
|
||||||
```
|
```
|
||||||
|
|
||||||
Because both arrive by name, a setting cannot share a name with a port — the
|
Because both arrive by name, a setting cannot share a name with a port. The
|
||||||
node reports it rather than picking one.
|
node reports it rather than picking one.
|
||||||
|
|
||||||
Settings are part of the flow document, so changing one is an edit that gets
|
Settings are part of the flow document, so changing one is an edit that gets
|
||||||
@@ -41,7 +41,7 @@ consumer visibly downstream of it.
|
|||||||
|
|
||||||
## Flow inputs: what arrives from outside
|
## Flow inputs: what arrives from outside
|
||||||
|
|
||||||
Some messages are not computed by any node in the flow — a dashboard control
|
Some messages are not computed by any node in the flow, such as a dashboard control
|
||||||
writes them, the API publishes them, a batch run passes them in. Declare those
|
writes them, the API publishes them, a batch run passes them in. Declare those
|
||||||
as the flow's **inputs**, with the value they start from:
|
as the flow's **inputs**, with the value they start from:
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ and the canvas says so. With it, the flow starts from 21.0 and whatever writes
|
|||||||
the message afterwards takes over.
|
the message afterwards takes over.
|
||||||
|
|
||||||
The canvas draws each one as a labelled endpoint feeding the nodes that read
|
The canvas draws each one as a labelled endpoint feeding the nodes that read
|
||||||
it, the same way it draws a dashboard tile or another flow — so a value never
|
it, the same way it draws a dashboard tile or another flow, so a value never
|
||||||
appears from nowhere. They are edited in the flow's own panel.
|
appears from nowhere. They are edited in the flow's own panel.
|
||||||
|
|
||||||
A batch flow's inputs are also its run parameters, and its **outputs** name what
|
A batch flow's inputs are also its run parameters, and its **outputs** name what
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ Fluksio 0.1.0 — data in /home/you/my-research/.fluksio
|
|||||||
```
|
```
|
||||||
|
|
||||||
Read the last two lines: you are already signed in. Signing in to your own
|
Read the last two lines: you are already signed in. Signing in to your own
|
||||||
machine is a formality — the password was printed by the same process that
|
machine is a formality, since the password was printed by the same process that
|
||||||
would have checked it — so `serve` writes the token itself and every command
|
would have checked it, so `serve` writes the token itself and every command
|
||||||
below just works. `fluksio login` is for an engine somewhere *else*.
|
below just works. `fluksio login` is for an engine somewhere *else*.
|
||||||
|
|
||||||
**Write that password down** anyway. It is shown once, and it is what the
|
**Write that password down** anyway. It is shown once, and it is what the
|
||||||
@@ -41,7 +41,7 @@ dashboard asks for.
|
|||||||
|
|
||||||
### One instance per project
|
### One instance per project
|
||||||
|
|
||||||
`.fluksio` sits beside your code, and is found the way `.git` is — from the
|
`.fluksio` sits beside your code, and is found the way `.git` is: from the
|
||||||
directory you are standing in, or any directory above it. So two repositories
|
directory you are standing in, or any directory above it. So two repositories
|
||||||
on one machine are two engines: separate flows, separate run history,
|
on one machine are two engines: separate flows, separate run history,
|
||||||
separate token, and no chance of one experiment's graph turning up in the
|
separate token, and no chance of one experiment's graph turning up in the
|
||||||
@@ -59,7 +59,7 @@ other's.
|
|||||||
climate/
|
climate/
|
||||||
```
|
```
|
||||||
|
|
||||||
It holds a database and a credential, so it ignores itself from within — a
|
It holds a database and a credential, so it ignores itself from within with a
|
||||||
`.gitignore` of `*`, the same thing `uv` writes into `.venv`. Nothing to add
|
`.gitignore` of `*`, the same thing `uv` writes into `.venv`. Nothing to add
|
||||||
to your project's own.
|
to your project's own.
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ Give them different ports (`--port`) if you want two running at once.
|
|||||||
|
|
||||||
!!! tip "One engine for the machine instead"
|
!!! tip "One engine for the machine instead"
|
||||||
|
|
||||||
`fluksio serve --global` uses `~/.fluksio` — shared by every directory,
|
`fluksio serve --global` uses `~/.fluksio`, shared by every directory,
|
||||||
which is what you want for a personal server rather than a project. When
|
which is what you want for a personal server rather than a project. When
|
||||||
both exist, the banner says which one you are looking at and how to reach
|
both exist, the banner says which one you are looking at and how to reach
|
||||||
the other. `--data-dir` still names any directory outright, which is worth
|
the other. `--data-dir` still names any directory outright, which is worth
|
||||||
@@ -77,10 +77,9 @@ Give them different ports (`--port`) if you want two running at once.
|
|||||||
|
|
||||||
!!! tip "Keep it running"
|
!!! tip "Keep it running"
|
||||||
|
|
||||||
The engine is meant to be resident. That is the whole reason submitting a
|
The engine is meant to be resident, which is what makes submitting a run
|
||||||
run costs about 15 ms instead of the second a project-bootstrapping
|
cost about 15 ms. Leave it in a `tmux` window, or write a small
|
||||||
orchestrator spends before it does anything. Leave it in a `tmux` window,
|
`systemd --user` unit for it.
|
||||||
or write a small `systemd --user` unit for it.
|
|
||||||
|
|
||||||
## Talking to it over HTTP
|
## Talking to it over HTTP
|
||||||
|
|
||||||
@@ -96,13 +95,13 @@ While you are experimenting, the interactive schema at
|
|||||||
<http://127.0.0.1:8000/docs> is the fastest way to see what is available.
|
<http://127.0.0.1:8000/docs> is the fastest way to see what is available.
|
||||||
|
|
||||||
For an engine on another machine, `fluksio login --url https://…` asks for a
|
For an engine on another machine, `fluksio login --url https://…` asks for a
|
||||||
password and stores the token the same way — in this project's `.fluksio`, or
|
password and stores the token the same way: in this project's `.fluksio`, or
|
||||||
with `--global` in `~/.fluksio`.
|
with `--global` in `~/.fluksio`.
|
||||||
|
|
||||||
## Your packages are already there
|
## Your packages are already there
|
||||||
|
|
||||||
If you installed Fluksio into the environment you work in — the venv that
|
If you installed Fluksio into the environment you work in (the venv that
|
||||||
already has torch or numpy in it — that is the environment your nodes run on.
|
already has torch or numpy in it) that is the environment your nodes run on.
|
||||||
Nothing to declare, nothing to install twice:
|
Nothing to declare, nothing to install twice:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -119,8 +118,8 @@ fluksio serve
|
|||||||
your environment, adopted. Add packages with pip.
|
your environment, adopted. Add packages with pip.
|
||||||
```
|
```
|
||||||
|
|
||||||
That venv is yours. Add a package the way you added the rest — `pip install
|
That venv is yours. Add a package the way you added the rest (`pip install
|
||||||
scikit-learn` — and `fluksio sync` (or a restart) retires the workers so they
|
scikit-learn`) and `fluksio sync` (or a restart) retires the workers so they
|
||||||
pick it up. The Modules screen lists what is installed and stays read-only,
|
pick it up. The Modules screen lists what is installed and stays read-only,
|
||||||
because the alternative would be Fluksio deciding what belongs in an
|
because the alternative would be Fluksio deciding what belongs in an
|
||||||
environment it did not make.
|
environment it did not make.
|
||||||
@@ -130,7 +129,7 @@ environment it did not make.
|
|||||||
The cost of not having two environments: a package the engine depends on
|
The cost of not having two environments: a package the engine depends on
|
||||||
is one you can now upgrade out from under it. In practice this is what
|
is one you can now upgrade out from under it. In practice this is what
|
||||||
everybody does with every other tool in the venv, and the answer when it
|
everybody does with every other tool in the venv, and the answer when it
|
||||||
bites is the same — pin it back, or keep Fluksio somewhere separate with
|
bites is the same: pin it back, or keep Fluksio somewhere separate with
|
||||||
the venv of its own below.
|
the venv of its own below.
|
||||||
|
|
||||||
### A venv of Fluksio's own
|
### A venv of Fluksio's own
|
||||||
@@ -144,7 +143,7 @@ or an environment too precious to let a node's dependency near. Set
|
|||||||
a venv of its own; the Modules screen installs into it.
|
a venv of its own; the Modules screen installs into it.
|
||||||
```
|
```
|
||||||
|
|
||||||
Then the Modules screen is how packages get in — a pip manifest, installed
|
Then the Modules screen is how packages get in: a pip manifest, installed
|
||||||
with `uv pip sync` and versioned alongside your flows, so what a run imported
|
with `uv pip sync` and versioned alongside your flows, so what a run imported
|
||||||
is recorded with what it ran:
|
is recorded with what it ran:
|
||||||
|
|
||||||
@@ -156,7 +155,7 @@ curl -X POST $FLUKSIO/modules/apply -H "Authorization: Bearer $TOKEN" \
|
|||||||
|
|
||||||
Adding a package takes effect immediately; nothing restarts. The Docker image
|
Adding a package takes effect immediately; nothing restarts. The Docker image
|
||||||
sets `NODE_VENV=managed` for itself, because the venv in it holds the app and
|
sets `NODE_VENV=managed` for itself, because the venv in it holds the app and
|
||||||
nothing of yours — so a container is always this case.
|
nothing of yours, so a container is always this case.
|
||||||
|
|
||||||
!!! tip "A GPU box works the same way"
|
!!! tip "A GPU box works the same way"
|
||||||
|
|
||||||
@@ -169,13 +168,13 @@ nothing of yours — so a container is always this case.
|
|||||||
--token "$WORKER_TOKEN" --labels gpu
|
--token "$WORKER_TOKEN" --labels gpu
|
||||||
```
|
```
|
||||||
|
|
||||||
Then give the node `device="gpu"` — see [Remote workers](../code/workers.md).
|
Then give the node `device="gpu"`; see [Remote workers](../code/workers.md).
|
||||||
|
|
||||||
## Say which functions are nodes
|
## Say which functions are nodes
|
||||||
|
|
||||||
A **flow** is a graph of nodes. A **batch flow** is one that runs on demand
|
A **flow** is a graph of nodes. A **batch flow** is one that runs on demand
|
||||||
from parameters to a result, which is what an experiment is. A node is one of
|
from parameters to a result, which is what an experiment is. A node is one of
|
||||||
your own functions — it stays in your repository, imported by its siblings as
|
your own functions. It stays in your repository, imported by its siblings as
|
||||||
it always was.
|
it always was.
|
||||||
|
|
||||||
Say your project looks roughly like this:
|
Say your project looks roughly like this:
|
||||||
@@ -222,13 +221,13 @@ def fit(dataset, lr, epochs=25):
|
|||||||
```
|
```
|
||||||
|
|
||||||
`yield` replaces `print`. Each one publishes on the node's `loss` port the
|
`yield` replaces `print`. Each one publishes on the node's `loss` port the
|
||||||
instant it happens, and the run keeps every value as a series — which is why
|
instant it happens, and the run keeps every value as a series, which is why
|
||||||
Fluksio has no `log_metric()` call. A metric that leaves through a port is a
|
Fluksio has no `log_metric()` call. A metric that leaves through a port is a
|
||||||
message like any other: a chart can bind to it, a downstream node can consume
|
message like any other: a chart can bind to it, a downstream node can consume
|
||||||
it, and it shows up on the canvas. A metric that escapes through a logging
|
it, and it shows up on the canvas. A metric that escapes through a logging
|
||||||
function is invisible to all three.
|
function is invisible to all three.
|
||||||
|
|
||||||
`fluksio.save_artifact` handles the things too big to be messages — a
|
`fluksio.save_artifact` handles the things too big to be messages: a
|
||||||
checkpoint, a dataset, a plot. It stores the bytes by their hash and returns a
|
checkpoint, a dataset, a plot. It stores the bytes by their hash and returns a
|
||||||
small reference. Nothing changes about how you write the file.
|
small reference. Nothing changes about how you write the file.
|
||||||
|
|
||||||
@@ -241,24 +240,24 @@ stops your own code from running.
|
|||||||
|
|
||||||
- **Ports are declared, never inferred.** `requires` names the messages the
|
- **Ports are declared, never inferred.** `requires` names the messages the
|
||||||
node reads; each arrives as the parameter of the same name. A bare string is
|
node reads; each arrives as the parameter of the same name. A bare string is
|
||||||
the shorthand — `"dataset"` takes its type from whoever provides it, so a
|
the shorthand: `"dataset"` takes its type from whoever provides it, so a
|
||||||
message's type is written down exactly once.
|
message's type is written down exactly once.
|
||||||
- **Parameters with a default that are not ports become settings.** `epochs`
|
- **Parameters with a default that are not ports become settings.** `epochs`
|
||||||
above is stored with the flow and tunable on the canvas without touching
|
above is stored with the flow and tunable on the canvas without touching
|
||||||
this file.
|
this file.
|
||||||
- **The declaration is checked against the function.** A port with no matching
|
- **The declaration is checked against the function.** A port with no matching
|
||||||
parameter, or a parameter that is neither port nor setting, is an error when
|
parameter, or a parameter that is neither port nor setting, is an error when
|
||||||
the module is imported — not when the node is first called.
|
the module is imported, not when the node is first called.
|
||||||
- **The decorator hands the function back untouched.** `fit(dataset, lr=0.05)`
|
- **The decorator hands the function back untouched.** `fit(dataset, lr=0.05)`
|
||||||
is an ordinary call, and the module is an ordinary script — so a
|
is an ordinary call, and the module is an ordinary script, so a
|
||||||
`if __name__ == "__main__":` check beside the nodes runs. Syncing one
|
`if __name__ == "__main__":` check beside the nodes runs. Syncing one
|
||||||
defined in a script *run as* `__main__` is what cannot work, since the
|
defined in a script *run as* `__main__` is what cannot work, since the
|
||||||
generated body would have no name to import it by.
|
generated body would have no name to import it by.
|
||||||
|
|
||||||
!!! note "Where a `yield` cannot reach"
|
!!! note "Where a `yield` cannot reach"
|
||||||
|
|
||||||
If the number comes from inside somebody else's callback — Keras, Lightning,
|
If the number comes from inside somebody else's callback (Keras, Lightning,
|
||||||
HuggingFace `Trainer` — `fluksio.emit(loss=...)` writes the same port the
|
HuggingFace `Trainer`) `fluksio.emit(loss=...)` writes the same port the
|
||||||
same way:
|
same way:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -271,7 +270,7 @@ stops your own code from running.
|
|||||||
|
|
||||||
A training node is not like the rest of a flow. NumPy, JAX and PyTorch each
|
A training node is not like the rest of a flow. NumPy, JAX and PyTorch each
|
||||||
size their thread pool to every core they can see, so a few of them at once
|
size their thread pool to every core they can see, so a few of them at once
|
||||||
oversubscribe the machine badly enough to starve the engine itself — the API
|
oversubscribe the machine badly enough to starve the engine itself: the API
|
||||||
stops answering and every client waiting on a run dies with it. On a GPU the
|
stops answering and every client waiting on a run dies with it. On a GPU the
|
||||||
same shape deadlocks: two processes each preallocating most of the card sit at
|
same shape deadlocks: two processes each preallocating most of the card sit at
|
||||||
zero utilisation with nothing failing and nothing to read.
|
zero utilisation with nothing failing and nothing to read.
|
||||||
@@ -289,7 +288,7 @@ def finetune(checkpoint):
|
|||||||
```
|
```
|
||||||
|
|
||||||
Two things follow. The node waits its turn rather than starting alongside more
|
Two things follow. The node waits its turn rather than starting alongside more
|
||||||
of itself than the machine has room for — the same waiting it already does for
|
of itself than the machine has room for, the same waiting it already does for
|
||||||
a worker. And the worker it runs in is *started* with thread limits matching
|
a worker. And the worker it runs in is *started* with thread limits matching
|
||||||
what it was given, because that is the only moment a numerical library reads
|
what it was given, because that is the only moment a numerical library reads
|
||||||
them; a GPU node is told which card is its through `CUDA_VISIBLE_DEVICES`, and
|
them; a GPU node is told which card is its through `CUDA_VISIBLE_DEVICES`, and
|
||||||
@@ -299,8 +298,8 @@ nothing else is given that card while it runs.
|
|||||||
string whose contents depend on the version you have installed, so writing it
|
string whose contents depend on the version you have installed, so writing it
|
||||||
for you would silently replace whatever you had put there.
|
for you would silently replace whatever you had put there.
|
||||||
|
|
||||||
The engine has to be told how many cards it has — `fluksio serve --gpus 1`, or
|
The engine has to be told how many cards it has (`fluksio serve --gpus 1`, or
|
||||||
`FLOW_GPUS` — because detecting them would mean depending on a vendor's
|
`FLOW_GPUS`) because detecting them would mean depending on a vendor's
|
||||||
tooling. Until it is, a node asking for one is quietly given zero and runs
|
tooling. Until it is, a node asking for one is quietly given zero and runs
|
||||||
alongside every other; the log says so the first time it happens.
|
alongside every other; the log says so the first time it happens.
|
||||||
|
|
||||||
@@ -313,9 +312,9 @@ the next GPU run paying for its imports again.
|
|||||||
imports jax without `resources={"gpus": 1}` runs on the shared pool, is never
|
imports jax without `resources={"gpus": 1}` runs on the shared pool, is never
|
||||||
given `CUDA_VISIBLE_DEVICES`, and leaves a warm worker holding whatever it
|
given `CUDA_VISIBLE_DEVICES`, and leaves a warm worker holding whatever it
|
||||||
preallocated. Declaring the card is what makes it a worker the engine knows
|
preallocated. Declaring the card is what makes it a worker the engine knows
|
||||||
to retire — and what stops two such nodes running at once in the first place.
|
to retire, and what stops two such nodes running at once in the first place.
|
||||||
|
|
||||||
Declaring nothing is the default and is right for most nodes — a poll, a
|
Declaring nothing is the default and is right for most nodes: a poll, a
|
||||||
threshold, a message on its way somewhere. Those share the engine's worker pool
|
threshold, a message on its way somewhere. Those share the engine's worker pool
|
||||||
and are given a fair share of `FLOW_CPUS` as a thread cap, which is what stops
|
and are given a fair share of `FLOW_CPUS` as a thread cap, which is what stops
|
||||||
several of them at once from each sizing to the whole box.
|
several of them at once from each sizing to the whole box.
|
||||||
@@ -339,13 +338,13 @@ The flavor is read every time the node is built, so editing it changes what the
|
|||||||
next run gets. `fluksio flavors` lists them; the Workers screen edits them.
|
next run gets. `fluksio flavors` lists them; the Workers screen edits them.
|
||||||
|
|
||||||
`ram` takes `"2G"` and `duration_s` takes `"30m"`, and a flavor already says how
|
`ram` takes `"2G"` and `duration_s` takes `"30m"`, and a flavor already says how
|
||||||
much — pass one or the numbers, not both. `duration_s` is a statement about the
|
much, so pass one or the numbers, not both. `duration_s` is a statement about the
|
||||||
node for whoever is planning around it, not a limit; the limit is `timeout`.
|
node for whoever is planning around it, not a limit; the limit is `timeout`.
|
||||||
|
|
||||||
### Where it runs
|
### Where it runs
|
||||||
|
|
||||||
This is one decision, not two. Of every machine attached — this engine and each
|
This is one decision, not two. Of every machine attached (this engine and each
|
||||||
worker — the node goes to one that can grant what it asked for and has it free.
|
worker) the node goes to one that can grant what it asked for and has it free.
|
||||||
So a node asking for a card finds the box that has one, without naming it:
|
So a node asking for a card finds the box that has one, without naming it:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -354,7 +353,7 @@ So a node asking for a card finds the box that has one, without naming it:
|
|||||||
```
|
```
|
||||||
|
|
||||||
If nothing attached can ever grant the ask, it is cut down to what is here and
|
If nothing attached can ever grant the ask, it is cut down to what is here and
|
||||||
runs anyway — a flow written on a cluster still has to work on a laptop. If
|
runs anyway, since a flow written on a cluster still has to work on a laptop. If
|
||||||
something *could* but is busy, the node waits and says so.
|
something *could* but is busy, the node waits and says so.
|
||||||
|
|
||||||
Ask what is free, and what is waiting for it, at
|
Ask what is free, and what is waiting for it, at
|
||||||
@@ -363,8 +362,8 @@ Ask what is free, and what is waiting for it, at
|
|||||||
!!! note "Accounted, not enforced"
|
!!! note "Accounted, not enforced"
|
||||||
|
|
||||||
Nothing stops a node that ignores its declaration; the numbers are
|
Nothing stops a node that ignores its declaration; the numbers are
|
||||||
bookkeeping plus the environment its libraries read. Real enforcement —
|
bookkeeping plus the environment its libraries read. Real enforcement
|
||||||
cgroups, rlimits — is a next step, not this one.
|
(cgroups, rlimits) is a next step, not this one.
|
||||||
|
|
||||||
## Say which nodes make a flow
|
## Say which nodes make a flow
|
||||||
|
|
||||||
@@ -390,7 +389,7 @@ train = Flow(
|
|||||||
`inputs` are the run's parameters, with the value a run gets when it names
|
`inputs` are the run's parameters, with the value a run gets when it names
|
||||||
none; `outputs` are what a run reports as its result.
|
none; `outputs` are what a run reports as its result.
|
||||||
|
|
||||||
Nodes are connected because one **provides** a message another **requires** —
|
Nodes are connected because one **provides** a message another **requires**,
|
||||||
never because one imported the other. Importing `fit` into a second flow means
|
never because one imported the other. Importing `fit` into a second flow means
|
||||||
"the same code", not "wired to it".
|
"the same code", not "wired to it".
|
||||||
|
|
||||||
@@ -439,7 +438,7 @@ def process(dataset, lr, **settings):
|
|||||||
return (yield from fit(dataset=dataset, lr=lr, **settings))
|
return (yield from fit(dataset=dataset, lr=lr, **settings))
|
||||||
```
|
```
|
||||||
|
|
||||||
So the flow store still holds a complete, runnable, git-versioned definition —
|
So the flow store still holds a complete, runnable, git-versioned definition;
|
||||||
the body simply happens to import rather than duplicate. Your code stays in
|
the body simply happens to import rather than duplicate. Your code stays in
|
||||||
your repository, under your version control.
|
your repository, under your version control.
|
||||||
|
|
||||||
@@ -450,7 +449,7 @@ alongside the store's own. `--dry-run` prints all of this and uploads nothing;
|
|||||||
!!! warning "Sync after you edit"
|
!!! warning "Sync after you edit"
|
||||||
|
|
||||||
The engine's workers are long-lived and hold your imported modules in
|
The engine's workers are long-lived and hold your imported modules in
|
||||||
memory, so an edit to `myresearch` is invisible until they are retired —
|
memory, so an edit to `myresearch` is invisible until they are retired,
|
||||||
which is what every `fluksio sync` does, including one that has nothing to
|
which is what every `fluksio sync` does, including one that has nothing to
|
||||||
upload. If you are editing many times an hour, attach your own interpreter
|
upload. If you are editing many times an hour, attach your own interpreter
|
||||||
as a worker instead: it starts a process per call and therefore reads your
|
as a worker instead: it starts a process per call and therefore reads your
|
||||||
@@ -467,7 +466,7 @@ alongside the store's own. `--dry-run` prints all of this and uploads nothing;
|
|||||||
|
|
||||||
The generated body puts your repository on `sys.path` by absolute path,
|
The generated body puts your repository on `sys.path` by absolute path,
|
||||||
which is right for the machine you synced from and meaningless in a
|
which is right for the machine you synced from and meaningless in a
|
||||||
container. For anything else, install the package where the node runs —
|
container. For anything else, install the package where the node runs:
|
||||||
`-e /home/you/my-research` in the module manifest, or
|
`-e /home/you/my-research` in the module manifest, or
|
||||||
`myresearch @ git+ssh://…@a1b2c3d`, which travels where a path does not.
|
`myresearch @ git+ssh://…@a1b2c3d`, which travels where a path does not.
|
||||||
|
|
||||||
@@ -488,7 +487,7 @@ the GPU box is not switched on. See [Remote workers](../code/workers.md).
|
|||||||
### What the canvas does with a synced flow
|
### What the canvas does with a synced flow
|
||||||
|
|
||||||
Its node bodies are generated, so the editor shows them read-only and says
|
Its node bodies are generated, so the editor shows them read-only and says
|
||||||
where the real code is. Everything else behaves as usual — but ports and
|
where the real code is. Everything else behaves as usual, but ports and
|
||||||
settings changed there are overwritten by the next sync, which is the point of
|
settings changed there are overwritten by the next sync, which is the point of
|
||||||
the repository being the source of truth. A node whose code you edit on the
|
the repository being the source of truth. A node whose code you edit on the
|
||||||
canvas makes the next sync stop and say so rather than discarding your edit;
|
canvas makes the next sync stop and say so rather than discarding your edit;
|
||||||
@@ -501,19 +500,19 @@ fluksio run train --lr 0.003 --seed 7
|
|||||||
fluksio runs --flow train
|
fluksio runs --flow train
|
||||||
```
|
```
|
||||||
|
|
||||||
`run` syncs first, so after an edit the command is just `fluksio run` — there
|
`run` syncs first, so after an edit the command is just `fluksio run`, with no
|
||||||
is no step to forget. `--no-sync` skips it for a tight loop where nothing
|
step to forget. `--no-sync` skips it for a tight loop where nothing
|
||||||
changed, since syncing retires the workers and the next call pays its imports
|
changed, since syncing retires the workers and the next call pays its imports
|
||||||
again.
|
again.
|
||||||
|
|
||||||
`--lr` is typed by the flow's own inputs, so `0.003` arrives as a float. A
|
`--lr` is typed by the flow's own inputs, so `0.003` arrives as a float. A
|
||||||
parameter you did not declare, or one of the wrong type, is refused before
|
parameter you did not declare, or one of the wrong type, is refused before
|
||||||
anything executes. It answers immediately with a queued run — training is
|
anything executes. It answers immediately with a queued run, since training is
|
||||||
measured in hours, so nothing waits for it unless you pass `--wait`, or
|
measured in hours, so nothing waits for it unless you pass `--wait`, or
|
||||||
`--follow`, which waits and prints the numbers as the run reports them.
|
`--follow`, which waits and prints the numbers as the run reports them.
|
||||||
|
|
||||||
No engine has to be up for any of this: `fluksio run --local` boots the
|
No engine has to be up for any of this: `fluksio run --local` boots the
|
||||||
engine inside the command instead, on the same `.fluksio` — the same database,
|
engine inside the command instead, on the same `.fluksio`: the same database,
|
||||||
artifacts and history a served one would use. It costs a few seconds of
|
artifacts and history a served one would use. It costs a few seconds of
|
||||||
startup per invocation against the ~15 ms of submitting to an engine that is
|
startup per invocation against the ~15 ms of submitting to an engine that is
|
||||||
already up, so it is for the run you want now rather than the loop you are
|
already up, so it is for the run you want now rather than the loop you are
|
||||||
@@ -521,10 +520,10 @@ iterating in.
|
|||||||
|
|
||||||
A second run of a flow you did not change mostly does not execute. Each of
|
A second run of a flow you did not change mostly does not execute. Each of
|
||||||
your nodes is fingerprinted by its settings, the ports it declares, the values
|
your nodes is fingerprinted by its settings, the ports it declares, the values
|
||||||
it reads, and a digest of your repository's Python files — so editing anything
|
it reads, and a digest of your repository's Python files, so editing anything
|
||||||
your node calls into invalidates it, not only the decorated function itself.
|
your node calls into invalidates it, not only the decorated function itself.
|
||||||
One an earlier run already answered is restored from that run rather than run
|
One an earlier run already answered is restored from that run rather than run
|
||||||
again — reported as `cached`, so a flow with nothing left to do finishes as
|
again, reported as `cached`, so a flow with nothing left to do finishes as
|
||||||
`(3/3 cached)`. Change `--lr` and only the nodes downstream of it run. The
|
`(3/3 cached)`. Change `--lr` and only the nodes downstream of it run. The
|
||||||
digest is blunt on purpose: an edit anywhere in the repository re-runs
|
digest is blunt on purpose: an edit anywhere in the repository re-runs
|
||||||
everything. `--no-cache` turns caching off for one run, `@node(...,
|
everything. `--no-cache` turns caching off for one run, `@node(...,
|
||||||
@@ -555,7 +554,7 @@ curl -s "$FLUKSIO/runs/<id>/metrics?name=train.loss" -H "Authorization: Bearer $
|
|||||||
|
|
||||||
The run carries its parameters, a digest of them, the seed, its result, how
|
The run carries its parameters, a digest of them, the seed, its result, how
|
||||||
long each node took, what it logged, every artifact it produced, and both
|
long each node took, what it logged, every artifact it produced, and both
|
||||||
commits — the flow store's and your repository's. That is the answer to "what
|
commits: the flow store's and your repository's. That is the answer to "what
|
||||||
was the learning rate on the run that got 94%?". When a run failed,
|
was the learning rate on the run that got 94%?". When a run failed,
|
||||||
`run.failures` is the node that did it, with its traceback and its logs.
|
`run.failures` is the node that did it, with its traceback and its logs.
|
||||||
|
|
||||||
@@ -563,9 +562,9 @@ Beside your commit is `code_digest`, and `fluksio runs` prints the pair as
|
|||||||
`a1b2c3d-dirty+9f0e1a2`. The commit alone cannot identify what ran: your node
|
`a1b2c3d-dirty+9f0e1a2`. The commit alone cannot identify what ran: your node
|
||||||
bodies are imports, so the engine executes whatever is on disk when the worker
|
bodies are imports, so the engine executes whatever is on disk when the worker
|
||||||
starts, and an uncommitted tree stamps `-dirty` for every run it ever produces.
|
starts, and an uncommitted tree stamps `-dirty` for every run it ever produces.
|
||||||
The digest is over the modules this flow's nodes actually reach — not the
|
The digest is over the modules this flow's nodes actually reach, not the
|
||||||
whole repository, so it moves when the code behind a number moves and stays
|
whole repository, so it moves when the code behind a number moves and stays
|
||||||
put when a notebook beside it changes — and it is read at the moment the run
|
put when a notebook beside it changes. It is read at the moment the run
|
||||||
starts, so in a sweep whose runs queue for hours each one records the code
|
starts, so in a sweep whose runs queue for hours each one records the code
|
||||||
that actually executed it rather than the code that was there when you
|
that actually executed it rather than the code that was there when you
|
||||||
submitted. It is what makes the column worth joining an
|
submitted. It is what makes the column worth joining an
|
||||||
@@ -594,7 +593,7 @@ and a blip is seconds:
|
|||||||
|
|
||||||
A command that still cannot reach the engine says
|
A command that still cannot reach the engine says
|
||||||
`fluksio: engine not answering (…)` and names the run it started, which is
|
`fluksio: engine not answering (…)` and names the run it started, which is
|
||||||
running on the engine regardless — `fluksio runs` finds it again.
|
running on the engine regardless, and `fluksio runs` finds it again.
|
||||||
|
|
||||||
## Sweep it
|
## Sweep it
|
||||||
|
|
||||||
@@ -618,7 +617,7 @@ curl -X POST $FLUKSIO/runs/flows/train/sweep -H "Authorization: Bearer $TOKEN" \
|
|||||||
```
|
```
|
||||||
|
|
||||||
They share a `group_id`, so the sweep is `GET /runs?group=…`. Running them
|
They share a `group_id`, so the sweep is `GET /runs?group=…`. Running them
|
||||||
concurrently is safe because each run gets a state backend of its own — two
|
concurrently is safe because each run gets a state backend of its own: two
|
||||||
runs of one flow cannot overwrite each other's values.
|
runs of one flow cannot overwrite each other's values.
|
||||||
|
|
||||||
Compare the curves in one call:
|
Compare the curves in one call:
|
||||||
@@ -633,7 +632,7 @@ which answers in exactly the shape a chart widget draws.
|
|||||||
## Take it into a dataframe
|
## Take it into a dataframe
|
||||||
|
|
||||||
The analysis itself wants a table, and `export` writes the two an analysis
|
The analysis itself wants a table, and `export` writes the two an analysis
|
||||||
asks for — the curves long, and one row per run with the parameters that
|
asks for: the curves long, and one row per run with the parameters that
|
||||||
varied beside its final numbers:
|
varied beside its final numbers:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -643,8 +642,8 @@ fluksio export runs --flow train --status ok -o arms.csv
|
|||||||
|
|
||||||
Both carry the run id on every row, and `arms.csv` carries the commit and the
|
Both carry the run id on every row, and `arms.csv` carries the commit and the
|
||||||
code digest beside it, so an exported file still says what produced its
|
code digest beside it, so an exported file still says what produced its
|
||||||
numbers. Numbers inside a record are columns of their own —
|
numbers. Numbers inside a record are columns of their own
|
||||||
`metric.final_metrics.train_loss` — and `--metrics` and `--params` take those
|
(`metric.final_metrics.train_loss`) and `--metrics` and `--params` take those
|
||||||
dotted paths to narrow the table. Metric names are flow-qualified, so
|
dotted paths to narrow the table. Metric names are flow-qualified, so
|
||||||
`--list` prints the ones a selection carries. `Client.export_metrics()` and
|
`--list` prints the ones a selection carries. `Client.export_metrics()` and
|
||||||
`Client.export_runs()` answer the same rows to a notebook, ready for
|
`Client.export_runs()` answer the same rows to a notebook, ready for
|
||||||
@@ -653,8 +652,8 @@ dotted paths to narrow the table. Metric names are flow-qualified, so
|
|||||||
## Looking at them in the portal
|
## Looking at them in the portal
|
||||||
|
|
||||||
None of the above needs a browser, but there is one. **Runs** is the table of
|
None of the above needs a browser, but there is one. **Runs** is the table of
|
||||||
everything that has run: filter it to a flow, to a status, or to one sweep —
|
everything that has run: filter it to a flow, to a status, or to one sweep,
|
||||||
where it grows a column per parameter that actually varied — and tick two runs
|
where it grows a column per parameter that actually varied, and tick two runs
|
||||||
to put their curves side by side.
|
to put their curves side by side.
|
||||||
|
|
||||||
A run records its metrics under the same names a dashboard binds to, so a
|
A run records its metrics under the same names a dashboard binds to, so a
|
||||||
@@ -721,13 +720,13 @@ api.post("/flows/train/publish", json={"version": version}).raise_for_status()
|
|||||||
```
|
```
|
||||||
|
|
||||||
Two things worth noticing, because the decorators only move where they are
|
Two things worth noticing, because the decorators only move where they are
|
||||||
said. Ports are declared, not inferred — `process(lr, epochs)` gets its
|
said. Ports are declared, not inferred: `process(lr, epochs)` gets its
|
||||||
arguments from the ports of the same name, and the types are checked on every
|
arguments from the ports of the same name, and the types are checked on every
|
||||||
value. And saving writes a *draft*; `publish` is what the engine picks up.
|
value. And saving writes a *draft*; `publish` is what the engine picks up.
|
||||||
That separation is what lets you edit a flow that is running.
|
That separation is what lets you edit a flow that is running.
|
||||||
|
|
||||||
A flow uploaded this way carries no `origin`, which is what tells the canvas —
|
A flow uploaded this way carries no `origin`, which is what tells the canvas,
|
||||||
and the next `fluksio sync` — that it was not generated. See
|
and the next `fluksio sync`, that it was not generated. See
|
||||||
[The API](../code/api.md).
|
[The API](../code/api.md).
|
||||||
|
|
||||||
## Small scripts you are just playing with
|
## Small scripts you are just playing with
|
||||||
@@ -741,7 +740,7 @@ cd .fluksio/flows
|
|||||||
git log --oneline
|
git log --oneline
|
||||||
```
|
```
|
||||||
|
|
||||||
Every save is a commit — the node source, the parameters, the graph. A run
|
Every save is a commit: the node source, the parameters, the graph. A run
|
||||||
records the commit it ran at, so `git show` on that hash is literally the code
|
records the commit it ran at, so `git show` on that hash is literally the code
|
||||||
that produced the number. You get the versioning without adopting anything.
|
that produced the number. You get the versioning without adopting anything.
|
||||||
|
|
||||||
@@ -759,8 +758,8 @@ A submit is around 15 ms, so calling that in a loop is a reasonable thing to do.
|
|||||||
|
|
||||||
## Get a browser onto it
|
## Get a browser onto it
|
||||||
|
|
||||||
The pip install gives you the engine and the API, not a web interface — a
|
The pip install gives you the engine and the API, not a web interface, since a
|
||||||
machine with no inbound route cannot serve one usefully anyway. To see the
|
machine with no inbound route cannot serve one usefully. To see the
|
||||||
canvas, the run history and live loss curves, pair the instance with a
|
canvas, the run history and live loss curves, pair the instance with a
|
||||||
portal, which serves the dashboard from its side:
|
portal, which serves the dashboard from its side:
|
||||||
|
|
||||||
@@ -773,7 +772,7 @@ Nothing needs to be exposed: your machine dials out and holds the connection
|
|||||||
open. See [Accounts and the portal](../interface/portal.md).
|
open. See [Accounts and the portal](../interface/portal.md).
|
||||||
|
|
||||||
If you would rather stay entirely offline, run the dashboard SPA yourself from
|
If you would rather stay entirely offline, run the dashboard SPA yourself from
|
||||||
the app's Docker image — see the [facility path](facility-automation.md), which
|
the app's Docker image; see the [facility path](facility-automation.md), which
|
||||||
is the same stack.
|
is the same stack.
|
||||||
|
|
||||||
## Where to go next
|
## Where to go next
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ in this interface a lot, so it is worth learning it properly.
|
|||||||
└── workers on other boxes (optional)
|
└── workers on other boxes (optional)
|
||||||
```
|
```
|
||||||
|
|
||||||
One engine holds every flow. Nothing here is a plugin you install separately —
|
One engine holds every flow. Nothing here is a plugin you install separately:
|
||||||
the broker client, the time-series writer, the dashboards and the alerting are
|
the broker client, the time-series writer, the dashboards and the alerting are
|
||||||
all part of the same process, editing the same graph.
|
all part of the same process, editing the same graph.
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ all part of the same process, editing the same graph.
|
|||||||
|
|
||||||
- Docker and Compose v2 on the host
|
- Docker and Compose v2 on the host
|
||||||
- The address you will type into a browser. On a home network that is the
|
- The address you will type into a browser. On a home network that is the
|
||||||
server's own address — `192.168.1.50` — and nothing else is needed. A
|
server's own address, `192.168.1.50`, and nothing else is needed. A
|
||||||
hostname is optional, and covered below
|
hostname is optional, and covered below
|
||||||
- Optionally: an MQTT broker and an InfluxDB you already run. If not, the stack
|
- Optionally: an MQTT broker and an InfluxDB you already run. If not, the stack
|
||||||
can start both for you
|
can start both for you
|
||||||
@@ -53,15 +53,15 @@ $EDITOR .env # FIRST_SUPERUSER, ENVIRONMENT=production
|
|||||||
| `FIRST_SUPERUSER_PASSWORD` | leave it as `changethis` and one is generated for you |
|
| `FIRST_SUPERUSER_PASSWORD` | leave it as `changethis` and one is generated for you |
|
||||||
| `ENVIRONMENT` | `production` closes the interactive API schema; `local` leaves it open |
|
| `ENVIRONMENT` | `production` closes the interactive API schema; `local` leaves it open |
|
||||||
|
|
||||||
Everything the instance owns — the database, your flows, secrets,
|
Everything the instance owns (the database, your flows, secrets, artifacts,
|
||||||
artifacts, the packages your node code imports — is on one Docker volume.
|
the packages your node code imports) is on one Docker volume.
|
||||||
Backing that volume up is backing up the instance.
|
Backing that volume up is backing up the instance.
|
||||||
|
|
||||||
??? note "Even smaller: no Docker at all"
|
??? note "Even smaller: no Docker at all"
|
||||||
|
|
||||||
`pip install fluksio && fluksio serve` gives you the same engine with no
|
`pip install fluksio && fluksio serve` gives you the same engine with no
|
||||||
containers, keeping its data in `~/.fluksio`. What it does not give you is
|
containers, keeping its data in `~/.fluksio`. What it does not give you is
|
||||||
the web interface, which the SPA container serves — so you would drive it
|
the web interface, which the SPA container serves, so you would drive it
|
||||||
from the API, or [pair it with a portal](../interface/portal.md) that
|
from the API, or [pair it with a portal](../interface/portal.md) that
|
||||||
serves the dashboard for you. Good for a Raspberry Pi that only runs flows;
|
serves the dashboard for you. Good for a Raspberry Pi that only runs flows;
|
||||||
less good as your main instance.
|
less good as your main instance.
|
||||||
@@ -82,7 +82,7 @@ APP_PORT=8080 docker compose -p fluksio-app --env-file .env \
|
|||||||
-f docker/compose.yml -f docker/compose.lan.yml up -d --build
|
-f docker/compose.yml -f docker/compose.lan.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://192.168.1.50:8080`. That one address is the whole application —
|
Open `http://192.168.1.50:8080`. That one address is the whole application:
|
||||||
the interface, the API under `/api`, and the websocket that puts live values on
|
the interface, the API under `/api`, and the websocket that puts live values on
|
||||||
the canvas.
|
the canvas.
|
||||||
|
|
||||||
@@ -96,13 +96,13 @@ maintaining: the browser is talking to one origin.
|
|||||||
|
|
||||||
A published port is plain HTTP with no certificate. Between your own
|
A published port is plain HTTP with no certificate. Between your own
|
||||||
machines that is fine, and it is not something to forward from a router. To
|
machines that is fine, and it is not something to forward from a router. To
|
||||||
reach the house from outside, put a reverse proxy with TLS in front of it —
|
reach the house from outside, put a reverse proxy with TLS in front of it,
|
||||||
or [pair it with a portal](../interface/portal.md), which needs no inbound
|
or [pair it with a portal](../interface/portal.md), which needs no inbound
|
||||||
route at all.
|
route at all.
|
||||||
|
|
||||||
### With a hostname, behind a reverse proxy
|
### With a hostname, behind a reverse proxy
|
||||||
|
|
||||||
If you have DNS — or want certificates — set `DOMAIN` and run `make up`
|
If you have DNS, or want certificates, set `DOMAIN` and run `make up`
|
||||||
instead. The interface is then served on `app.${DOMAIN}` and the API on
|
instead. The interface is then served on `app.${DOMAIN}` and the API on
|
||||||
`api.${DOMAIN}`, and nothing is published on a host port.
|
`api.${DOMAIN}`, and nothing is published on a host port.
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ attach it to the `proxy` network and forward `app.${DOMAIN}` →
|
|||||||
|
|
||||||
!!! tip "Developing on the same machine"
|
!!! tip "Developing on the same machine"
|
||||||
|
|
||||||
`make dev-lan` is the same idea for the integrated dev stack — the app on
|
`make dev-lan` is the same idea for the integrated dev stack: the app on
|
||||||
`http://<host-ip>:8080`, `APP_PORT` to move it. `make dev` rebuilds without
|
`http://<host-ip>:8080`, `APP_PORT` to move it. `make dev` rebuilds without
|
||||||
the published port, so re-run `make dev-lan` after one.
|
the published port, so re-run `make dev-lan` after one.
|
||||||
|
|
||||||
@@ -128,9 +128,8 @@ Go to **Flows → New flow** and call it `house`. You land on the canvas.
|
|||||||
|
|
||||||
A flow is a set of nodes that talk to each other through **named messages**.
|
A flow is a set of nodes that talk to each other through **named messages**.
|
||||||
You do not draw wires: a node says which messages it needs and which it
|
You do not draw wires: a node says which messages it needs and which it
|
||||||
produces, and the canvas draws the graph that follows from those names. That
|
produces, and the canvas draws the graph that follows. Renaming is safe, fan-in
|
||||||
sounds like a small difference and turns out to be a large one — renaming is
|
is free, and two flows share a value by naming it.
|
||||||
safe, fan-in is free, and two flows can share a value by naming it.
|
|
||||||
|
|
||||||
### Read a sensor
|
### Read a sensor
|
||||||
|
|
||||||
@@ -150,8 +149,8 @@ broker sends one. Click the wire to see the last payload and its history.
|
|||||||
|
|
||||||
### Do something with it
|
### Do something with it
|
||||||
|
|
||||||
Add a **Function** node. This is a Python node — the code editor opens in its
|
Add a **Function** node. This is a Python node, and the code editor opens in
|
||||||
panel:
|
its panel:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def process(living_temperature, comfortable=21.0):
|
def process(living_temperature, comfortable=21.0):
|
||||||
@@ -160,7 +159,7 @@ def process(living_temperature, comfortable=21.0):
|
|||||||
```
|
```
|
||||||
|
|
||||||
Declare `living_temperature` as an input (type `float`) and `heat_wanted` as an
|
Declare `living_temperature` as an input (type `float`) and `heat_wanted` as an
|
||||||
output (type `bool`). `comfortable` is not a port — it is a **setting**,
|
output (type `bool`). `comfortable` is not a port but a **setting**,
|
||||||
because it is a constant of this node rather than something the graph carries.
|
because it is a constant of this node rather than something the graph carries.
|
||||||
It shows up as a field in the node's Settings section.
|
It shows up as a field in the node's Settings section.
|
||||||
|
|
||||||
@@ -178,8 +177,8 @@ Publish the flow. You have a thermostat.
|
|||||||
!!! warning "Test before it touches a relay"
|
!!! warning "Test before it touches a relay"
|
||||||
|
|
||||||
A flow can be **paused** (it holds messages instead of running them) and
|
A flow can be **paused** (it holds messages instead of running them) and
|
||||||
**stepped** (release exactly one). Together with the run button — which
|
**stepped** (release exactly one). Together with the run button, which
|
||||||
injects a value by hand — that is how you convince yourself the logic is
|
injects a value by hand, that is how you convince yourself the logic is
|
||||||
right before the contactor finds out. Both live on the dock at the bottom
|
right before the contactor finds out. Both live on the dock at the bottom
|
||||||
of the canvas.
|
of the canvas.
|
||||||
|
|
||||||
@@ -212,11 +211,10 @@ point.
|
|||||||
Note the token. Credentials never sit in a flow: `{"$secret": "influx-token"}`
|
Note the token. Credentials never sit in a flow: `{"$secret": "influx-token"}`
|
||||||
is a reference into an encrypted store, and the editor renders those fields as
|
is a reference into an encrypted store, and the editor renders those fields as
|
||||||
a secret picker. Add the actual value once under **Secrets**. Flows are a git
|
a secret picker. Add the actual value once under **Secrets**. Flows are a git
|
||||||
repository you might well push somewhere — this is what keeps a password out of
|
repository you might well push somewhere, so this keeps a password out of it.
|
||||||
it.
|
|
||||||
|
|
||||||
Reading back is the same node with `queries` instead of `writes`, or — for
|
Reading back is the same node with `queries` instead of `writes`, or, for
|
||||||
anything a chart asks for — a pair of small Python nodes on either side that
|
anything a chart asks for, a pair of small Python nodes on either side that
|
||||||
build a Flux query and shape its rows. That indirection is deliberate: the
|
build a Flux query and shape its rows. That indirection is deliberate: the
|
||||||
database node holds the connection and nothing else, so a dashboard widget
|
database node holds the connection and nothing else, so a dashboard widget
|
||||||
never learns which database answered it.
|
never learns which database answered it.
|
||||||
@@ -231,7 +229,7 @@ Widgets are typed the same way ports are: a switch binds to a `bool`, a gauge
|
|||||||
to a number, an agenda to a `list`. Bind it wrong and the editor says so rather
|
to a number, an agenda to a `list`. Bind it wrong and the editor says so rather
|
||||||
than drawing nothing.
|
than drawing nothing.
|
||||||
|
|
||||||
Controls work in the other direction — a switch on a dashboard publishes the
|
Controls work in the other direction: a switch on a dashboard publishes the
|
||||||
message it is bound to, exactly as a node would. The canvas draws it as a
|
message it is bound to, exactly as a node would. The canvas draws it as a
|
||||||
labelled endpoint feeding the nodes that read it, so a value never appears from
|
labelled endpoint feeding the nodes that read it, so a value never appears from
|
||||||
nowhere.
|
nowhere.
|
||||||
@@ -247,7 +245,7 @@ lives on a different network, or something needs hardware the engine's host
|
|||||||
does not have.
|
does not have.
|
||||||
|
|
||||||
The unit of distribution is the **worker**. It runs the code of nodes you mark
|
The unit of distribution is the **worker**. It runs the code of nodes you mark
|
||||||
for it, and it dials *out* to the engine — so the Pi in the shed does not need
|
for it, and it dials *out* to the engine, so the Pi in the shed does not need
|
||||||
an inbound route, and the engine does not need to reach it.
|
an inbound route, and the engine does not need to reach it.
|
||||||
|
|
||||||
On the engine, mint a token:
|
On the engine, mint a token:
|
||||||
@@ -276,7 +274,7 @@ flow before the hardware arrives.
|
|||||||
!!! note "What a worker is not"
|
!!! note "What a worker is not"
|
||||||
|
|
||||||
It is not a second engine. Subscriptions, schedules, webhooks and the
|
It is not a second engine. Subscriptions, schedules, webhooks and the
|
||||||
dashboards all stay in one process — that is what keeps a value having one
|
dashboards all stay in one process, which keeps a value having one
|
||||||
definition. A worker executes node bodies, nothing else. Scaling *the
|
definition. A worker executes node bodies, nothing else. Scaling *the
|
||||||
engine* to several processes is not supported: run one.
|
engine* to several processes is not supported: run one.
|
||||||
|
|
||||||
@@ -291,9 +289,9 @@ What you probably want on day one is everything, to ntfy:
|
|||||||
- add an ntfy channel with your server and topic
|
- add an ntfy channel with your server and topic
|
||||||
- add a rule with no events ticked, which means *all of them*
|
- add a rule with no events ticked, which means *all of them*
|
||||||
|
|
||||||
The engine deduplicates aggressively — the same node failing every second is
|
The engine deduplicates aggressively, so the same node failing every second is
|
||||||
one alert, not thirty-six thousand — and caps the total at ten an hour however
|
one alert rather than thirty-six thousand, and caps the total at ten an hour
|
||||||
bad it gets. See [Secrets, modules and alerts](../interface/operations.md).
|
however bad it gets. See [Secrets, modules and alerts](../interface/operations.md).
|
||||||
|
|
||||||
## Keep it alive
|
## Keep it alive
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# Pick your starting point
|
# Pick your starting point
|
||||||
|
|
||||||
People arrive at Fluksio from two directions, and the honest answer to "how do
|
Setup differs depending on what you are here for. Pick the path that matches.
|
||||||
I set this up?" is different for each — not just in the commands, but in how
|
|
||||||
much of an afternoon it is reasonable to spend.
|
|
||||||
|
|
||||||
Pick the one that sounds like you. Everything past this section is the same for
|
Pick the one that sounds like you. Everything past this section is the same for
|
||||||
both.
|
both.
|
||||||
@@ -52,8 +50,8 @@ Some rough tells:
|
|||||||
| **Data lives in** | SQLite beside the flows | InfluxDB, usually |
|
| **Data lives in** | SQLite beside the flows | InfluxDB, usually |
|
||||||
| **The thing you look at** | run history and loss curves | a dashboard, maybe on a wall |
|
| **The thing you look at** | run history and loss curves | a dashboard, maybe on a wall |
|
||||||
|
|
||||||
If both describe you — a lab with instruments to drive *and* models to
|
If both describe you (a lab with instruments to drive *and* models to fit),
|
||||||
fit — start with the data-science path. It is the smaller instance, and it
|
start with the data-science path. It is the smaller instance, and it
|
||||||
grows into the other one without being reinstalled: the same engine, the same
|
grows into the other one without being reinstalled: the same engine, the same
|
||||||
flows, just more of them running all the time.
|
flows, just more of them running all the time.
|
||||||
|
|
||||||
|
|||||||
+18
-23
@@ -1,45 +1,40 @@
|
|||||||
# Fluksio
|
# Fluksio
|
||||||
|
|
||||||
Fluksio is a node-based automation engine. You describe what should happen as a
|
Fluksio is a node-based automation engine. You describe what should happen as a
|
||||||
graph of small pieces of logic, and it keeps that graph running — reacting to
|
graph of small pieces of logic, and it keeps that graph running: reacting to
|
||||||
what arrives, or executing once from parameters to a result.
|
what arrives, or executing once from parameters to a result.
|
||||||
|
|
||||||
Two very different jobs turn out to be the same shape, which is why the same
|
It runs two kinds of graph:
|
||||||
engine does both:
|
|
||||||
|
|
||||||
- **A house, a lab or a plant** produces values forever. A sensor publishes, a
|
- A **live flow** produces values forever. A sensor publishes, a rule fires, a
|
||||||
rule fires, a relay closes, a dashboard on the wall shows what happened. The
|
relay closes, a dashboard on the wall shows what happened. It never ends.
|
||||||
flow never ends.
|
- A **run** produces a value once. Parameters go in, stages execute, and it
|
||||||
- **An experiment** produces a value once. Parameters go in, stages execute,
|
finishes, leaving metrics, artifacts and a record to compare against.
|
||||||
and at some point it is *done* and has left behind metrics, artifacts and a
|
|
||||||
record you can compare against last month's.
|
|
||||||
|
|
||||||
The first is a **live flow**, the second is a **run**. Both are the same nodes,
|
Both use the same nodes, the same type checking, the same editor and the same
|
||||||
the same type checking, the same editor and the same API.
|
API.
|
||||||
|
|
||||||
## What you actually get
|
## What you get
|
||||||
|
|
||||||
- A **flow engine** that owns your graph, checks the types on every edge, and
|
- A **flow engine** that owns your graph, checks the types on every edge, and
|
||||||
keeps running when a node fails rather than taking the rest down with it.
|
keeps running when a node fails.
|
||||||
- A **canvas** that lays flows out for you and an editor for the Python inside
|
- A **canvas** that lays flows out for you and an editor for the Python inside
|
||||||
each node — with the running values drawn on the wires while you work.
|
each node, with the running values drawn on the wires while you work.
|
||||||
- **Dashboards** built next to the logic that feeds them, including ones you
|
- **Dashboards** built next to the logic that feeds them, including ones you
|
||||||
can hang on a wall tablet that has no keyboard.
|
can hang on a wall tablet that has no keyboard.
|
||||||
- **Runs**: parameters, metrics, artifacts, sweeps, and a queryable history —
|
- **Runs**: parameters, metrics, artifacts, sweeps and a queryable history, with
|
||||||
without a second server and without paying a project bootstrap per execution.
|
no second server and no project bootstrap per execution.
|
||||||
- **Distributed workers**: a node marked `device: gpu` runs on the machine that
|
- **Distributed workers**: a node marked `device: gpu` runs on the machine that
|
||||||
has one, which dials out to the engine rather than needing to be reachable.
|
has one, which dials out to the engine rather than needing to be reachable.
|
||||||
- An **HTTP API** that came first — everything the browser does, you can do from
|
- An **HTTP API** covering everything the browser does, plus an MCP endpoint for
|
||||||
a script — plus an MCP endpoint for agents.
|
agents.
|
||||||
|
|
||||||
## Start here
|
## Start here
|
||||||
|
|
||||||
The setup is genuinely different depending on what you are here for, so
|
[Getting started](getting-started/index.md) has two paths: one installs a Python
|
||||||
[Getting started](getting-started/index.md) splits in two: one path installs a
|
package, the other stands up a server. Everything after that is shared.
|
||||||
Python package and gets out of your way, the other stands up a server you will
|
|
||||||
be running for years. Everything after that is shared.
|
|
||||||
|
|
||||||
If you would rather look before installing, the hosted demo at
|
To look before installing, the hosted demo at
|
||||||
[fluksio.com](https://fluksio.com) runs a real instance with a small-house panel
|
[fluksio.com](https://fluksio.com) runs a real instance with a small-house panel
|
||||||
and a training pipeline on it.
|
and a training pipeline on it.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Dashboards and panels
|
# Dashboards and panels
|
||||||
|
|
||||||
A dashboard is a grid of widgets bound to message names — the same names that
|
A dashboard is a grid of widgets bound to message names, the same names that
|
||||||
wire the graph. It is its own document rather than a set of nodes, so it reads
|
wire the graph. It is its own document rather than a set of nodes, so it reads
|
||||||
across flows without belonging to any of them, and a flow stays the logic it
|
across flows without belonging to any of them, and a flow stays the logic it
|
||||||
was.
|
was.
|
||||||
@@ -39,7 +39,7 @@ and dragging is off. Picking a widget and editing its settings still works.
|
|||||||
| **Clock** | — | the time, in a size a wall can read |
|
| **Clock** | — | the time, in a size a wall can read |
|
||||||
|
|
||||||
Every widget carries a **title**, and **Show title** decides whether the panel
|
Every widget carries a **title**, and **Show title** decides whether the panel
|
||||||
draws it. Turned off, the tile is just the reading — which is what a row of
|
draws it. Turned off, the tile is just the reading, which is what a row of
|
||||||
gauges under one heading wants. The title is still the widget's name: what a
|
gauges under one heading wants. The title is still the widget's name: what a
|
||||||
screen reader calls its controls, and what a published value is labelled with.
|
screen reader calls its controls, and what a published value is labelled with.
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ screen reader calls its controls, and what a published value is labelled with.
|
|||||||
| **Selector** | one of a list | a mode, a scene, a preset — as a menu, or as a row of choices with the active one held |
|
| **Selector** | one of a list | a mode, a scene, a preset — as a menu, or as a row of choices with the active one held |
|
||||||
| **Colour** | `[h, s, v]`, `[r, g, b]` or `"#rrggbb"` | a hue wheel with saturation and brightness, for an RGB fixture |
|
| **Colour** | `[h, s, v]`, `[r, g, b]` or `"#rrggbb"` | a hue wheel with saturation and brightness, for an RGB fixture |
|
||||||
|
|
||||||
The colour wheel sends `[h, s, v]` by default — hue 0-360 degrees, saturation
|
The colour wheel sends `[h, s, v]` by default (hue 0-360 degrees, saturation
|
||||||
and value 0-100 percent, which is what a DMX encoder expects — and its
|
and value 0-100 percent, which is what a DMX encoder expects) and its
|
||||||
**Sends** setting switches that to `[r, g, b]` (0-255 each) or to a
|
**Sends** setting switches that to `[r, g, b]` (0-255 each) or to a
|
||||||
`"#rrggbb"` string, because fixtures differ. The first two bind a `list`
|
`"#rrggbb"` string, because fixtures differ. The first two bind a `list`
|
||||||
message, the third a `str`.
|
message, the third a `str`.
|
||||||
@@ -82,21 +82,20 @@ buffer of recent values, extended over the socket as new ones land. Nothing
|
|||||||
else is involved. This is what you want for "the last few hours of the living
|
else is involved. This is what you want for "the last few hours of the living
|
||||||
room".
|
room".
|
||||||
|
|
||||||
**Querying.** The chart publishes a *request* — the window and resolution it
|
**Querying.** The chart publishes a *request* (the window and resolution it
|
||||||
wants — exactly as a slider publishes a value, and draws the `series` some flow
|
wants) exactly as a slider publishes a value, and draws the `series` some flow
|
||||||
answers with. What serves that request is the flow's business: typically a
|
answers with. What serves that request is the flow's business: typically a
|
||||||
small Python node that builds a Flux query, an InfluxDB node that runs it, and
|
small Python node that builds a Flux query, an InfluxDB node that runs it, and
|
||||||
another Python node that shapes the rows.
|
another Python node that shapes the rows.
|
||||||
|
|
||||||
That indirection is the point. The widget never learns which database answered
|
The widget never learns which database answered it, so swapping the store
|
||||||
it, so swapping the store is a change to one flow and nothing else. The answer
|
changes one flow and nothing else. An answer states what it was computed for,
|
||||||
also states what it was computed for, so an answer to a different question is
|
so an answer to a different question is ignored.
|
||||||
ignored rather than two charts overwriting each other's picture.
|
|
||||||
|
|
||||||
!!! note "Drop the bucket that is still filling"
|
!!! note "Drop the bucket that is still filling"
|
||||||
|
|
||||||
The request carries a window and an interval, and the binning is the flow's
|
The request carries a window and an interval, and the binning is the flow's
|
||||||
own work — so the newest bucket only ever holds the part of an interval
|
own work, so the newest bucket only ever holds the part of an interval
|
||||||
that has elapsed. Drawn, it reads as a fall that never happened. The
|
that has elapsed. Drawn, it reads as a fall that never happened. The
|
||||||
engine's own [Activity charts](index.md#activity) end on the last closed
|
engine's own [Activity charts](index.md#activity) end on the last closed
|
||||||
bin for that reason; a flow answering a chart has to drop or hold back its
|
bin for that reason; a flow answering a chart has to drop or hold back its
|
||||||
@@ -105,8 +104,8 @@ ignored rather than two charts overwriting each other's picture.
|
|||||||
## Media tiles
|
## Media tiles
|
||||||
|
|
||||||
A media widget draws what its message points at: a picture, a clip with
|
A media widget draws what its message points at: a picture, a clip with
|
||||||
controls, a video. Media does not travel as a message — a reference to it does
|
controls, a video. Media does not travel as a message; a reference to it does.
|
||||||
— so the tile fetches the bytes behind whichever reference the message holds,
|
The tile fetches the bytes behind whichever reference the message holds,
|
||||||
and redraws when a new one arrives.
|
and redraws when a new one arrives.
|
||||||
|
|
||||||
**Crop or fit** decides how a picture fills the tile. **Play as it arrives**
|
**Crop or fit** decides how a picture fills the tile. **Play as it arrives**
|
||||||
@@ -115,7 +114,7 @@ touched the page, so a screen nobody has tapped stays silent.
|
|||||||
|
|
||||||
Rate is the thing to get right. A frame every second or two is a glance at a
|
Rate is the thing to get right. A frame every second or two is a glance at a
|
||||||
door, and works; through the portal, make that every few seconds. Live video is
|
door, and works; through the portal, make that every few seconds. Live video is
|
||||||
not something to push through the message plane at all — put the camera's own
|
not something to push through the message plane at all. Put the camera's own
|
||||||
address in **Live stream** and the browser plays it from source, leaving the
|
address in **Live stream** and the browser plays it from source, leaving the
|
||||||
messages to carry the occasional still that a flow can actually react to.
|
messages to carry the occasional still that a flow can actually react to.
|
||||||
|
|
||||||
@@ -136,7 +135,7 @@ carrying what to do about it:
|
|||||||
|
|
||||||
The buttons and the bar publish words: `toggle`, `next`, `prev` and
|
The buttons and the bar publish words: `toggle`, `next`, `prev` and
|
||||||
`seek:<seconds>`. Those are a streamer's own vocabulary rather than this app's,
|
`seek:<seconds>`. Those are a streamer's own vocabulary rather than this app's,
|
||||||
which is what lets one tile drive whatever is on the other end — the node that
|
which is what lets one tile drive whatever is on the other end: the node that
|
||||||
receives them decides what they mean for its device.
|
receives them decides what they mean for its device.
|
||||||
|
|
||||||
The position counts forward in the browser between readings, so the bar moves
|
The position counts forward in the browser between readings, so the bar moves
|
||||||
@@ -147,7 +146,7 @@ reading that arrives is taken as the truth and the count restarts from it.
|
|||||||
|
|
||||||
Most of what a dashboard carries is a widget: a tile bound to a message. A
|
Most of what a dashboard carries is a widget: a tile bound to a message. A
|
||||||
handful of things are not, because they belong to the whole surface rather than
|
handful of things are not, because they belong to the whole surface rather than
|
||||||
to any tile on it — and a screen bolted to a wall has nobody standing at it to
|
to any tile on it, and a screen bolted to a wall has nobody standing at it to
|
||||||
set them.
|
set them.
|
||||||
|
|
||||||
| Setting | Is | Driven by |
|
| Setting | Is | Driven by |
|
||||||
@@ -169,25 +168,24 @@ They all work the same way, and both halves are optional:
|
|||||||
fallback: what the dashboard uses before the first message arrives, and
|
fallback: what the dashboard uses before the first message arrives, and
|
||||||
whenever the message is silent.
|
whenever the message is silent.
|
||||||
|
|
||||||
A bound setting is type-checked like a widget binding — a Theme pointed at a
|
A bound setting is type-checked like a widget binding, so a Theme pointed at a
|
||||||
`float` is refused by the editor and by the server — and the panel's credential
|
`float` is refused by the editor and by the server. The panel's credential
|
||||||
is extended to it, so a paired screen may read its own theme message and
|
is extended to it, so a paired screen may read its own theme message and
|
||||||
nothing further.
|
nothing further.
|
||||||
|
|
||||||
There is no schedule field, on purpose. **A schedule is a node publishing to
|
There is no schedule field. **A schedule is a node publishing to the bound
|
||||||
the bound message**: an `inject` with a cron expression, feeding a `change`
|
message**: an `inject` with a cron expression feeding a `change` node that maps
|
||||||
node that maps the hour onto a palette, is the whole of "warmer after sunset" —
|
the hour onto a palette is the whole of "warmer after sunset". The same channel
|
||||||
and the same channel then serves anything else you want to drive, including
|
drives anything else, including locking a panel down remotely.
|
||||||
locking a panel down remotely.
|
|
||||||
|
|
||||||
### Look
|
### Look
|
||||||
|
|
||||||
**Fluksio** is the app's own design — `--card` surfaces separated by a hairline
|
**Fluksio** is the app's own design (`--card` surfaces separated by a hairline
|
||||||
and a low shadow, pill controls, one slate-blue accent — and is what a dashboard
|
and a low shadow, pill controls, one slate-blue accent) and is what a dashboard
|
||||||
wears until you choose otherwise. **Material** lays flat, tonal cards on a plain
|
wears until you choose otherwise. **Material** lays flat, tonal cards on a plain
|
||||||
ground. **Glass** floats translucent tiles over a soft, slowly moving one.
|
ground. **Glass** floats translucent tiles over a soft, slowly moving one.
|
||||||
|
|
||||||
They are three complete sets of components, not three stylesheets — but they are
|
They are three complete sets of components, not three stylesheets, but they are
|
||||||
the same dashboard: every widget, every control and every keystroke behaves
|
the same dashboard: every widget, every control and every keystroke behaves
|
||||||
identically, so switching look never changes what a panel can do.
|
identically, so switching look never changes what a panel can do.
|
||||||
|
|
||||||
@@ -205,7 +203,7 @@ A palette is an ordered list of colours, and **position is the role**:
|
|||||||
| 6+ | further chart colours |
|
| 6+ | further chart colours |
|
||||||
|
|
||||||
Paste a [coolors.co](https://coolors.co) link (or a list of hex colours) and the
|
Paste a [coolors.co](https://coolors.co) link (or a list of hex colours) and the
|
||||||
whole dashboard is recoloured — every widget, the rail and the charts. Leave the
|
whole dashboard is recoloured: every widget, the rail and the charts. Leave the
|
||||||
later roles off and they are worked out from the ones you gave, so **three
|
later roles off and they are worked out from the ones you gave, so **three
|
||||||
colours are a whole dashboard**. **Rotate** turns the list when the roles landed
|
colours are a whole dashboard**. **Rotate** turns the list when the roles landed
|
||||||
in the wrong order. Text that could not be read on a surface is replaced with
|
in the wrong order. Text that could not be read on a surface is replaced with
|
||||||
@@ -213,8 +211,8 @@ black or white there, so no palette can produce a line nobody can see.
|
|||||||
|
|
||||||
While a palette is set, **Theme is idle**: the first colour is the ground, so
|
While a palette is set, **Theme is idle**: the first colour is the ground, so
|
||||||
whether the panel reads light or dark is already decided by the palette itself.
|
whether the panel reads light or dark is already decided by the palette itself.
|
||||||
Fault and success keep their own colours in every palette — a failure must never
|
Fault and success keep their own colours in every palette, so a failure is never
|
||||||
be paintable as a reading.
|
paintable as a reading.
|
||||||
|
|
||||||
### Background
|
### Background
|
||||||
|
|
||||||
@@ -249,25 +247,25 @@ A wall tablet has no keyboard, so it pairs.
|
|||||||
2. Point the device's browser at the link the dialog shows. The device then
|
2. Point the device's browser at the link the dialog shows. The device then
|
||||||
displays a six-character code.
|
displays a six-character code.
|
||||||
3. Type that code into the same panel's **Pair device** field. The line under
|
3. Type that code into the same panel's **Pair device** field. The line under
|
||||||
it names what is holding the code — check it is the screen you just hung,
|
it names what is holding the code. Check it is the screen you just hung,
|
||||||
because approving adopts whatever answered. The screen picks the credential
|
because approving adopts whatever answered. The screen picks the credential
|
||||||
up within a few seconds and never asks again.
|
up within a few seconds and never asks again.
|
||||||
|
|
||||||
What the screen holds is not a login. It reaches that panel's published
|
What the screen holds is not a login. It reaches that panel's published
|
||||||
dashboards and the messages its own widgets read or publish, and nothing else —
|
dashboards and the messages its own widgets read or publish, and nothing else.
|
||||||
a message no tile on it draws is refused in both directions.
|
A message no tile on it draws is refused in both directions.
|
||||||
|
|
||||||
The *credential* cannot be made strictly read-only, and that is honest rather
|
The *credential* cannot be made strictly read-only, and that is honest rather
|
||||||
than an oversight: a querying chart publishes its request, and a control on a
|
than an oversight: a querying chart publishes its request, and a control on a
|
||||||
panel is the reason you put one there. The dashboard's own **Lock** setting
|
panel is the reason you put one there. The dashboard's own **Lock** setting
|
||||||
above stops its controls publishing and can be driven by a flow, which is how
|
above stops its controls publishing and can be driven by a flow, which is how
|
||||||
you quieten a screen remotely — but that is the surface behaving, not the
|
you quieten a screen remotely, but that is the surface behaving, not the
|
||||||
credential being narrowed.
|
credential being narrowed.
|
||||||
|
|
||||||
Deleting the panel revokes the credential and the assignment together, which is
|
Deleting the panel revokes the credential and the assignment together, which is
|
||||||
how you retire a device and what it showed. Unpairing revokes only the
|
how you retire a device and what it showed. Unpairing revokes only the
|
||||||
credential: the panel, its dashboards and their arrangement stay exactly where
|
credential: the panel, its dashboards and their arrangement stay exactly where
|
||||||
they are, and the screen falls back to asking for a new code — which is how you
|
they are, and the screen falls back to asking for a new code, which is how you
|
||||||
swap the device out without rebuilding what hangs there.
|
swap the device out without rebuilding what hangs there.
|
||||||
|
|
||||||
!!! note "If the link is wrong"
|
!!! note "If the link is wrong"
|
||||||
@@ -290,7 +288,7 @@ The portal names the panel and nothing else. What the panel may read is decided
|
|||||||
on the instance, on every call, by the same check a locally paired screen
|
on the instance, on every call, by the same check a locally paired screen
|
||||||
passes. Two differences: it acts as the account the instance was enrolled
|
passes. Two differences: it acts as the account the instance was enrolled
|
||||||
with rather than as whoever approved it, and deleting the panel stops it here
|
with rather than as whoever approved it, and deleting the panel stops it here
|
||||||
immediately while the portal's copy of the token expires on its own — which is
|
immediately while the portal's copy of the token expires on its own, which is
|
||||||
also why unpairing, which works on a credential this instance signed, does
|
also why unpairing, which works on a credential this instance signed, does
|
||||||
not reach a remote screen. Revoke that one at the hub.
|
not reach a remote screen. Revoke that one at the hub.
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ full-bleed graph, so what you are looking at is always the flow.
|
|||||||
|
|
||||||
## The canvas
|
## The canvas
|
||||||
|
|
||||||
Nodes are laid out for you — left to right on a desktop, top to bottom on a
|
Nodes are laid out for you, left to right on a desktop and top to bottom on a
|
||||||
phone — and the arrangement is recomputed as the graph changes. There is
|
phone, and the arrangement is recomputed as the graph changes. There is
|
||||||
nothing to drag and nothing to tidy.
|
nothing to drag and nothing to tidy.
|
||||||
|
|
||||||
That is on purpose. The edges come from message names, so a stored layout would
|
The edges come from message names, so there is no layout to store or to merge
|
||||||
be a second, weaker description of the same thing, and something to merge when
|
when two people edit the same flow.
|
||||||
two people edit the same flow. It also keeps flows honest: a graph nobody can
|
|
||||||
hand-arrange is one worth splitting.
|
|
||||||
|
|
||||||
What is drawn:
|
What is drawn:
|
||||||
|
|
||||||
@@ -38,7 +36,7 @@ the canvas with its panel open.
|
|||||||
Floats on the right; a full-screen sheet on a phone.
|
Floats on the right; a full-screen sheet on a phone.
|
||||||
|
|
||||||
**Consumes** and **Provides** are the ports. Each row is a message name, a
|
**Consumes** and **Provides** are the ports. Each row is a message name, a
|
||||||
payload type, and — on an output — a *streaming* toggle for ports that publish
|
payload type, and, on an output, a *streaming* toggle for ports that publish
|
||||||
repeatedly during one execution. The name field suggests messages already in
|
repeatedly during one execution. The name field suggests messages already in
|
||||||
the flow, which is usually how you wire something: type the name that already
|
the flow, which is usually how you wire something: type the name that already
|
||||||
exists.
|
exists.
|
||||||
@@ -47,7 +45,7 @@ Renaming an output renames the message everywhere it is read. Inputs are not
|
|||||||
renamed, because an input is more often re-pointed at a different message than
|
renamed, because an input is more often re-pointed at a different message than
|
||||||
renamed.
|
renamed.
|
||||||
|
|
||||||
**Settings** is generated from the node type's parameter schema — so an MQTT
|
**Settings** is generated from the node type's parameter schema, so an MQTT
|
||||||
node shows broker, topic and QoS, and an ntfy node shows server, topic and
|
node shows broker, topic and QoS, and an ntfy node shows server, topic and
|
||||||
priority. A field marked as a credential renders as a picker over your stored
|
priority. A field marked as a credential renders as a picker over your stored
|
||||||
[secrets](operations.md) instead of a text box.
|
[secrets](operations.md) instead of a text box.
|
||||||
@@ -59,7 +57,7 @@ ports. See [Where a node's values come from](../concepts/values.md).
|
|||||||
**Timeout** (Function nodes) is how many seconds the code may be *silent*
|
**Timeout** (Function nodes) is how many seconds the code may be *silent*
|
||||||
before it is stopped: it measures silence, not duration, so a node that yields
|
before it is stopped: it measures silence, not duration, so a node that yields
|
||||||
every few seconds can run for hours under a timeout of 300. Left empty there is
|
every few seconds can run for hours under a timeout of 300. Left empty there is
|
||||||
no limit, which is the default — set one where a node going quiet means it is
|
no limit, which is the default. Set one where a node going quiet means it is
|
||||||
stuck rather than working. A `0` says so explicitly.
|
stuck rather than working. A `0` says so explicitly.
|
||||||
|
|
||||||
**Code** is the editor. It saves as you type and applies on ⌘S; the node
|
**Code** is the editor. It saves as you type and applies on ⌘S; the node
|
||||||
@@ -71,10 +69,10 @@ instead.
|
|||||||
Editing a shared node edits the copy every flow using it runs, which is the
|
Editing a shared node edits the copy every flow using it runs, which is the
|
||||||
point and also the caution.
|
point and also the caution.
|
||||||
|
|
||||||
!!! note "Not in the panel (yet)"
|
!!! note "Not in the panel yet"
|
||||||
|
|
||||||
`device` and `device_policy` — which machine a node's code runs on — are set
|
`device` and `device_policy`, which machine a node's code runs on, are set
|
||||||
through the API rather than the panel. See
|
through the API rather than the panel, with `PUT /flows/{name}`. See
|
||||||
[Remote workers](../code/workers.md).
|
[Remote workers](../code/workers.md).
|
||||||
|
|
||||||
## The flow panel
|
## The flow panel
|
||||||
@@ -95,8 +93,8 @@ Click the canvas title to open the flow's own settings.
|
|||||||
|
|
||||||
Click a wire. You get the last payload that went down it, when, and a sparkline
|
Click a wire. You get the last payload that went down it, when, and a sparkline
|
||||||
of its recent numeric history. You can also republish that value from the node
|
of its recent numeric history. You can also republish that value from the node
|
||||||
that produced it, which re-runs everything downstream — the fastest way to test
|
that produced it, which re-runs everything downstream. It is the fastest way to
|
||||||
a change without waiting for the real sensor.
|
test a change without waiting for the real sensor.
|
||||||
|
|
||||||
## The dock
|
## The dock
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# The dashboard app
|
# The dashboard app
|
||||||
|
|
||||||
The web interface is a single-page app served at `app.${DOMAIN}` — or, for an
|
The web interface is a single-page app served at `app.${DOMAIN}`, or, for an
|
||||||
instance reached through a portal, at `${DOMAIN}/i/{instance-id}`.
|
instance reached through a portal, at `${DOMAIN}/i/{instance-id}`.
|
||||||
Either way it is the same application, and it is a client of the same REST API
|
Either way it is the same application, and it is a client of the same REST API
|
||||||
you can script against.
|
you can script against.
|
||||||
@@ -54,7 +54,7 @@ Each flow also has a switch beside it in the list, which starts and stops it.
|
|||||||
|
|
||||||
Two columns under the graph, exactly as tall as each other, most recently
|
Two columns under the graph, exactly as tall as each other, most recently
|
||||||
worked on first. The flows column is the list with the switches; the dashboards
|
worked on first. The flows column is the list with the switches; the dashboards
|
||||||
column is a mosaic, each tile a schematic of that dashboard's layout — blocks
|
column is a mosaic, each tile a schematic of that dashboard's layout: blocks
|
||||||
where its widgets sit, shaded by what kind of widget each one is. Neither
|
where its widgets sit, shaded by what kind of widget each one is. Neither
|
||||||
column grows past about six rows: past that it scrolls in place rather than
|
column grows past about six rows: past that it scrolls in place rather than
|
||||||
pushing the health block down the page.
|
pushing the health block down the page.
|
||||||
@@ -98,7 +98,7 @@ Opening one takes you to [the flow editor](flow-editor.md).
|
|||||||
|
|
||||||
### Deleting several at once
|
### Deleting several at once
|
||||||
|
|
||||||
Press and hold a card — or ctrl-click it — to pick it, then tap the rest. While
|
Press and hold a card, or ctrl-click it, to pick it, then tap the rest. While
|
||||||
anything is picked, **New flow** in the toolbar becomes a trash button, and it
|
anything is picked, **New flow** in the toolbar becomes a trash button, and it
|
||||||
asks once before deleting the lot. Unpicking the last one puts the list back;
|
asks once before deleting the lot. Unpicking the last one puts the list back;
|
||||||
so does Escape. The Dashboards screen works the same way.
|
so does Escape. The Dashboards screen works the same way.
|
||||||
@@ -117,5 +117,5 @@ so does Escape. The Dashboards screen works the same way.
|
|||||||
## Appearance
|
## Appearance
|
||||||
|
|
||||||
Light and dark follow your system by default; **Settings → Appearance**
|
Light and dark follow your system by default; **Settings → Appearance**
|
||||||
overrides it. Both themes are first-class — the wall-panel view in particular is
|
overrides it. Both themes are first-class, and the wall-panel view is designed
|
||||||
designed to be legible in dark from across a room.
|
to be legible in dark from across a room.
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ They are encrypted at rest with a key derived from the instance's
|
|||||||
flows are a git repository you may well push somewhere: what gets committed and
|
flows are a git repository you may well push somewhere: what gets committed and
|
||||||
shared never contains a password.
|
shared never contains a password.
|
||||||
|
|
||||||
A name that no longer resolves is a clear error on the node that wanted it —
|
A name that no longer resolves is a clear error on the node that wanted it
|
||||||
"No secret named 'x' — add it under Secrets" — rather than a mysterious
|
("No secret named 'x'; add it under Secrets") rather than a mysterious
|
||||||
authentication failure.
|
authentication failure.
|
||||||
|
|
||||||
!!! warning "Rotating `SECRET_KEY`"
|
!!! warning "Rotating `SECRET_KEY`"
|
||||||
@@ -36,7 +36,7 @@ authentication failure.
|
|||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
Node code runs in a virtual environment of its own, on the instance's data
|
Node code runs in a virtual environment of its own, on the instance's data
|
||||||
volume — separate from the packages Fluksio itself runs on. A pin of yours can
|
volume, separate from the packages Fluksio itself runs on. A pin of yours can
|
||||||
never shadow one of ours, and vice versa.
|
never shadow one of ours, and vice versa.
|
||||||
|
|
||||||
**Modules** is a pip manifest and an **Apply** button. It is installed with
|
**Modules** is a pip manifest and an **Apply** button. It is installed with
|
||||||
@@ -49,7 +49,7 @@ and whether the manifest and the environment agree. A failed resolve comes back
|
|||||||
as `uv`'s own output, in the pane, because that is the only thing anyone can
|
as `uv`'s own output, in the pane, because that is the only thing anyone can
|
||||||
act on.
|
act on.
|
||||||
|
|
||||||
An install takes effect immediately — the worker processes pick up the new
|
An install takes effect immediately: the worker processes pick up the new
|
||||||
interpreter state without the engine restarting.
|
interpreter state without the engine restarting.
|
||||||
|
|
||||||
!!! note "Big scientific stacks"
|
!!! note "Big scientific stacks"
|
||||||
@@ -79,7 +79,7 @@ literal, so a webhook URL with a token in it does not sit in the config.
|
|||||||
### Rules
|
### Rules
|
||||||
|
|
||||||
A rule is a set of events and the channels they go to. **A rule with no events
|
A rule is a set of events and the channels they go to. **A rule with no events
|
||||||
ticked covers all of them**, including ones added in later versions — which is
|
ticked covers all of them**, including ones added in later versions, which is
|
||||||
usually what you want for the first rule you write.
|
usually what you want for the first rule you write.
|
||||||
|
|
||||||
| Event | Fires when |
|
| Event | Fires when |
|
||||||
@@ -113,7 +113,7 @@ Superusers get an **Admin** screen: add, edit and remove accounts, and see who
|
|||||||
is a superuser.
|
is a superuser.
|
||||||
|
|
||||||
Accounts that arrived through a portal are badged *Portal*. They have no
|
Accounts that arrived through a portal are badged *Portal*. They have no
|
||||||
password — they sign in at the portal, not here — and they are never
|
password (they sign in at the portal, not here) and they are never
|
||||||
superusers. Deleting one cuts their access immediately and independently of the
|
superusers. Deleting one cuts their access immediately and independently of the
|
||||||
portal, so it works even if the portal cannot be reached. See
|
portal, so it works even if the portal cannot be reached. See
|
||||||
[Accounts and the portal](portal.md).
|
[Accounts and the portal](portal.md).
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ The **portal** is optional, and it exists to solve two specific problems:
|
|||||||
|
|
||||||
An enrolled instance dials *out* to the portal and holds one websocket
|
An enrolled instance dials *out* to the portal and holds one websocket
|
||||||
open. The portal serves the dashboard from its own side, and only the API calls
|
open. The portal serves the dashboard from its own side, and only the API calls
|
||||||
travel down the tunnel — so the interface loads at portal speed and your
|
travel down the tunnel, so the interface loads at portal speed and your
|
||||||
machine stays unreachable from the internet.
|
machine stays unreachable from the internet.
|
||||||
|
|
||||||
## Enrolling
|
## Enrolling
|
||||||
|
|
||||||
Two halves, deliberately: whoever performs the second step decides what the
|
Two halves: whoever performs the second step decides what the instance's owner
|
||||||
instance's owner gets.
|
gets.
|
||||||
|
|
||||||
**On the portal** (`hub.${DOMAIN}`, or [fluksio.com](https://fluksio.com) for
|
**On the portal** (`hub.${DOMAIN}`, or [fluksio.com](https://fluksio.com) for
|
||||||
the hosted one): **Instances → Add instance**, give it a name, and copy
|
the hosted one): **Instances → Add instance**, give it a name, and copy
|
||||||
@@ -39,10 +39,9 @@ fluksio enroll ABCD-1234 --portal https://hub.fluksio.com
|
|||||||
|
|
||||||
The code is single-use and expires in fifteen minutes.
|
The code is single-use and expires in fifteen minutes.
|
||||||
|
|
||||||
A portal session then arrives as *that local account* — the settings screen
|
A portal session then arrives as *that local account*, which the settings screen
|
||||||
states this plainly, because it is the whole security model in one sentence.
|
states plainly. Use `--as someone@example.com` to enrol as a specific local
|
||||||
Use `--as someone@example.com` to enrol as a specific local account when the
|
account when the instance has several superusers.
|
||||||
instance has several superusers.
|
|
||||||
|
|
||||||
Once enrolled, the instance appears under **Instances** with its
|
Once enrolled, the instance appears under **Instances** with its
|
||||||
status, when it was last seen and its version. **Open** takes you to its
|
status, when it was last seen and its version. **Open** takes you to its
|
||||||
@@ -55,7 +54,7 @@ the tunnel. What those requests may do is decided **on the instance**, by
|
|||||||
the same checks a local session passes.
|
the same checks a local session passes.
|
||||||
|
|
||||||
The trust anchor is a signing keypair on the portal. Every instance pins
|
The trust anchor is a signing keypair on the portal. Every instance pins
|
||||||
its public half at enrolment and rejects anything else — which is what stops a
|
its public half at enrolment and rejects anything else, which is what stops a
|
||||||
hijacked DNS entry or a mis-issued certificate from impersonating the portal.
|
hijacked DNS entry or a mis-issued certificate from impersonating the portal.
|
||||||
|
|
||||||
## Letting someone else in
|
## Letting someone else in
|
||||||
@@ -72,7 +71,7 @@ admits them, and they arrive as a local user of their own rather than as you.
|
|||||||
you.
|
you.
|
||||||
|
|
||||||
The instance redeems that code against the portal using its own credential.
|
The instance redeems that code against the portal using its own credential.
|
||||||
A portal session cannot do this — which is what stops somebody you let in from
|
A portal session cannot do this, which is what stops somebody you let in from
|
||||||
letting others in.
|
letting others in.
|
||||||
|
|
||||||
On the instance they appear under **Admin → Users**, badged *Portal*, never
|
On the instance they appear under **Admin → Users**, badged *Portal*, never
|
||||||
@@ -95,7 +94,7 @@ unsure: it does not need the portal's cooperation.
|
|||||||
|
|
||||||
## Running your own portal
|
## Running your own portal
|
||||||
|
|
||||||
The portal is the `index` stack's `hub` service — accounts, the registry of
|
The portal is the `index` stack's `hub` service: accounts, the registry of
|
||||||
connected instances, and the websocket each one dials in on. Two things
|
connected instances, and the websocket each one dials in on. Two things
|
||||||
about it are load-bearing:
|
about it are load-bearing:
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,10 @@
|
|||||||
- [The HTTP API](/code/api/): auth, flows, messages, runs, observability, websocket
|
- [The HTTP API](/code/api/): auth, flows, messages, runs, observability, websocket
|
||||||
- [Remote workers](/code/workers/): fluksio-worker, device labels, artifacts across hosts
|
- [Remote workers](/code/workers/): fluksio-worker, device labels, artifacts across hosts
|
||||||
- [Agents over MCP](/code/agents/): the tool list and the security model
|
- [Agents over MCP](/code/agents/): the tool list and the security model
|
||||||
|
- [Writing a connector](/code/connectors/): packaging a device integration as a node type
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
- [Node types](/reference/node-types/): every built-in type and its settings
|
- [Node types](/reference/node-types/): every built-in type and its settings
|
||||||
- [Payload types](/reference/payload-types/): float, int, str, bool, json, record, list, series, artifact
|
- [Payload types](/reference/payload-types/): float, int, str, bool, json, record, list, series, artifact
|
||||||
- [Configuration](/reference/configuration/): every environment variable
|
- [Configuration](/reference/configuration/): every environment variable
|
||||||
|
- [The connector contract](/reference/connector-contract/): what a connector package must provide
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Anything already exported wins over the file.
|
|||||||
| `DATA_DIR` | `flow-data` (`./.fluksio` via the CLI; `~/.fluksio` with `--global`) | everything below derives from this |
|
| `DATA_DIR` | `flow-data` (`./.fluksio` via the CLI; `~/.fluksio` with `--global`) | everything below derives from this |
|
||||||
| `DATABASE_URL` | SQLite in `DATA_DIR` | any SQLAlchemy URL |
|
| `DATABASE_URL` | SQLite in `DATA_DIR` | any SQLAlchemy URL |
|
||||||
| `FLOWS_DIR` | `$DATA_DIR/flows` | the git repository holding flows |
|
| `FLOWS_DIR` | `$DATA_DIR/flows` | the git repository holding flows |
|
||||||
| `SECRETS_FILE` | `$DATA_DIR/secrets.enc` | encrypted credentials, deliberately outside the repo |
|
| `SECRETS_FILE` | `$DATA_DIR/secrets.enc` | encrypted credentials, kept outside the repo |
|
||||||
| `ALERTS_FILE` | `$DATA_DIR/alerts.json` | channels and rules |
|
| `ALERTS_FILE` | `$DATA_DIR/alerts.json` | channels and rules |
|
||||||
| `PROVISIONERS_FILE` | `$DATA_DIR/provisioners.json` | clusters a machine can be started from; absent means none |
|
| `PROVISIONERS_FILE` | `$DATA_DIR/provisioners.json` | clusters a machine can be started from; absent means none |
|
||||||
| `PANELS_FILE` | `$DATA_DIR/panels.json` | wall-panel pairings |
|
| `PANELS_FILE` | `$DATA_DIR/panels.json` | wall-panel pairings |
|
||||||
@@ -25,7 +25,7 @@ Anything already exported wins over the file.
|
|||||||
| `CLOUD_CONFIG_FILE` | `$DATA_DIR/cloud.json` | the portal enrolment, if any |
|
| `CLOUD_CONFIG_FILE` | `$DATA_DIR/cloud.json` | the portal enrolment, if any |
|
||||||
| `NODE_VENV` | `auto` | which interpreter node code runs on — see below |
|
| `NODE_VENV` | `auto` | which interpreter node code runs on — see below |
|
||||||
|
|
||||||
Set `DATA_DIR` and the rest follow. Set one explicitly and it wins — which is
|
Set `DATA_DIR` and the rest follow. Set one explicitly and it wins, which is
|
||||||
what the container images do to pin everything onto `/data`.
|
what the container images do to pin everything onto `/data`.
|
||||||
|
|
||||||
`NODE_VENV` is the exception, being about an environment rather than a path:
|
`NODE_VENV` is the exception, being about an environment rather than a path:
|
||||||
@@ -43,7 +43,7 @@ An instance that already has a managed venv keeps it on upgrade under
|
|||||||
|
|
||||||
`secrets.enc`, `alerts.json`, `panels.json` and `oauth-key.pem` are written
|
`secrets.enc`, `alerts.json`, `panels.json` and `oauth-key.pem` are written
|
||||||
at runtime. In a container, anything not on a volume lands in the writable
|
at runtime. In a container, anything not on a volume lands in the writable
|
||||||
layer and is lost on the next rebuild — un-pairing every screen and
|
layer and is lost on the next rebuild, un-pairing every screen and
|
||||||
revoking every agent.
|
revoking every agent.
|
||||||
|
|
||||||
## State
|
## State
|
||||||
@@ -54,8 +54,7 @@ An instance that already has a managed venv keeps it on upgrade under
|
|||||||
| `REDIS_PORT` | `6379` | |
|
| `REDIS_PORT` | `6379` | |
|
||||||
|
|
||||||
Flow state is the last value of every message, node memory, and the run queue.
|
Flow state is the last value of every message, node memory, and the run queue.
|
||||||
Redis here is persistence, not a cache — run it with append-only persistence
|
Redis here is persistence, not a cache. Run it with append-only persistence on.
|
||||||
on.
|
|
||||||
|
|
||||||
## Identity and access
|
## Identity and access
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ there to satisfy.
|
|||||||
|
|
||||||
Empty is the useful value: the interface then addresses the API relative to
|
Empty is the useful value: the interface then addresses the API relative to
|
||||||
whichever origin served the page, so one image answers on a hostname, on a
|
whichever origin served the page, so one image answers on a hostname, on a
|
||||||
`http://<host-ip>:<port>`, and through an ssh tunnel alike — and no origin has
|
`http://<host-ip>:<port>`, and through an ssh tunnel alike, and no origin has
|
||||||
to be added to `BACKEND_CORS_ORIGINS`, because there is only one.
|
to be added to `BACKEND_CORS_ORIGINS`, because there is only one.
|
||||||
`docker/compose.lan.yml` builds it that way and puts an `/api` proxy in front
|
`docker/compose.lan.yml` builds it that way and puts an `/api` proxy in front
|
||||||
of the backend to complete it; see
|
of the backend to complete it; see
|
||||||
@@ -115,7 +114,7 @@ o'clock nor the same hour in summer as in winter. Set it to where the
|
|||||||
instance is.
|
instance is.
|
||||||
|
|
||||||
`production` closes `/docs`, `/redoc` and the OpenAPI document, because the
|
`production` closes `/docs`, `/redoc` and the OpenAPI document, because the
|
||||||
schema enumerates every endpoint the instance serves — including the paths
|
schema enumerates every endpoint the instance serves, including the paths
|
||||||
webhook nodes mounted at runtime. It also turns a `changethis` secret from a
|
webhook nodes mounted at runtime. It also turns a `changethis` secret from a
|
||||||
warning into a refusal to start.
|
warning into a refusal to start.
|
||||||
|
|
||||||
@@ -133,8 +132,9 @@ warning into a refusal to start.
|
|||||||
| `ARTIFACT_GC_INTERVAL_S` | `3600` | how often artifact bytes nothing refers to are swept away; 0 never sweeps |
|
| `ARTIFACT_GC_INTERVAL_S` | `3600` | how often artifact bytes nothing refers to are swept away; 0 never sweeps |
|
||||||
| `ARTIFACT_GC_GRACE_S` | `3600` | how long a freshly written artifact is spared, whatever refers to it |
|
| `ARTIFACT_GC_GRACE_S` | `3600` | how long a freshly written artifact is spared, whatever refers to it |
|
||||||
|
|
||||||
The three concurrency limits are also flags on `fluksio serve` — `--max-workers`,
|
The three concurrency limits are also flags on `fluksio serve`
|
||||||
`--max-cascades`, `--max-runs` — as is the card count, `--gpus`. The flags
|
(`--max-workers`, `--max-cascades`, `--max-runs`), as is the card count,
|
||||||
|
`--gpus`. The flags
|
||||||
outrank the file, and the engine says which numbers it started with in its
|
outrank the file, and the engine says which numbers it started with in its
|
||||||
first lines. Each pool size must be at least 1 and the card count at least 0:
|
first lines. Each pool size must be at least 1 and the card count at least 0:
|
||||||
a number below that is refused as a flag error naming it, rather than read as
|
a number below that is refused as a flag error naming it, rather than read as
|
||||||
@@ -153,7 +153,7 @@ several at once cannot each size themselves to the whole machine. Setting
|
|||||||
that default.
|
that default.
|
||||||
|
|
||||||
These two are this machine's figures. An attached worker reports its own when
|
These two are this machine's figures. An attached worker reports its own when
|
||||||
it dials in, and a node goes to whichever machine can grant what it asked for —
|
it dials in, and a node goes to whichever machine can grant what it asked for,
|
||||||
so a GPU on a worker needs no `FLOW_GPUS` here. What every machine has free, and
|
so a GPU on a worker needs no `FLOW_GPUS` here. What every machine has free, and
|
||||||
which nodes are queued, is `GET /api/v1/workers/resources` and the Workers
|
which nodes are queued, is `GET /api/v1/workers/resources` and the Workers
|
||||||
screen. Named sizes live in the database and are `GET /api/v1/flavors`;
|
screen. Named sizes live in the database and are `GET /api/v1/flavors`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The connector contract
|
# The connector contract
|
||||||
|
|
||||||
A connector is the device-facing node class. It talks to something outside the
|
A connector is the device-facing node class. It talks to something outside the
|
||||||
engine — a device, a service, a protocol — and publishes what it finds as
|
engine (a device, a service, a protocol) and publishes what it finds as
|
||||||
ordinary typed messages, so the rest of a flow cannot tell the difference
|
ordinary typed messages, so the rest of a flow cannot tell the difference
|
||||||
between a reading from a heat pump and one from a `print` statement.
|
between a reading from a heat pump and one from a `print` statement.
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class Params(ConnectorNode.Params):
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Booleans render as switches, numbers and strings as inputs. Objects and
|
- Booleans render as switches, numbers and strings as inputs. Objects and
|
||||||
arrays are not rendered — keep settings flat.
|
arrays are not rendered, so keep settings flat.
|
||||||
- A field carrying **`x-secret`** renders as a picker over the stored secrets
|
- A field carrying **`x-secret`** renders as a picker over the stored secrets
|
||||||
and writes a reference, `{"$secret": "name"}`, rather than the value. The
|
and writes a reference, `{"$secret": "name"}`, rather than the value. The
|
||||||
engine resolves it when the node is built, so a credential never lands in
|
engine resolves it when the node is built, so a credential never lands in
|
||||||
@@ -98,13 +98,13 @@ optional `interval`.
|
|||||||
at most that often. A connector should poll at the rate the device is
|
at most that often. A connector should poll at the rate the device is
|
||||||
comfortable with and leave delivery rates to whoever wires it up.
|
comfortable with and leave delivery rates to whoever wires it up.
|
||||||
- A key no port declares is an error, and it fails the whole reading rather
|
- A key no port declares is an error, and it fails the whole reading rather
|
||||||
than the one value — a mistyped metric name is how a training curve goes
|
than the one value. A mistyped metric name is how a training curve goes
|
||||||
missing.
|
missing.
|
||||||
|
|
||||||
### When the device decides what the ports are
|
### When the device decides what the ports are
|
||||||
|
|
||||||
A connector for a device whose readings vary by model — which components a
|
A connector for a device whose readings vary by model (which components a
|
||||||
relay has, which entities were flashed onto a board — cannot know its ports in
|
relay has, which entities were flashed onto a board) cannot know its ports in
|
||||||
advance, and returning everything the device reports would fail on the first
|
advance, and returning everything the device reports would fail on the first
|
||||||
value nobody bound. Narrow the reading to the ports that were declared:
|
value nobody bound. Narrow the reading to the ports that were declared:
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ return {port: value for port, value in reading.items() if port in declared}
|
|||||||
|
|
||||||
`spec.port` is the local, unqualified name and stays that way for the node's
|
`spec.port` is the local, unqualified name and stays that way for the node's
|
||||||
whole life, so the set can be taken fresh each time. Say something in the log
|
whole life, so the set can be taken fresh each time. Say something in the log
|
||||||
when a declared port is not one the device has — once, not once per poll: from
|
when a declared port is not one the device has, once rather than once per poll: from
|
||||||
the canvas a renamed entity and a typo look the same, and both leave a port
|
the canvas a renamed entity and a typo look the same, and both leave a port
|
||||||
silent forever.
|
silent forever.
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ async def poll(self) -> dict[str, Any] | None:
|
|||||||
- Return `None` when there is nothing new.
|
- Return `None` when there is nothing new.
|
||||||
- **Only changed values are published.** A device polled every few seconds
|
- **Only changed values are published.** A device polled every few seconds
|
||||||
usually says the same thing, and every publication wakes everything
|
usually says the same thing, and every publication wakes everything
|
||||||
downstream, so the loop compares against what it last published — what it
|
downstream, so the loop compares against what it last published, meaning what it
|
||||||
actually published, so a publication that failed is retried next tick rather
|
actually published, so a publication that failed is retried next tick rather
|
||||||
than counting as said.
|
than counting as said.
|
||||||
- Raising is not fatal: it is reported as a health problem and retried on the
|
- Raising is not fatal: it is reported as a health problem and retried on the
|
||||||
@@ -182,7 +182,7 @@ async def poll(self) -> dict[str, Any] | None:
|
|||||||
```
|
```
|
||||||
|
|
||||||
`save_artifact` stores the bytes and returns what an `image`, `audio` or
|
`save_artifact` stores the bytes and returns what an `image`, `audio` or
|
||||||
`video` port carries — the media type has to match the port's type. It only
|
`video` port carries: the media type has to match the port's type. It only
|
||||||
works once the node has started, since the store is the engine's and is handed
|
works once the node has started, since the store is the engine's and is handed
|
||||||
over then.
|
over then.
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ async def stop(self, app=None) -> None: ...
|
|||||||
|
|
||||||
- `start` is called when the connector's flow starts, and after every rebuild.
|
- `start` is called when the connector's flow starts, and after every rebuild.
|
||||||
- `stop` is called before a rebuild and when the flow is stopped. **It must be
|
- `stop` is called before a rebuild and when the flow is stopped. **It must be
|
||||||
idempotent** — it is called whether or not `start` succeeded.
|
idempotent**: it is called whether or not `start` succeeded.
|
||||||
- Overriding either means calling `super()` if the polling loop is also wanted.
|
- Overriding either means calling `super()` if the polling loop is also wanted.
|
||||||
- The `app` argument is the FastAPI application, for the rare connector that
|
- The `app` argument is the FastAPI application, for the rare connector that
|
||||||
needs to mount a route. Most ignore it.
|
needs to mount a route. Most ignore it.
|
||||||
@@ -216,7 +216,7 @@ self.report_health("down", str(exc))
|
|||||||
```
|
```
|
||||||
|
|
||||||
Three values, `ok`, `degraded` and `down`, plus an optional detail string.
|
Three values, `ok`, `degraded` and `down`, plus an optional detail string.
|
||||||
Reporting the same status twice is free — only changes are published. A node
|
Reporting the same status twice is free, since only changes are published. A node
|
||||||
reporting `down` is named among its flow's issues and counted on the health
|
reporting `down` is named among its flow's issues and counted on the health
|
||||||
summary on Home; `degraded` means still working, and is not. The polling loop
|
summary on Home; `degraded` means still working, and is not. The polling loop
|
||||||
already reports around `poll()`; a connector managing its own connection should
|
already reports around `poll()`; a connector managing its own connection should
|
||||||
|
|||||||
@@ -3,10 +3,8 @@
|
|||||||
Every type the canvas can place. Each is configured by filling in a form the
|
Every type the canvas can place. Each is configured by filling in a form the
|
||||||
editor generates from its parameter schema, so they all behave the same way.
|
editor generates from its parameter schema, so they all behave the same way.
|
||||||
|
|
||||||
Anything here could be written as a **Function** node — that is what the
|
Anything here can also be written as a **Function** node. These types cover the
|
||||||
function node is for. These exist because the same handful of shapes account
|
shapes that come up most often, as a form to fill in rather than repeated code.
|
||||||
for most of a real instance, and a rule you fill in is easier to read on a
|
|
||||||
canvas, and to change, than five lines of code repeated eighty times.
|
|
||||||
|
|
||||||
`GET /flows/node-types` returns this list with each type's full parameter
|
`GET /flows/node-types` returns this list with each type's full parameter
|
||||||
schema, including any [connectors](#connectors) installed on your instance.
|
schema, including any [connectors](#connectors) installed on your instance.
|
||||||
@@ -40,13 +38,15 @@ A node with *outputs only* subscribes; a node with *inputs* publishes.
|
|||||||
| `qos` | `0` | 0, 1 or 2 |
|
| `qos` | `0` | 0, 1 or 2 |
|
||||||
| `retain` | `false` | on published messages |
|
| `retain` | `false` | on published messages |
|
||||||
| `keepalive` | `60` | seconds |
|
| `keepalive` | `60` | seconds |
|
||||||
|
| `timeout` | `10.0` | seconds before a broker operation is given up on |
|
||||||
|
| `publish_queue_size` | `256` | payloads that may wait for the broker; past this the oldest is dropped and the node reports degraded |
|
||||||
| `json_key` | — | key to lift out of an object payload; one for every port, or `{"port": "key"}` |
|
| `json_key` | — | key to lift out of an object payload; one for every port, or `{"port": "key"}` |
|
||||||
|
|
||||||
A topic may be a filter: `+` matches one level, `#` the rest. Everything a
|
A topic may be a filter: `+` matches one level, `#` the rest. Everything a
|
||||||
filter matches lands on the same port, so use one port per topic where the
|
filter matches lands on the same port, so use one port per topic where the
|
||||||
difference matters.
|
difference matters.
|
||||||
|
|
||||||
`json_key` is for a device that wraps its reading — Victron publishes
|
`json_key` is for a device that wraps its reading: Victron publishes
|
||||||
`{"value": 47}` on every path. Without it, a payload object is unwrapped only
|
`{"value": 47}` on every path. Without it, a payload object is unwrapped only
|
||||||
when it happens to carry the port's own name as a key.
|
when it happens to carry the port's own name as a key.
|
||||||
|
|
||||||
@@ -100,10 +100,8 @@ Outputs only makes it a **webhook**: the engine mounts a route at
|
|||||||
written, and the rows come back on the first output port as
|
written, and the rows come back on the first output port as
|
||||||
`{"rows": [{ts, value, field, measurement, tags}], ...}`.
|
`{"rows": [{ts, value, field, measurement, tags}], ...}`.
|
||||||
|
|
||||||
That is what keeps a database node a database node: it holds the credentials
|
A database node holds the credentials and the connection, nothing else.
|
||||||
and the connection and nothing else. Building a query and shaping its rows are
|
Building a query and shaping its rows are Function nodes on either side.
|
||||||
ordinary Function nodes on either side, so a dashboard widget never learns which
|
|
||||||
database answered it.
|
|
||||||
|
|
||||||
### Notification
|
### Notification
|
||||||
|
|
||||||
@@ -126,7 +124,7 @@ thing configured elsewhere.
|
|||||||
|
|
||||||
### Inject
|
### Inject
|
||||||
|
|
||||||
**`inject`** — emit a value on request, on a schedule, or when the flow starts.
|
**`inject`** — emit a value on request, on a timer, or when the flow starts.
|
||||||
|
|
||||||
| Setting | Default | Notes |
|
| Setting | Default | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -138,8 +136,8 @@ thing configured elsewhere.
|
|||||||
| `start_delay` | `1.0` | how long to wait before that first emission |
|
| `start_delay` | `1.0` | how long to wait before that first emission |
|
||||||
|
|
||||||
The scheduler: a `cron` expression here is what makes a flow run by the clock.
|
The scheduler: a `cron` expression here is what makes a flow run by the clock.
|
||||||
It is also the most-placed node in a real instance — mostly as a button
|
It is also the most-placed node in a real instance, mostly as a button someone
|
||||||
someone presses.
|
presses.
|
||||||
|
|
||||||
### Delay & schedule
|
### Delay & schedule
|
||||||
|
|
||||||
@@ -165,7 +163,7 @@ needs a busy cascade slot waits for one.
|
|||||||
!!! note "Not in a batch flow"
|
!!! note "Not in a batch flow"
|
||||||
|
|
||||||
A rate limit holds a value back for a timer to release, and a run has no
|
A rate limit holds a value back for a timer to release, and a run has no
|
||||||
timer — the value would be dropped rather than delayed. Submitting a batch
|
timer, since the value would be dropped rather than delayed. Submitting a batch
|
||||||
flow with a rate-limited port is refused instead.
|
flow with a rate-limited port is refused instead.
|
||||||
|
|
||||||
### Trigger
|
### Trigger
|
||||||
@@ -183,13 +181,13 @@ starts from a value arriving. For a cron tick, see [`inject`](#inject).
|
|||||||
| `passthrough` | `false` | send the incoming value instead of `first` |
|
| `passthrough` | `false` | send the incoming value instead of `first` |
|
||||||
| `wait_port` | — | an input carrying the wait, when it differs per message |
|
| `wait_port` | — | an input carrying the wait, when it differs per message |
|
||||||
|
|
||||||
The shape this exists for: *the door opened — turn the light on, and off again
|
The shape this exists for: *the door opened, so turn the light on, and off
|
||||||
in two minutes unless it opens again.*
|
again in two minutes unless it opens again.*
|
||||||
|
|
||||||
`wait_port` covers the case where how long to wait is itself a value: a
|
`wait_port` covers the case where how long to wait is itself a value: a
|
||||||
rollershutter takes 26 seconds up and 28 down, so the node that decides the
|
rollershutter takes 26 seconds up and 28 down, so the node that decides the
|
||||||
direction says how long to run for as well. A wait of zero or less sends
|
direction says how long to run for as well. A wait of zero or less sends
|
||||||
nothing afterwards — and still cancels whatever the last message scheduled,
|
nothing afterwards, and still cancels whatever the last message scheduled,
|
||||||
which is how a *stop* is commanded exactly once.
|
which is how a *stop* is commanded exactly once.
|
||||||
|
|
||||||
## Logic
|
## Logic
|
||||||
@@ -289,13 +287,12 @@ reloads identically.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `seed` | `0` | |
|
| `seed` | `0` | |
|
||||||
|
|
||||||
Kept as a worked example of numeric logic rather than as a modelling tool. If
|
A worked example of numeric logic, not a modelling tool. To train something,
|
||||||
you are training something, that is a [batch flow](../concepts/runs.md) and a
|
use a [batch flow](../concepts/runs.md) and a Function node.
|
||||||
Function node.
|
|
||||||
|
|
||||||
## Connectors
|
## Connectors
|
||||||
|
|
||||||
Anything else in the palette came from an installed **connector** package — a
|
Anything else in the palette came from an installed **connector** package: a
|
||||||
node type written against a published contract and discovered through the
|
node type written against a published contract and discovered through the
|
||||||
`fluksio.node_types` entry point group. The editor shows which package supplied
|
`fluksio.node_types` entry point group. The editor shows which package supplied
|
||||||
it.
|
it.
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ the state backend, the work queue and the worker protocol unchanged.
|
|||||||
| `str` | a string |
|
| `str` | a string |
|
||||||
| `bool` | exactly `true` or `false` |
|
| `bool` | exactly `true` or `false` |
|
||||||
|
|
||||||
`bool` is an `int` subclass in Python and deliberately not a number here: a flag
|
`bool` is an `int` subclass in Python but is not a number here, so a switch
|
||||||
is not a measurement, and a switch bound to a temperature is a mistake worth
|
cannot bind to a temperature.
|
||||||
catching.
|
|
||||||
|
|
||||||
`NaN` and infinity are refused, wherever they sit — including inside a `json`,
|
`NaN` and infinity are refused, wherever they sit, including inside a `json`,
|
||||||
`record`, `series` or `list`. JSON cannot spell either, so one that travelled
|
`record`, `series` or `list`. JSON cannot spell either, so one that travelled
|
||||||
would come back as a response nobody can parse and a row the database rejects,
|
would come back as a response nobody can parse and a row the database rejects,
|
||||||
a long way from the node that made it. An empty subset or a division with no
|
a long way from the node that made it. An empty subset or a division with no
|
||||||
@@ -43,8 +42,8 @@ Flat named scalars.
|
|||||||
{"title": "Boiler", "body": "Pressure low", "severity": "warning"}
|
{"title": "Boiler", "body": "Pressure low", "severity": "warning"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Nesting is deliberately out: a record that can contain a record is a schema
|
A record cannot contain a record, so its shape is readable from the declaration
|
||||||
language, and the shape stops being readable from the declaration alone.
|
alone.
|
||||||
|
|
||||||
Read by the **Notification** widget. It is also what an alert channel of kind
|
Read by the **Notification** widget. It is also what an alert channel of kind
|
||||||
*dashboard* writes.
|
*dashboard* writes.
|
||||||
@@ -64,7 +63,7 @@ point.
|
|||||||
|
|
||||||
### `series`
|
### `series`
|
||||||
|
|
||||||
Labelled lines of `(timestamp, value)` pairs — what a chart draws.
|
Labelled lines of `(timestamp, value)` pairs, which is what a chart draws.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -91,7 +90,7 @@ A reference to stored bytes.
|
|||||||
{"digest": "sha256:…", "size": 4194304, "media_type": "application/octet-stream", "name": "weights.pt"}
|
{"digest": "sha256:…", "size": 4194304, "media_type": "application/octet-stream", "name": "weights.pt"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Binary payloads — tensors, checkpoints, images — never travel as a message. The
|
Binary payloads (tensors, checkpoints, images) never travel as a message. The
|
||||||
bytes go to a content-addressed store and the message carries this. A
|
bytes go to a content-addressed store and the message carries this. A
|
||||||
thirty-megabyte checkpoint never sits in the state backend, and the reference
|
thirty-megabyte checkpoint never sits in the state backend, and the reference
|
||||||
stays valid wherever the store is reachable from, including on another machine.
|
stays valid wherever the store is reachable from, including on another machine.
|
||||||
@@ -110,14 +109,14 @@ The same reference, narrowed to a kind of media by its `media_type`.
|
|||||||
|
|
||||||
An `audio` port takes `audio/*` and refuses anything else, so a speech
|
An `audio` port takes `audio/*` and refuses anything else, so a speech
|
||||||
recogniser declares what it eats rather than taking any bytes at all and
|
recogniser declares what it eats rather than taking any bytes at all and
|
||||||
finding out. An `artifact` port still accepts all three — media narrows
|
finding out. An `artifact` port still accepts all three: media narrows
|
||||||
artifact, not the other way round.
|
artifact, not the other way round.
|
||||||
|
|
||||||
`meta` is optional and nothing here reads it: sample rates, dimensions and
|
`meta` is optional and nothing here reads it: sample rates, dimensions and
|
||||||
sequence numbers are for whoever consumes the media.
|
sequence numbers are for whoever consumes the media.
|
||||||
|
|
||||||
Bytes still never travel as a message. A camera publishes one reference per
|
Bytes still never travel as a message. A camera publishes one reference per
|
||||||
frame and a microphone one per chunk — which makes a media stream an ordinary
|
frame and a microphone one per chunk, which makes a media stream an ordinary
|
||||||
[streaming port](../concepts/flows.md#streaming-ports), and each frame an
|
[streaming port](../concepts/flows.md#streaming-ports), and each frame an
|
||||||
artifact. What that costs is worth knowing before pointing a camera at it:
|
artifact. What that costs is worth knowing before pointing a camera at it:
|
||||||
|
|
||||||
@@ -143,8 +142,8 @@ next author nothing.
|
|||||||
## Naming a run's output
|
## Naming a run's output
|
||||||
|
|
||||||
Any *run parameter* is also accepted as text, since nobody wants to paste an
|
Any *run parameter* is also accepted as text, since nobody wants to paste an
|
||||||
object into a shell. `@run:<id>.<output>` names what an earlier run produced —
|
object into a shell. `@run:<id>.<output>` names what an earlier run produced,
|
||||||
whatever its type, an `artifact` reference or a `json` config alike — and a bare
|
whatever its type, an `artifact` reference or a `json` config alike. A bare
|
||||||
`sha256:…` digest names content in the artifact store. Both resolve before the
|
`sha256:…` digest names content in the artifact store. Both resolve before the
|
||||||
run starts, so the CLI, the run dialog and a python caller all mean the same
|
run starts, so the CLI, the run dialog and a python caller all mean the same
|
||||||
thing by the same string.
|
thing by the same string.
|
||||||
@@ -171,8 +170,8 @@ Enforced on the server as well as in the editor.
|
|||||||
|
|
||||||
A value that does not match its port's declaration raises on the node that
|
A value that does not match its port's declaration raises on the node that
|
||||||
published it, naming the port and what arrived. It does not get published, and
|
published it, naming the port and what arrived. It does not get published, and
|
||||||
it does not reach anything downstream — a wrong value stopping at its source is
|
it does not reach anything downstream. A wrong value stopping at its source is
|
||||||
much easier to diagnose than one propagating.
|
easier to diagnose than one propagating.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user