Tensor Disk Offload
Objective
Reduce aggregation peak memory for large PyTorch model updates by materializing incoming streamed tensor payloads on disk and resolving them lazily during aggregation.
Scope
Applies to streamed PyTorch tensor payloads handled by
TensorDecomposer.Controlled by
enable_tensor_disk_offloadin a supported receiving workflow/controller config: FedAvg (including FedProx), FedOpt, SCAFFOLD, or Swarm.Default is
False(legacy in-memory behavior).If model updates are converted to NumPy before transport, tensor disk offload is not engaged.
Tensor streaming, ref pass-through, and tensor disk offload are separate
behaviors. The sender’s TensorDownloadable remains memory-backed while it
serves a transport ref. An intermediate process may preserve that ref without
downloading it. Disk offload applies when the receiving aggregation workflow
terminates the ref and downloads its tensor chunks. It does not spool the
trainer’s model or training result to disk at the source, and it does not reduce
the memory required to load or train the model in the trainer.
How To Enable
FedAvg:
nvflare/recipe/fedavg.py->FedAvgRecipe(..., enable_tensor_disk_offload=True)nvflare/app_opt/pt/recipes/fedavg.py-> PT recipe forwards the same flagnvflare/app_common/workflows/fedavg.py->FedAvg(..., enable_tensor_disk_offload=True)PyTorch FedProx uses the same FedAvg recipe and server aggregation path
Other server-controlled workflows:
nvflare/app_opt/pt/recipes/fedopt.py->FedOptRecipe(..., enable_tensor_disk_offload=True)nvflare/app_opt/pt/recipes/scaffold.py->ScaffoldRecipe(..., enable_tensor_disk_offload=True)both require
server_expected_format=ExchangeFormat.PYTORCH; custom configurations can enable the same setting directly onScatterAndGatherorScaffold
Swarm/CCWF:
nvflare/app_opt/pt/recipes/swarm.py-> useSwarmLearningRecipe(aggregation_format=ExchangeFormat.PYTORCH, enable_tensor_disk_offload=True)nvflare/app_common/ccwf/ccwf_job.py-> useSwarmClientConfig(..., enable_tensor_disk_offload=True)for custom Job API configurationsnvflare/app_common/ccwf/swarm_client_ctl.pyowns an offload root on each eligible client because the aggregation role moves between clients, but enables disk materialization only for terminal aggregation downloads
If no active Cell is available, the offload context is not enabled and the runtime falls back to in-memory download.
Data Flow
TensorDownloadable chunks
|
v
TensorDecomposer.download()
- enable_tensor_disk_offload=False -> deserialize in memory
- enable_tensor_disk_offload=True -> write safetensors temp files
|
v
LazyTensorDict
|
v
ViaDownloaderDecomposer.recompose()
|
v
Lazy refs in payload tree
|
+--> aggregator consumes lazy refs (materialize on demand)
Server-controlled FedAvg/FedProx, FedOpt, and SCAFFOLD workflows install their disk-offload setting on the receiving server Cell. Swarm does not enable disk offload globally on its client Cells because the same Cells also receive learner tasks and final-result broadcasts. Instead, the selected aggregation controller puts the setting and its job-scoped root in the terminal result download’s FOBS decode context. The destination therefore does not depend on mutable Cell-wide state, and non-aggregation deliveries remain ordinary in-memory tensors.
Runtime Behavior
FedAvg
In nvflare/app_common/workflows/fedavg.py:
custom aggregators receive
result.paramsas-iswith
enable_tensor_disk_offload=True, lazy refs are passed through directlybuilt-in weighted aggregation materializes per tensor inside
WeightedAggregationHelper.add()and relies on lazy-ref object lifetime / GC for temp-resource cleanup
The built-in weighted path remains lazy-friendly and memory-efficient.
Filter Compatibility
Tensor disk offload produces per-tensor lazy values with a materialize() method. These values are
different from the transport-level LazyDownloadRef objects resolved at the Client API site-filter
boundary. Conventional content filters, including model quantizers and dequantizers, expect concrete
NumPy arrays or PyTorch tensors and are not compatible with tensor disk-offload lazy values.
Do not combine enable_tensor_disk_offload=True with conventional content filters on the same
receiving path. Disable tensor disk offload for that path, move the transformation to an explicit
send/receive endpoint, or use a consumer such as the built-in weighted aggregator that explicitly
materializes each tensor. Supporting filters while retaining bounded-memory disk offload requires a
separate lazy-ref-aware or streaming/per-tensor filter contract.
Swarm/CCWF
ClientAPIExecutor preserves the Cell/FOBS large-payload references between an
external trainer and the CCWF controller. When a training result is sent to a
remote aggregation client, SwarmClientController requests PASS_THROUGH on that
message. The aggregation controller therefore receives refs instead of
materializing the tensors inside the Cell receive callback, and explicitly
resolves them with its job-scoped disk root. A result returned by a local
external trainer also crosses a Cell boundary as a ref and is resolved by the
local aggregation controller with the same disk root. In both cases tensor disk
offload yields lazy tensor refs, and the built-in
InTimeAccumulateWeightedAggregator materializes one tensor at a time.
The Swarm controller owns the decision to preserve or resolve transport refs.
On a non-aggregation client it keeps refs when the local learn executor is
ClientAPIExecutor(execution_mode="external_process"), making the external
trainer their single consumer. If that external-process site is also the selected
aggregator, the controller resolves the task once for its aggregation base model
and passes the same in-memory payload to the trainer. This avoids two consumers
racing a one-receiver download transaction. The controller also resolves tasks
into memory for in_process, attach, and non-ClientAPIExecutor learners.
This conservative fallback supports jobs where sites use different learner
execution modes. Disk-backed aggregation refs remain local to the aggregation
client and are never passed to a learner.
The external trainer process’s Cell is not configured as a disk-offload
receiver. It is the terminal consumer of an incoming learn task, and the Client
API materializes ordinary in-memory tensors for model loading. For an outgoing
training result, TensorDownloadable still holds the tensors in trainer memory
while serving a transport ref. ClientAPIExecutor and the client job preserve
and route that ref; the selected aggregation controller decides whether its
terminal download is materialized on disk. Source-side tensor spooling is not
part of this feature.
If an in-process learner is also the local aggregation client, its own result is already an in-memory object with no transport ref to preserve. That one local contribution remains in memory; remote contributions still use terminal aggregation-CJ disk offload.
SwarmLearningRecipe defaults to NumPy exchange for compatibility. Disk offload
therefore requires aggregation_format=ExchangeFormat.PYTORCH; streamed
NumPy arrays are not handled by TensorDecomposer.
Custom Aggregator Contract
When a custom aggregator is used, payload params may contain lazy refs (duck-typed object with materialize()).
Custom aggregators are responsible for:
materializing refs when tensor math is required
releasing lazy-ref object references after use so temp resources can be reclaimed
Temp File Lifecycle
Each workflow creates a job-scoped offload root (
nvflare_tensor_offload_<job>_*), with safetensors download directories beneath it.Temp dir selection follows Python
tempfilebehavior (TMPDIR/ OS default, typically/tmp).In containerized deployments,
/tmpmay be tmpfs (RAM-backed); setTMPDIRto a disk-backed mount to realize memory offload benefits.LazyTensorDictowns a shared_TempDirRef; each lazy ref keeps this reference alive.Lazy download directories are reclaimed when their refs are released, with GC as a fallback.
FedAvg-style workflows restore the prior FOBS context and remove their job-scoped root when the workflow exits.
Swarm keeps its job-scoped root and tensor-forwarding route through workflow finalization. Explicit terminal downloads inherit the run abort signal. At job
END_RUN, Swarm cancels active disk consumers under its root, gives the controller-owned learning and aggregation threads a bounded drain window, then removes the root and its remaining contents. A receiver that abandons an active download also sends a capability-negotiated cancellation to the producer. The producer records that receiver as failed across every ref in the transaction and can release an accepted external-process result source without waiting for its normal transfer timeout. Older producers keep the existing bounded timeout path.These workflow-local cleanup paths require the server/client job process to reach normal teardown. Reclaiming scratch after an abrupt server/client job process death requires launcher-owned storage whose cleanup capability survives process and container boundaries; that lifecycle is outside this workflow-level feature.
Failure Behavior
Download failures trigger
DiskTensorConsumer.download_failed(...), which removes the temp dir.If an owned external trainer dies after its lazy result envelope is accepted, its CJ sends a bounded acknowledged failure notice for the exact source FQCN/reference IDs to the declared terminal receiver. Matching active downloads are interrupted immediately; a bounded tombstone also covers failure notices that arrive just before download startup. FedAvg and Swarm then use their existing materialization/task-error paths instead of retrying the dead source for the normal 600-second streaming timeout.
Mid-transfer receiver cancellation is best effort. If the producer advertised support, it promptly settles the abandoned receiver as failed; otherwise the transaction’s existing receiver/transaction timeout remains the cleanup backstop.
Invalid safetensors payload/header parsing fails fast and bubbles up as a download-consume error.
Existing in-memory download path remains unchanged when offload is disabled.
Design-Relevant Files
nvflare/app_opt/pt/decomposers.pynvflare/app_opt/pt/lazy_tensor_dict.pynvflare/app_opt/pt/tensor_downloader.pynvflare/fuel/utils/fobs/decomposers/via_downloader.pynvflare/app_common/workflows/fedavg.pynvflare/app_common/ccwf/swarm_client_ctl.pynvflare/app_common/ccwf/ccwf_job.pynvflare/recipe/fedavg.pynvflare/app_opt/pt/recipes/swarm.py
Test Coverage
tests/unit_test/app_common/workflow/fedavg_test.pytests/unit_test/app_common/ccwf/test_swarm_tensor_disk_offload.pytests/unit_test/recipe/swarm_recipe_test.pytests/unit_test/app_opt/pt/test_lazy_tensor_dict.pytests/unit_test/app_opt/pt/test_disk_tensor_consumer.pytests/unit_test/app_common/aggregators/weighted_aggregation_helper_test.pytests/unit_test/private/fed/server/server_runner_test.pytests/stress_test/fedavg_large_model/fedavg_stress_test.py