Create a FLIP app from a Flower app
Warning
This page assumes you are familiar with the Federated Learning Nodes component page and with the project / model lifecycle described in Common user functions. Before starting, you must already have: a FLIP account with the researcher role, an approved project with a saved cohort query, and a model created under that project (so you have a flip-model-id).
This page walks through the code changes required to adapt a stock Flower app (for example, one copied from the official Flower quickstart examples or pulled from the internal FLIP app hub) so that it can run on FLIP. The walkthrough covers the four first-class SDK calls a FLIP-compatible app needs:
flip.update_status(...)to signal model lifecycle transitions to the central hubflip.get_dataframe(...)to retrieve the project’s cohort DataFrameflip.get_by_accession_number(...)to pull imaging resources for each accessionflip.send_metrics(...)to push per-round training metrics back to the FLIP UI
A full, runnable reference is available in the flip-fl-base-flower repository:
src/standard/app/— a minimal training-onlyServerApp/ClientApptemplate.tutorials/monai/— a MONAI spleen-segmentation example that exercises every SDK call covered below.
Starting point
A Flower app that FLIP can run is just a standard Flower project with a pyproject.toml that declares a ServerApp and a ClientApp. The minimum folder layout is:
my-flower-app/
├── app/
│ ├── __init__.py
│ ├── client_app.py
│ └── server_app.py
└── pyproject.toml
The pyproject.toml wires the two entry points together:
[tool.flwr.app.components]
serverapp = "app.server_app:app"
clientapp = "app.client_app:app"
If you are starting from scratch, copy src/standard/ from flip-fl-base-flower as a template — it already contains the minimum FLIP integration described below.
Note
The flip-utils package (published on PyPI, imported as flip) is pre-installed in every FLIP FL node image. You only need to declare it as a dependency in your pyproject.toml for local development — you do not have to vendor or install it at run time.
ServerApp: reporting model lifecycle status
On the server side, the FLIP integration is a single object (FLIP) injected into the @app.main() function plus four status transitions around strategy.start(...).
Imports
from flip import FLIP
from flip.constants.flip_constants import ModelStatus
from flwr.app import ArrayRecord, Context
from flwr.serverapp import Grid, ServerApp
from flwr.serverapp.strategy import FedAvg
Injecting the FLIP client
Add flip: FLIP = FLIP() as a default argument to the @app.main() callable. A fresh FLIP instance picks up its central-hub connection details from the environment that the FLIP-managed SuperLink provides at run time, so no additional configuration is needed.
app = ServerApp()
@app.main()
def main(grid: Grid, context: Context, flip: FLIP = FLIP()) -> None:
...
Reading the model ID
Every FLIP-submitted run receives a flip-model-id key in context.run_config. This is the ID to pass to every flip.* call that updates central-hub state.
run_config = context.run_config
num_rounds = int(run_config.get("num-server-rounds", 1))
model_id = run_config.get("flip-model-id")
Note
Declare flip-model-id in [tool.flwr.app.config] of your pyproject.toml as a placeholder (for example, flip-model-id = "uuid"). Flower only allows a flwr run caller — including FLIP’s FL API — to override keys that are already declared. The value you declare locally is irrelevant; FLIP injects the real model UUID at submit time.
The four lifecycle transitions
Wrap your training entry point with four update_status calls. These drive the progress bar on the model page in the FLIP UI (see View Results).
flip.update_status(model_id, ModelStatus.INITIATED) # after config is read
model = get_model()
flip.update_status(model_id, ModelStatus.PREPARED) # after initial weights are built
arrays = ArrayRecord(model.state_dict())
strategy = FedAvg(fraction_train=1.0, fraction_evaluate=0.0)
strategy.start(grid=grid, initial_arrays=arrays, num_rounds=num_rounds)
flip.update_status(model_id, ModelStatus.TRAINING_STARTED) # after strategy.start returns
flip.update_status(model_id, ModelStatus.RESULTS_UPLOADED) # after post-training finalisation
Warning
Forgetting the final ModelStatus.RESULTS_UPLOADED transition will leave the model indefinitely in the “training” state in the FLIP UI even though execution has ended.
Full minimal example
Reproduced from flip-fl-base-flower/src/standard/app/server_app.py:
from flip import FLIP
from flip.constants.flip_constants import ModelStatus
from flwr.app import ArrayRecord, Context
from flwr.serverapp import Grid, ServerApp
from flwr.serverapp.strategy import FedAvg
from app.models import get_model
app = ServerApp()
@app.main()
def main(grid: Grid, context: Context, flip: FLIP = FLIP()) -> None:
run_config = context.run_config
num_rounds = int(run_config.get("num-server-rounds"))
flip_model_id = run_config.get("flip-model-id")
flip.update_status(flip_model_id, ModelStatus.INITIATED)
model = get_model()
flip.update_status(flip_model_id, ModelStatus.PREPARED)
arrays = ArrayRecord(model.state_dict())
strategy = FedAvg(fraction_train=1.0, fraction_evaluate=0.0)
strategy.start(grid=grid, initial_arrays=arrays, num_rounds=num_rounds)
flip.update_status(flip_model_id, ModelStatus.TRAINING_STARTED)
flip.update_status(flip_model_id, ModelStatus.RESULTS_UPLOADED)
Strategy considerations
Most stock Flower strategies (FedAvg, FedProx, and the other built-ins) work unchanged on FLIP because the FLIP touchpoints live in the ServerApp callable, not in the strategy itself. You can pick any strategy the flwr.serverapp.strategy module exposes.
If you subclass a strategy to add custom aggregation or post-round hooks, the flip and model_id values bound inside main() are free to close over: pass them into the subclass constructor and call flip.update_status or flip.send_metrics from aggregation callbacks as needed.
class MyStrategy(FedAvg):
def __init__(self, *args, flip: FLIP, model_id: str, **kwargs):
super().__init__(*args, **kwargs)
self._flip = flip
self._model_id = model_id
def aggregate_train(self, server_round, replies):
result = super().aggregate_train(server_round, replies)
# push any custom server-side signals here
return result
ClientApp: fetching the FLIP DataFrame
On the client side, the cohort DataFrame is fetched once at the start of each training round via flip.get_dataframe. The call runs against the project’s saved cohort query (see Create Cohort Query) and returns a pandas DataFrame containing at minimum an accession_id column plus whatever other columns the SQL SELECT projected.
Minimal in-line version
from flip import FLIP
flip_client = FLIP()
project_id = context.run_config["flip-project-id"]
query = context.run_config.get("flip-cohort-query", "*")
df = flip_client.get_dataframe(project_id=project_id, query=query)
Wrapper pattern (from the MONAI tutorial)
The MONAI tutorial bundles the FLIP client with cohort metadata into a small helper class. This makes it easier to thread data-loading state through the @app.train() callable:
import logging
from flip import FLIP
class FLIP_BASE:
def __init__(self):
self.project_id = ""
self.query = ""
self.dataframe = None
self.flip = FLIP()
self.logger = logging.getLogger(self.__class__.__name__)
self.logger.setLevel(logging.INFO)
Then inside the ClientApp:
flip_utils = FLIP_BASE()
flip_utils.project_id = run_config.get("flip-project-id")
flip_utils.query = run_config.get("flip-cohort-query", "*")
flip_utils.dataframe = flip_utils.flip.get_dataframe(
project_id=flip_utils.project_id,
query=flip_utils.query,
)
Note
Declare flip-project-id and flip-cohort-query in [tool.flwr.app.config] for the same reason you declare flip-model-id: so that FLIP’s FL API can override them at submit time with the project’s real values.
ClientApp: fetching images by accession
Once you have the cohort DataFrame, iterate its accession_id column and call flip.get_by_accession_number to pull the imaging resources for each study. The call returns a pathlib.Path pointing at a local directory containing files named input_*.nii.gz (and, if you also requested ResourceType.SEGMENTATION, label_*.nii.gz).
from pathlib import Path
from flip.constants import ResourceType
for accession_id in df["accession_id"]:
try:
accession_folder_path = flip_client.get_by_accession_number(
project_id,
accession_id,
resource_type=[ResourceType.NIFTI],
)
except Exception as err:
print(f"Could not get image data for {accession_id}: {err}")
continue
for img in accession_folder_path.rglob("input_*.nii.gz"):
seg = str(img).replace("/input_", "/label_")
if not Path(seg).exists():
continue
datalist.append({"image": str(img), "label": seg})
Warning
Always wrap get_by_accession_number in a try / except loop and continue on failure. A single trust may be missing a resource type for a specific accession (for example, a study with imaging but no segmentation), and you should not abort the whole round on one bad accession.
Note
ResourceType is an enum in flip.constants. The samples use ResourceType.NIFTI and ResourceType.SEGMENTATION; you may pass either a single value or a list.
ClientApp: sending per-round metrics to the hub
Per-round, per-client metrics are pushed to the central hub with flip.send_metrics. This is what populates the graphs on the model page (see the “Metrics” section of Common user functions). This push is independent of any MetricRecord you return inside the Flower Message — the MetricRecord is for in-network aggregation by the strategy, while send_metrics is for FLIP UI surfacing.
import os
client_name = os.getenv("SUPERNODE_NAME", "unknown_client")
model_id = run_config.get("flip-model-id")
# global_round from server is 1-based; convert to 0-based for local epoch arithmetic
global_round = int(msg.content["config"]["server-round"]) - 1
for epoch in range(local_epochs):
train_loss = train_func(...)
val_dice, val_loss = validate_func(...)
round_num = global_round * local_epochs + epoch + 1
flip_utils.flip.send_metrics(client_name, model_id, label="TRAIN_LOSS", value=train_loss, round=round_num)
flip_utils.flip.send_metrics(client_name, model_id, label="VAL_LOSS", value=val_loss, round=round_num)
flip_utils.flip.send_metrics(client_name, model_id, label="VAL_DICE", value=val_dice, round=round_num)
Warning
SUPERNODE_NAME must match the trust name registered in the central-hub database exactly, otherwise metrics will be recorded against unknown_client and will not be attributable to the site in the FLIP UI. The FLIP-provisioned SuperNode images set SUPERNODE_NAME for you; if you are running locally you must export it yourself.
Note
Use stable, upper-case label strings (TRAIN_LOSS, VAL_LOSS, VAL_DICE are the conventions used in the MONAI tutorial) so that metrics from repeated runs line up on the same chart in the UI.
Wiring it up via pyproject.toml
The pyproject.toml of a FLIP-compatible Flower app needs three things beyond a normal Flower project:
flip-utilsdeclared as a dependency.A
[tool.flwr.app.config]block declaring the FLIP run-config keys, even if the values are placeholders.Training hyperparameters (
num-server-rounds,local-epochs, etc.) declared in the same block so you can override them viaflwr run . --run-config "key=value"locally.
Abridged from flip-fl-base-flower/tutorials/monai/pyproject.toml:
[project]
name = "quickstart-monai"
version = "1.0.0"
dependencies = [
"flip-utils>=0.1.2",
"flwr[simulation]>=1.26.1",
# ... your model-framework deps (monai, torch, nibabel, ...)
]
[tool.flwr.app]
publisher = "flwrlabs"
[tool.flwr.app.components]
serverapp = "app.server_app:app"
clientapp = "app.client_app:app"
[tool.flwr.app.config]
num-server-rounds = 3
local-epochs = 1
learning-rate = 1e-4
batch-size = 2
# FLIP-injected keys — declare placeholders so FLIP can override at submit time
flip-model-id = "uuid"
flip-project-id = "uuid"
flip-cohort-query = "*"
Submitting the app to FLIP
Once your app runs locally (see the next section), upload it through the FLIP UI’s model page the same way you would upload a FLARE app. FLIP validates the required files for a Flower app (which differ from those required for a FLARE app — see the “Model Files” subsection of Common user functions and the Federated Learning Nodes page for the canonical list) and then lets you click Initiate Training.
At submit time, the FLIP FL API:
Injects
flip-model-id,flip-project-id, andflip-cohort-queryinto your app’s run config.Sets
SUPERNODE_NAMEon each participating trust’s SuperNode container.Starts the
ServerAppon the Central Hub’s SuperLink and theClientAppon each approved trust’s SuperNode.
Local testing before upload
From inside your app’s root directory:
pip install -e .
flwr run .
The MONAI tutorial also supports offline smoke-tests by pointing the FLIP helpers at a local CSV + NIfTI directory via the DEV_DATAFRAME and DEV_IMAGES_DIR environment variables:
DEV_DATAFRAME="../../data/spleen/sample_get_dataframe_response.csv" \
DEV_IMAGES_DIR="../../data/spleen/accession-resources" \
flwr run .
See flip-fl-base-flower/tutorials/monai/README.md for the full local-run instructions.
Common pitfalls
Missing ``RESULTS_UPLOADED``. Forgetting the final
flip.update_status(model_id, ModelStatus.RESULTS_UPLOADED)call leaves the model stuck on “training” in the UI.Wrong ``SUPERNODE_NAME``. Metrics pushed with a name that does not match the trust’s central-hub registration land under
unknown_clientand will not appear on the per-site chart.Undeclared run-config keys.
flwr run(and therefore FLIP’s FL API) can only override keys already declared in[tool.flwr.app.config]. Declaringflip-model-id,flip-project-id, andflip-cohort-querywith placeholder values is mandatory even though the real values are injected by FLIP.Missing ``ResourceType``. If a trust does not have the resource type you requested for a given accession,
get_by_accession_numberwill raise. Always wrap the call intry / exceptand skip the accession on failure so a single bad study does not abort the whole round.