Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions docs/design/nvflare_cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,10 @@ Current state: users have been using interactive console shell commands (`cat`,

When client-side log streaming to the server is enabled, client logs are treated as server-side stored job logs. `nvflare job logs` does not connect to client sites directly and does not execute shell commands on client machines. It asks the server for the job log content that the server has locally for the requested site target.

Client log streaming is enabled by the job, not by `nvflare job logs`. The portable job-level pattern is:
Client log streaming is enabled by the job, not by `nvflare job logs`. For Recipe jobs, use:

```python
from nvflare.app_common.logging.job_log_receiver import JobLogReceiver
from nvflare.app_common.logging.job_log_streamer import JobLogStreamer

recipe.job.to_clients(JobLogStreamer())
recipe.job.to_server(JobLogReceiver())
recipe.enable_log_streaming("log.txt")
```

`JobLogStreamer` runs in each client job process, tails the configured job log
Expand Down
4 changes: 2 additions & 2 deletions docs/user_guide/data_scientist_guide/job_recipe.rst
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ Recipe Metadata
---------------

Use ``set_recipe_meta`` to add generated job metadata from a recipe without
mutating ``recipe.job.job.meta_props`` directly. The helper sets one
``JobMetaKey`` metadata entry at a time:
mutating nested generated-job metadata directly. The helper sets one ``JobMetaKey``
metadata entry at a time:

.. code-block:: python

Expand Down
18 changes: 15 additions & 3 deletions docs/user_guide/data_scientist_guide/recipe_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,32 @@ Shared helpers:
Add the default Recipe log-streaming components. If no file names are given,
the recipe streams ``log.json``.

``recipe.enable_tensor_streaming(format="pytorch", tasks=None, tensor_send_timeout=30.0, wait_send_task_data_all_clients_timeout=300.0)``
Add matching tensor-streaming components to the server and all generated
client apps. The format must match the recipe's ``server_expected_format``.

Utility helpers:

``add_experiment_tracking(recipe, tracking_type, tracking_config=None, client_side=False, server_side=True, clients=None)``
Add supported experiment tracking receivers such as TensorBoard, MLflow, or
Weights & Biases. With ``client_side=True``, ``clients`` limits which sites
receive the client-side receiver; call once per site with different
``tracking_config`` values for per-site tracking destinations. Keep tracking
credentials in the executing site's environment or mounted secret files;
never put them in ``tracking_config``.
``tracking_config`` values for per-site tracking destinations. For MLflow,
omitting ``tracking_config`` uses local storage and defaults the experiment
name to ``<recipe-name>-experiment``. The run-name suffix defaults to
``<recipe-name>-Server`` for server-side tracking and
``<recipe-name>-Client`` for client-side tracking. Keep tracking credentials
in the executing site's environment or mounted secret files; never put them
in ``tracking_config``.

``add_cross_site_evaluation(recipe, submit_model_timeout=600, validation_timeout=6000, participating_clients=None)``
Add cross-site evaluation to a training recipe when the recipe/framework
supports it.

``add_final_global_evaluation(recipe, participating_clients=None, validation_timeout=6000)``
Add a final evaluation of a PyTorch recipe's persisted global model without
asking clients to submit their local models.

Per-Site And Metadata Helpers
-----------------------------

Expand Down
28 changes: 19 additions & 9 deletions examples/advanced/experiment-tracking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,17 +213,27 @@ add_experiment_tracking(recipe, "mlflow", tracking_config={
### Client-Side Tracking (Decentralized)

```python
from nvflare.app_opt.tracking.mlflow.mlflow_receiver import MLflowReceiver

# Add tracking to specific clients
for site_name in ["site-1", "site-2"]:
receiver = MLflowReceiver(
tracking_uri=f"file:///tmp/{site_name}/mlruns",
kw_args={"experiment_name": f"{site_name}-experiment"}
)
recipe.job.to(receiver, site_name, id="mlflow_receiver")
# Add the receiver to every client, with no server-side receiver.
# tracking_uri=None creates a local MLflow store in each site's job workspace.
add_experiment_tracking(
recipe,
"mlflow",
tracking_config={
"tracking_uri": None,
"kw_args": {"experiment_name": "local-client-experiment"},
},
client_side=True,
server_side=False,
)
```

If sites need different tracking URIs, credentials, or experiment names, construct
the recipe with per-site client apps (for example,
`per_site_config={"site-1": {}, "site-2": {}}`) and call
`add_experiment_tracking()` once per configuration with `clients=[site_name]`.
The `clients` argument requires that per-site topology; it intentionally rejects a
recipe whose client app is deployed through the default `@ALL` target.

---

## Additional Resources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ This example demonstrates **site-specific (decentralized) MLflow tracking** usin
- Server handles MLflow authentication

**Client-Side** (this example):
- Each client has its own MLflow instance
- Each client has its own local MLflow store
- Site-specific metric tracking
- Each site manages its own MLflow server
- Each site can optionally point at its own MLflow server

## Overview

This example shows how to configure per-client MLflow tracking:

```python
from model import SimpleNetwork

from nvflare.app_opt.pt.recipes.fedavg import FedAvgRecipe
from nvflare.app_opt.tracking.mlflow.mlflow_receiver import MLflowReceiver
from nvflare.apis.analytix import ANALYTIC_EVENT_TYPE
from nvflare.recipe import SimEnv
from nvflare.recipe import SimEnv, add_experiment_tracking

# Create recipe WITHOUT default server-side tracking
recipe = FedAvgRecipe(
Expand All @@ -36,27 +36,34 @@ recipe = FedAvgRecipe(
train_script="client.py",
)

# Add MLflow receiver to each client
for i in range(2):
site_name = f"site-{i + 1}"
tracking_uri = f"file:///tmp/nvflare/jobs/workdir/{site_name}/mlruns"

receiver = MLflowReceiver(
tracking_uri=tracking_uri,
events=[ANALYTIC_EVENT_TYPE], # Listen to LOCAL events (not federated)
kw_args={"experiment_name": f"site-{site_name}-experiment"}
)

recipe.job.to(receiver, site_name, id="mlflow_receiver")
# Add an MLflow receiver to every client, but not to the server. With no
# tracking_config, each receiver creates a local store and uses recipe-derived names.
add_experiment_tracking(
recipe,
"mlflow",
client_side=True,
server_side=False,
)

env = SimEnv(num_clients=2)
run = recipe.execute(env)
```

Only the placement flags are needed here because this example intentionally
tracks on clients. For default server-side MLflow tracking, the complete call is:

```python
add_experiment_tracking(recipe, "mlflow")
```

**Key points**:
- By default, no server-side tracking is enabled (analytics_receiver defaults to None)
- `events=[ANALYTIC_EVENT_TYPE]` - Listen to local events (not `fed.` events)
- Each site gets its own `tracking_uri`
- `client_side=True, server_side=False` keeps metric handling on the clients.
- The helper configures receivers to listen to local events rather than federated `fed.` events.
- Omitting `tracking_config` uses `tracking_uri=None`, which resolves to a separate MLflow store in each client's job workspace.
- The default `experiment_name` and configured run-name suffix are
`fedavg_mlflow_client-experiment` and `fedavg_mlflow_client-Client`; the
receiver adds site and job identifiers to the final run name.
- Sites can use explicit local paths or remote MLflow servers when their deployment requires them.

## Setup

Expand Down Expand Up @@ -87,16 +94,22 @@ python job.py

Since each site has its own MLflow receiver, metrics are stored separately:

With the example's `tracking_uri=None`, a SQLite store is created under each
site's job-result directory as `<site-workspace>/<job-id>/mlflow.db`. Under the
default simulation workspace, use `find` to locate the generated database:

### View Site-1 Metrics:
```bash
mlflow ui --backend-store-uri /tmp/nvflare/jobs/workdir/site-1/mlruns
find /tmp/nvflare/jobs/workdir/fedavg_mlflow_client/site-1 -type f -name mlflow.db
mlflow ui --backend-store-uri sqlite:////absolute/path/to/site-1/mlflow.db
```

Open browser to `http://localhost:5000`

### View Site-2 Metrics:
```bash
mlflow ui --backend-store-uri /tmp/nvflare/jobs/workdir/site-2/mlruns --port 5001
find /tmp/nvflare/jobs/workdir/fedavg_mlflow_client/site-2 -type f -name mlflow.db
mlflow ui --backend-store-uri sqlite:////absolute/path/to/site-2/mlflow.db --port 5001
```

Open browser to `http://localhost:5001`
Expand Down Expand Up @@ -166,29 +179,81 @@ To avoid confusion:
### Change Tracking Location

```python
tracking_uri = f"file:///my/custom/path/{site_name}/mlruns"
add_experiment_tracking(
recipe,
"mlflow",
tracking_config={"tracking_uri": "file:///my/custom/path/mlruns"},
client_side=True,
server_side=False,
)
```

In a distributed deployment, the same local path is resolved independently on
each site. In simulation, all clients share the host filesystem, so use the
default `tracking_uri=None` behavior or configure distinct paths if the stores
must remain physically separate.

For different configuration at each site, construct the recipe with per-site
client apps and then target each site explicitly:

```python
sites = ["site-1", "site-2"]
recipe = FedAvgRecipe(
...,
per_site_config={site: {} for site in sites},
)

for site in sites:
add_experiment_tracking(
recipe,
"mlflow",
tracking_config={
"tracking_uri": f"file:///my/custom/path/{site}/mlruns",
"kw_args": {"experiment_name": f"{site}-experiment"},
},
client_side=True,
server_side=False,
clients=[site],
)
```

Targeted `clients=[...]` placement requires `per_site_config` when the recipe is
constructed; it cannot split an existing `@ALL` client app after the fact.

### Add Experiment Tags

```python
receiver = MLflowReceiver(
tracking_uri=tracking_uri,
events=[ANALYTIC_EVENT_TYPE],
kw_args={
"experiment_name": f"{site_name}-experiment",
"run_name": f"{site_name}-run-001",
"experiment_tags": {"site": site_name, "privacy": "local"},
}
add_experiment_tracking(
recipe,
"mlflow",
tracking_config={
"tracking_uri": None,
"kw_args": {
"experiment_name": "local-client-experiment",
"run_name": "client-run-001",
"experiment_tags": {"privacy": "local"},
},
},
client_side=True,
server_side=False,
)
```

### Remote MLflow Server

```python
tracking_uri = "http://mlflow-server.site-1.local:5000"
add_experiment_tracking(
recipe,
"mlflow",
tracking_config={"tracking_uri": "http://mlflow-server.local:5000"},
client_side=True,
server_side=False,
)
```

If each site uses a different server or credentials, use the per-site pattern
above and pass the appropriate configuration with `clients=[site]`.

---

## Additional Resources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,11 @@ def define_parser():
train_script="client.py",
)

# Add MLflow tracking to all clients (client-side only, no server aggregation)
# Each client will track its own local metrics independently
# Add MLflow tracking to all clients (client-side only, no server aggregation).
# With no tracking_config, each client uses a local store and recipe-derived names.
add_experiment_tracking(
recipe,
"mlflow",
tracking_config={
"tracking_uri": None, # Will auto-configure per site
"kw_args": {
"experiment_name": "nvflare-fedavg-experiment",
"run_name": "nvflare-fedavg-client",
},
},
client_side=True,
server_side=False,
)
Expand Down
10 changes: 7 additions & 3 deletions examples/advanced/experiment-tracking/wandb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,13 @@ add_experiment_tracking(recipe, "wandb", tracking_config=wandb_config)

```python
# Each client logs to its own WandB run
for site_name in ["site-1", "site-2"]:
receiver = WandBReceiver(**client_config)
recipe.job.to(receiver, site_name, id="wandb_receiver")
add_experiment_tracking(
recipe,
"wandb",
tracking_config=client_config,
client_side=True,
server_side=False,
)
```

**Event Flow**:
Expand Down
2 changes: 1 addition & 1 deletion examples/advanced/kaplan-meier-he/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def main():
)

# Export job
recipe.job.export_job(args.job_dir)
recipe.export(args.job_dir)

# Run recipe
if args.startup_kit_location:
Expand Down
5 changes: 2 additions & 3 deletions examples/advanced/recipe-k8s/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,8 @@ def create_recipe(args: argparse.Namespace) -> FedAvgRecipe:
# The client training script and the server-side PyTorch persistor both
# import the model definition, so bundle it into every generated app.
model_path = str(Path(__file__).with_name("model.py"))
recipe.job.add_file_to_server(model_path)
for site_name in client_sites:
recipe.job.add_file_to(model_path, site_name)
recipe.add_server_file(model_path)
recipe.add_client_file(model_path, clients=list(client_sites))

# Scheduler-facing resource requirements stay separate from Kubernetes
# container settings. K8sJobLauncher maps num_of_gpus to nvidia.com/gpu.
Expand Down
9 changes: 5 additions & 4 deletions examples/advanced/tensor-stream/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,15 @@ This configuration is shared between standalone training and federated learning,

### Tensor Streaming Setup

The job uses `TensorServerStreamer` and `TensorClientStreamer` components:
Enable the matching server and client tensor-streaming components through the recipe:

```python
recipe.job.to_server(TensorServerStreamer(), "tensor_server_streamer")
recipe.job.to_clients(TensorClientStreamer(), "tensor_client_streamer")
recipe.enable_tensor_streaming()
```

These streamers handle the efficient transmission of large model tensors between server and clients.
The helper also accepts `format`, `tasks`, `tensor_send_timeout`, and
`wait_send_task_data_all_clients_timeout` when the defaults need to be adjusted.
It applies the shared settings consistently to both sides.

## Tensor Streaming Components

Expand Down
5 changes: 1 addition & 4 deletions examples/advanced/tensor-stream/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
from model import get_model

from nvflare.app_opt.pt.recipes.fedavg import FedAvgRecipe
from nvflare.app_opt.tensor_stream.client import TensorClientStreamer
from nvflare.app_opt.tensor_stream.server import TensorServerStreamer
from nvflare.client.config import ExchangeFormat
from nvflare.recipe import SimEnv, add_experiment_tracking

Expand Down Expand Up @@ -52,8 +50,7 @@ def main():
)
add_experiment_tracking(recipe, tracking_type="tensorboard")

recipe.job.to_server(TensorServerStreamer(), "tensor_server_streamer")
recipe.job.to_clients(TensorClientStreamer(), "tensor_client_streamer")
recipe.enable_tensor_streaming()
Comment thread
nvkevlu marked this conversation as resolved.

env = SimEnv(num_clients=n_clients)
run = recipe.execute(env)
Expand Down
Loading
Loading