Training /Training hyperparameters

SDK — Training Hyperparameters

TrainingConfig fields, the extra bag, federation knobs, and how to preview the rendered configs.

Overview

TrainingConfig feeds the client executor on every round. It has a typed surface for the common knobs plus an extra bag for anything unusual.

Fixed Fields

Python
from resontech import TrainingConfig

TrainingConfig(
    num_classes=10,        # 0 → let the adapter infer from the manifest
    local_epochs=2,        # epochs per round per worker
    batch_size=32,
    learning_rate=1e-3,
    img_size=224,          # square input resolution
    manifest_file="manifest.ndjson",
)

Every fixed field lands in executors[0].executor.args inside the generated config_fed_client.json, exactly as the web wizard produces them.

The extra Bag

Anything not covered by the fixed fields goes into extra. It's a plain dict[str, Any] and is merged into the executor args verbatim. Keys in extra win over fixed fields if they collide — useful for one-off overrides.

TrainingConfig(
    num_classes=10,
    local_epochs=3,
    extra={
        "weight_decay": 5e-4,
        "mixup_alpha":  0.2,
        "optimizer":    "adamw",
        "grad_clip":    1.0,
    },
)

In the generated config this becomes:

config_fed_client.json
{
  "executors": [{
    "tasks": ["train", "validate", "submit_model"],
    "executor": {
      "path": "custom_client_executor.GenericClientExecutor",
      "args": {
        "data_root":     "/var/tmp/nvflare/data",
        "adapter_module": "model_def",
        "train_fn":      "fl_train_model",
        "manifest_file": "manifest.ndjson",
        "local_epochs":  3,
        "batch_size":    32,
        "learning_rate": 0.001,
        "img_size":      224,
        "num_classes":   10,
        "weight_decay":  0.0005,
        "mixup_alpha":   0.2,
        "optimizer":     "adamw",
        "grad_clip":     1.0
      }
    }
  }]
}

How Your Code Reads Hyperparameters

From model_def.py (the default path)

The generated adapter passes the executor args to your code; you usually don't have to do anything. A change to TrainingConfig.learning_rate shows up in the generated JSON right away.

From a custom executor

If you pass executor=MyExecutor to rt_submit, your class __init__ receives every executor arg as a kwarg. Subclass the FL runtime's Executor base class and declare what you need:

Python
from nvflare.apis.executor import Executor

class MyExecutor(Executor):
    def __init__(
        self,
        local_epochs: int,
        batch_size: int,
        learning_rate: float,
        weight_decay: float = 0.0,   # picks up from extra
        mixup_alpha: float = 0.0,
        **kwargs,                      # everything else passes through
    ):
        super().__init__()
        ...

See Custom classes for the full workflow.

Federation Knobs

FederationConfig drives the server-side orchestration. Fields map 1:1 to the default Scatter-and-Gather workflow + persistor setup.

Python
from resontech import FederationConfig

FederationConfig(
    num_rounds=10,
    min_clients=2,
    wait_time_after_min_received=15,
    heart_beat_timeout=900,
    job_name="my-fl-job",
)
FieldMeaning
num_roundsTotal FL aggregation rounds.
min_clientsMinimum workers that must participate per round before aggregation.
wait_time_after_min_receivedSeconds the server waits after min_clients respond before aggregating (lets stragglers catch up).
heart_beat_timeoutSeconds before a silent client is considered dead.
job_nameDisplay name stored in meta.json.

Federation knobs go into config_fed_server.json and are not customisable at runtime by workers — set them correctly at submit time.

Message quantization

Add quantization="<value>" to FederationConfig to compress per-round model traffic (server → client global weights and client → server trained weights). The SDK installs symmetric ModelQuantizer / ModelDequantizer filters on the train task — other tasks pass through unfiltered.

FederationConfig(
    num_rounds=10,
    min_clients=3,
    job_name="my-job",
    quantization="float16",   # one of the values below, or None to disable
)
ValueWire size vs fp32Needs bitsandbytesNeeds CUDAWhat it is
None100%nonofp32 raw (default). Baseline.
"float16"~50%nonoIEEE half precision via torch cast. Pure-torch, runs on CPU.
"blockwise8"~25%yesyesint8 with per-block scale/zero-point (bitsandbytes).
"float4"~14%yesyes4-bit mini-float (bitsandbytes FP4).
"normfloat4"~14%yesyesNF4 — value bins by Gaussian (QLoRA paper).
"adaquant"variesnonoLearned adaptive per-tensor quantization. CPU-friendly.

Quick pick for your setup:

  • Safe default, no extra deps: "float16" — ~50% bandwidth cut, works on CPU workers.
  • Bandwidth-constrained, GPU workers: "blockwise8" for ~75% bandwidth reduction with minimal convergence impact.
  • Multi-billion-param / LLM federated runs (GPU): "normfloat4".
  • CPU-only worker pool, want smarter compression than fp16: "adaquant".
  • Debugging convergence: None first — quantization is opt-in for a reason.

The blockwise8, float4, and normfloat4 modes require bitsandbytes in your requirements.txt and a CUDA GPU on every worker (the NVFlare filter forcibly .cuda()s tensors). These modes also need a GPU on the FL server, which the default central server doesn't have. Contact Support before you use them. bf16 is not a valid value here despite showing up in NVIDIA marketing material.

Inspecting the Rendered Configs Before Submit

Always a good idea when tweaking hyperparameters:

Python
from resontech.rt.builder import RTJobBuilder

builder = RTJobBuilder(
    training_config=training,
    federation_config=federation,
)
configs = builder.render_configs()
print(configs["client"])
print(configs["server"])

Nothing touches disk — these are plain strings.