diff --git a/docs/source/howto-custom-algorithm.rst b/docs/source/howto-custom-algorithm.rst new file mode 100644 index 00000000..559f97b7 --- /dev/null +++ b/docs/source/howto-custom-algorithm.rst @@ -0,0 +1,158 @@ +.. _howto-custom-algorithm: + +How to: implement a custom algorithm +==================================== + +Built-in algorithms cover the classical methods; research often requires +variants. This guide implements a complete algorithm — a FedProx +variant with named parameter exchange and optional variance reduction — +walking through every file you need and every hook you can override. +The running example is ``test-files/custom_alg.py``; the code shown +below is included from that file, so it is exercised in CI (the CLI runs +it via ``test-files/custom_conf.yml``). + +The one-sentence model: **an algorithm is two config classes plus hook +overrides on the Server and Client classes** — the training loop, the +communication schedule scaffolding, logging, evaluation and the parallel +executors all stay untouched (:doc:`concepts` explains why). + +When is a custom algorithm needed? +---------------------------------- + +- your algorithm changes **what clients compute** locally (new local + objective, extra local state) → override client hooks; +- it changes **what is exchanged or how replies are aggregated** → + override server hooks; +- it needs **new hyperparameters** → add config fields. + +If you only need different *values* for existing hyperparameters, no +code is needed — use the built-in algorithm's config. + +The config classes +------------------ + +Two pydantic models, one for each side. New fields become CLI/CLI-YAML +options automatically; ``server_config_kw_doc`` / ``client_config_kw_doc`` +append the inherited fields' docs so the API reference stays complete: + +.. literalinclude:: ../../test-files/custom_alg.py + :language: python + :start-after: # --- begin: configs --- + :end-before: # --- end: configs --- + +The ``model_validator`` here fixes the local optimizer from the ``vr`` +flag — config-level invariants belong in validators, so invalid +combinations fail at construction, not mid-run. + +The server hooks +---------------- + +The server customizes the **downlink** (``communicate``) and the +**aggregation** (``update``). Everything else — sampling, the round +loop, metrics — is inherited: + +.. literalinclude:: ../../test-files/custom_alg.py + :language: python + :start-after: # --- begin: server --- + :end-before: # --- end: server --- + +Points worth noting: + +- ``communicate(target)`` builds a typed :class:`~fl_sim.messages.ServerToClient` + message; ``extras`` carries algorithm-specific payloads (here, + server-side gradients for variance reduction) alongside the model. +- ``update()`` runs on the main thread after the sampled clients' + replies arrive in ``self._received_messages`` — in sampled-client + order, which is what keeps aggregation bitwise identical across + :doc:`parallel_execution` backends. +- ``client_cls`` and ``config_cls`` wire the custom classes together; + ``@register_algorithm()`` (shown above each class) makes the algorithm + usable by name. + +The client hooks +---------------- + +The client customizes the **local training** (``update``) and the +**uplink** (``communicate``): + +.. literalinclude:: ../../test-files/custom_alg.py + :language: python + :start-after: # --- begin: client --- + :end-before: # --- end: client --- + +The most important contract on this page is in ``communicate``: it +**builds and returns** a :class:`~fl_sim.messages.ClientToServer` and +touches nothing but the client's own state. The framework sends the +message and maintains the communication counters. That is what makes +the hook safe to run inside a worker thread when +:doc:`parallel execution ` is on — a hook that +reached into server state would only work sequentially. + +Named exchange, by the way +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example exchanges parameters **by name** (a ``model_state`` +mapping from parameter name to tensor, i.e. :data:`~fl_sim.messages.ModelState`) +instead of the legacy flat positional list. Names survive reordering +and partial modules, and the loading side (``accept_parameters``, +``update``) looks parameters up by name. The flat-list style still +works everywhere; prefer names for new algorithms. + +Register and run +---------------- + +``@register_algorithm()`` (applied to all four classes above) puts the +algorithm into the registry; from then on the CLI and YAML matrices +accept it like any built-in: + +.. code-block:: yaml + + algorithm: + name: Custom + server: + num_iters: 100 + vr: true + client: + num_epochs: 5 + +The same four classes work programmatically, exactly like +:doc:`quickstart`: + +.. code-block:: python + + server = CustomServer( + model, dataset, CustomServerConfig(num_iters=5), CustomClientConfig(num_epochs=1) + ) + server.train() + +Safeguards for registered algorithms +------------------------------------ + +Two layers guard a registered algorithm: + +- the **golden regression** suite (``test/test_parallel_equivalence/``) + pins each algorithm's observable trajectory (messages, decisions, + counters, metrics positions) so refactors cannot silently change + behavior; +- the **hook contract test** (``test/test_hook_contract.py``) asserts + that client hooks touch no server state — the parallel-safety + property ``communicate`` above was written for. + +When you add an algorithm to the repository itself, add it to both. + +Legacy hook style +----------------- + +Older code overrides ``communicate(self, target)`` on the *client* and +mutates server state directly. It still works under the sequential +backend (with a ``DeprecationWarning``), but it is incompatible with +parallel backends and will be removed; new algorithms should build and +return the typed message as shown above. + +Next steps +---------- + +- :ref:`howto-custom-dataset` — bring your own data partitioning. +- :ref:`howto-custom-optimizer` — add a local optimizer. +- :doc:`parallel_execution` — the full hook contract and backends. +- :doc:`api/nodes` — the complete Server/Client API. diff --git a/docs/source/howto-custom-dataset.rst b/docs/source/howto-custom-dataset.rst new file mode 100644 index 00000000..5fc6732e --- /dev/null +++ b/docs/source/howto-custom-dataset.rst @@ -0,0 +1,200 @@ +.. _howto-custom-dataset: + +How to: add a custom dataset +============================ + +A federated dataset in fl-sim combines two components: the **raw data** +and a **partitioning scheme** that spreads it across the simulated +clients. This guide covers the complete example in +``test-files/custom_dataset.py`` (a re-implementation of the FedProx +paper's FEMNIST variant, exercised in CI via +``test-files/custom_conf.yml``) and then derives a minimal template +starting from NumPy arrays. + +Choose the base class +--------------------- + +- :class:`~fl_sim.data_processing.fed_dataset.FedVisionDataset` — image + data with the usual ``TensorDataset`` plumbing; what the example uses; +- :class:`~fl_sim.data_processing.fed_dataset.FedDataset` — the fully + abstract base, for anything non-vision. + +The required interface is small: load data in ``_preload``, hand out +dataloaders per client in ``get_dataloader``, and score predictions in +``evaluate``. + +The full example +---------------- + +.. literalinclude:: ../../test-files/custom_dataset.py + :language: python + :start-after: # --- begin: dataset --- + :end-before: # --- end: preload --- + +``_preload`` runs once at construction: resolve the data directory, +declare the defaults the framework queries (client counts, batch size, +the file layout), set the ``criterion``, download if needed, and read +the raw files into memory. fl-sim datasets are **immutable after +construction** — partitioning happens here, once. + +.. literalinclude:: ../../test-files/custom_dataset.py + :language: python + :start-after: # --- begin: dataloader --- + :end-before: # --- end: dataloader --- + +The contract of ``get_dataloader``: given a client index (or ``None`` +for "everything"), return a **pair** of dataloaders ``(train, test)`` +with shuffling enabled — the framework re-requests them every round, +and under ``rng_mode="per_client"`` (:doc:`tutorial-parallel-reproducible`) +their shuffling is what gets re-seeded per client. + +.. literalinclude:: ../../test-files/custom_dataset.py + :language: python + :start-after: # --- begin: evaluate --- + :end-before: # --- end: evaluate --- + +``evaluate`` turns a batch of predictions and labels into the metric +dictionary that lands in the logs (``acc``, ``loss``, ``num_samples``, +... — the same keys :doc:`quickstart` read back). + +.. literalinclude:: ../../test-files/custom_dataset.py + :language: python + :start-after: # --- begin: mirrors-and-models --- + :end-before: # --- end: mirrors-and-models --- + +The remaining properties are conventions rather than requirements: +``mirrors`` / ``url`` feed the download mechanism (fallback URLs tried +in order — see the :meth:`~fl_sim.data_processing.fed_dataset.FedDataset.mirrors` +API for the protocol), ``candidate_models`` declares the models that +fit this data (the CLI's ``model: name:`` picks from here), +``label_map`` names the classes, ``doi`` carries citations. + +The minimal template +-------------------- + +From NumPy arrays to a usable federated dataset. The template below has +been verified by end-to-end execution; three conventions it encodes are +easy to miss and costly to debug: + +- **define your own ``__init__``**: the abstract + :class:`~fl_sim.data_processing.fed_dataset.FedDataset` base has no + constructor of its own (the vision subclass supplies one); accept + ``(datadir, transform, seed, **extra_config)`` and re-seed via + ``set_seed`` so every constructor re-seeds the global RNG exactly + like the built-ins do; +- **the framework requests global loaders too**: ``get_dataloader`` + must handle ``client_idx=None`` ("all clients"), not just per-client + indices; +- **``candidate_models`` should return fl-sim models** (they carry the + ``.device`` attribute the device-allocation step relies on). + +Two ``load_partition_data*`` methods appear required but are a legacy +tff-style bulk-loading API that the training loop never calls — stub +implementations satisfy the abstract interface: + +.. code-block:: python + + import numpy as np + import torch + import torch.utils.data as torchdata + + from fl_sim.models import nn as mnn + from fl_sim.utils.misc import set_seed + from fl_sim.data_processing._register import register_fed_dataset + from fl_sim.data_processing.fed_dataset import FedDataset + + + @register_fed_dataset() + class MyArrays(FedDataset): + __name__ = "MyArrays" + + def __init__(self, datadir=None, transform="none", seed=0, **extra_config): + super().__init__() + self.datadir = datadir + self.transform = transform + self.seed = seed + set_seed(seed) # so that every constructor re-seeds, as the built-ins do + for k, v in extra_config.items(): + setattr(self, k, v) + self._preload() + + def _preload(self, datadir=None): + rng = np.random.default_rng(self.seed) + n_clients = 30 + self._parts = [ # one (features, labels) pair per client + ( + rng.normal(size=(100, 20)).astype(np.float32), + rng.integers(0, 2, size=100).astype(np.int64), + ) + for _ in range(n_clients) + ] + self.DEFAULT_TRAIN_CLIENTS_NUM = n_clients + self.DEFAULT_TEST_CLIENTS_NUM = n_clients + self.DEFAULT_BATCH_SIZE = 32 + self.criterion = torch.nn.CrossEntropyLoss() + + def get_dataloader(self, train_bs=None, test_bs=None, client_idx=None): + parts = self._parts if client_idx is None else [self._parts[client_idx]] + x = np.concatenate([p[0] for p in parts]) + y = np.concatenate([p[1] for p in parts]) + ds = torchdata.TensorDataset(torch.from_numpy(x), torch.from_numpy(y)) + dl = torchdata.DataLoader(ds, batch_size=train_bs or self.DEFAULT_BATCH_SIZE, shuffle=True) + return dl, dl # train and test loaders; a real dataset separates them + + def evaluate(self, probs, truths): + return { + "acc": (probs.argmax(1) == truths).float().mean().item(), + "loss": self.criterion(probs, truths).item(), + "num_samples": probs.shape[0], + } + + # legacy tff-style bulk loaders; the training loop never calls them + def load_partition_data(self, batch_size=None): + raise NotImplementedError + + def load_partition_data_distributed(self, process_id, batch_size=None): + raise NotImplementedError + + @property + def url(self): + return "" # nothing to download + + @property + def candidate_models(self): + # fl-sim models carry a .device attribute the framework relies on + return {"mlp": mnn.MLP(dim_in=20, dim_out=2, ndim=0)} + + @property + def doi(self): + return [] + +Usage +----- + +With the ``@register_fed_dataset()`` decorator applied, the dataset is +constructible by name once its module has been imported (or via the +module *path* as the dataset ``name`` — see +:class:`~fl_sim.data_processing.fed_data_args.FedDataArgs`), and every +built-in works on it unchanged: + +.. code-block:: python + + import my_arrays # the module above; registers the dataset + + from fl_sim.data_processing import FedDataArgs + from fl_sim.algorithms.fedopt import FedAvgClientConfig, FedAvgServer, FedAvgServerConfig + + dataset = FedDataArgs(name="MyArrays")._create_fed_dataset() + server = FedAvgServer( + dataset.candidate_models["mlp"], + dataset, + FedAvgServerConfig(num_iters=5, num_clients=10, clients_sample_ratio=1.0), + FedAvgClientConfig(num_epochs=1, batch_size=32), + ) + server.train() # identical to :doc:`quickstart` from here on + +Next steps +---------- + +- :ref:`howto-custom-algorithm` — the algorithm side of the contract. +- :doc:`api/datasets` — the full FedDataset API reference. diff --git a/docs/source/howto-custom-optimizer.rst b/docs/source/howto-custom-optimizer.rst new file mode 100644 index 00000000..b8e386ec --- /dev/null +++ b/docs/source/howto-custom-optimizer.rst @@ -0,0 +1,75 @@ +.. _howto-custom-optimizer: + +How to: add a custom optimizer +============================== + +The local training step is pluggable: fl-sim keeps its own optimizer +registry, and any optimizer registered there can be selected by name in +the client config. This is the shortest of the three guides; the +complete example is 48 lines (``test-files/custom_optimizer.py``, +exercised in CI via ``test-files/custom_conf.yml``). + +Where optimizers plug in +------------------------ + +fl-sim's local optimizers live in :mod:`fl_sim.optimizers` and +subclass :class:`~fl_sim.optimizers.base.ProxSGD` — the proximal SGD +base that supports the extra local-training knobs federated algorithms +need (proximal terms, variance-reduction buffers passed by +``optimizer.step(...)``, per the custom algorithm in +:ref:`howto-custom-algorithm`). + +Registration makes the optimizer discoverable by name — the same +pattern as algorithms and datasets: + +.. literalinclude:: ../../test-files/custom_optimizer.py + :language: python + :start-after: # --- begin: optimizer --- + :end-before: # --- end: optimizer --- + +This example wraps ``FedProxOptimizer``'s behavior under a new name; +a genuinely new optimizer overrides the ``ProxSGD`` step methods +instead of the constructor. + +Usage +----- + +Registered optimizers are referenced by name in the client config — +programmatically: + +.. code-block:: python + + FedAvgClientConfig(optimizer="CustomOptimizer", lr=1e-2) + +or in a YAML experiment matrix: + +.. code-block:: yaml + + algorithm: + client: + optimizer: CustomOptimizer + +The name is resolved through :func:`~fl_sim.optimizers.get_optimizer` +at client setup (a trailing ``Optimizer`` suffix is optional); an +unregistered name fails there with ``Optimizer ... is not registered``. + +Caveats +------- + +- the optimizer is constructed **once per client per run**; anything + stateful it keeps lives for the whole federated run; +- some algorithms fix the client optimizer in their client-config + validators (e.g. the FedProx family selects ``FedProx`` / + ``FedProx_VR`` from its ``vr`` flag) — for those algorithms the + ``optimizer`` config field is ignored by design; +- the server-side optimizers (FedAdam, FedYogi, … in the FedOpt + family) are a different mechanism — those are selected through the + server config's ``optimizer`` field and are not part of this + registry. + +Next steps +---------- + +- :ref:`howto-custom-algorithm` — when the *algorithm*, not just the + optimizer, is what's new. +- :doc:`api/optimizers` — the optimizer API reference. diff --git a/docs/source/index.rst b/docs/source/index.rst index bdb96ede..5ed85680 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,6 +24,16 @@ Project Links: `GitHub `_ | `gitee , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: fl-sim\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-10-29 00:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: zh_CN \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Generated-By: Babel 2.16.0\n" + +#: ../../source/howto-custom-algorithm.rst:4 +msgid "How to: implement a custom algorithm" +msgstr "操作指南:实现一个自定义算法" + +#: ../../source/howto-custom-algorithm.rst:6 +msgid "" +"Built-in algorithms cover the classical methods; research often requires variants. This guide implements a complete algorithm — a FedProx variant with named parameter exchange and optional variance " +"reduction — walking through every file you need and every hook you can override. The running example is ``test-files/custom_alg.py``; the code shown below is included from that file, so it is " +"exercised in CI (the CLI runs it via ``test-files/custom_conf.yml``)." +msgstr "内置算法覆盖了经典方法,而研究往往需要变体。本指南实现一个完整算法——带按名参数交换与可选方差削减的 FedProx 变体——走遍你需要的每个文件与每个可覆写的钩子。贯穿示例是 ``test-files/custom_alg.py``;下文展示的代码都从该文件引入,因此在 CI 中真实执行(CLI 经 ``test-files/custom_conf.yml`` 运行它)。" + +#: ../../source/howto-custom-algorithm.rst:14 +msgid "" +"The one-sentence model: **an algorithm is two config classes plus hook overrides on the Server and Client classes** — the training loop, the communication schedule scaffolding, logging, evaluation " +"and the parallel executors all stay untouched (:doc:`concepts` explains why)." +msgstr "一句话模型: **算法 = 一对配置类 + Server/Client 类上的钩子覆写** ——训练循环、通信调度脚手架、日志、评估、并行执行器全部原样( :doc:`concepts` 解释了为什么)。" + +#: ../../source/howto-custom-algorithm.rst:20 +msgid "When is a custom algorithm needed?" +msgstr "何时需要自定义算法" + +#: ../../source/howto-custom-algorithm.rst:22 +msgid "your algorithm changes **what clients compute** locally (new local objective, extra local state) → override client hooks;" +msgstr "算法改变了客户端 **本地计算的内容** (新的本地目标、额外的本地状态)→ 覆写客户端钩子;" + +#: ../../source/howto-custom-algorithm.rst:24 +msgid "it changes **what is exchanged or how replies are aggregated** → override server hooks;" +msgstr "算法改变了 **交换的内容或回复的聚合方式** → 覆写服务器钩子;" + +#: ../../source/howto-custom-algorithm.rst:26 +msgid "it needs **new hyperparameters** → add config fields." +msgstr "算法需要 **新的超参数** → 添加配置字段。" + +#: ../../source/howto-custom-algorithm.rst:28 +msgid "If you only need different *values* for existing hyperparameters, no code is needed — use the built-in algorithm's config." +msgstr "如果只是想给既有超参数设不同的 *值* ,不需要写任何代码——直接用内置算法的配置。" + +#: ../../source/howto-custom-algorithm.rst:32 +msgid "The config classes" +msgstr "配置类" + +#: ../../source/howto-custom-algorithm.rst:34 +msgid "" +"Two pydantic models, one for each side. New fields become CLI/CLI-YAML options automatically; ``server_config_kw_doc`` / ``client_config_kw_doc`` append the inherited fields' docs so the API " +"reference stays complete:" +msgstr "两个 pydantic 模型,各管一侧。新字段自动成为 CLI/YAML 选项; ``server_config_kw_doc`` / ``client_config_kw_doc`` 把继承字段的文档追加进来,API 参考得以保持完整:" + +#: ../../source/howto-custom-algorithm.rst:43 +msgid "The ``model_validator`` here fixes the local optimizer from the ``vr`` flag — config-level invariants belong in validators, so invalid combinations fail at construction, not mid-run." +msgstr "这里的 ``model_validator`` 由 ``vr`` 标志固定本地优化器——配置级不变式就该放在 validator 里,让非法组合在构造时失败,而不是跑到一半。" + +#: ../../source/howto-custom-algorithm.rst:48 +msgid "The server hooks" +msgstr "服务器钩子" + +#: ../../source/howto-custom-algorithm.rst:50 +msgid "The server customizes the **downlink** (``communicate``) and the **aggregation** (``update``). Everything else — sampling, the round loop, metrics — is inherited:" +msgstr "服务器定制 **下行** ( ``communicate`` )与 **聚合** ( ``update`` )。其余一切——采样、轮循环、指标——全部继承:" + +#: ../../source/howto-custom-algorithm.rst:59 +msgid "Points worth noting:" +msgstr "值得注意的点:" + +#: ../../source/howto-custom-algorithm.rst:61 +msgid "" +"``communicate(target)`` builds a typed :class:`~fl_sim.messages.ServerToClient` message; ``extras`` carries algorithm-specific payloads (here, server-side gradients for variance reduction) alongside" +" the model." +msgstr "``communicate(target)`` 构建带类型的 :class:`~fl_sim.messages.ServerToClient` 消息;``extras`` 携带算法特有载荷(此处是方差削减用的服务器侧梯度),与模型并列。" + +#: ../../source/howto-custom-algorithm.rst:64 +msgid "" +"``update()`` runs on the main thread after the sampled clients' replies arrive in ``self._received_messages`` — in sampled-client order, which is what keeps aggregation bitwise identical across " +":doc:`parallel_execution` backends." +msgstr "``update()`` 在主线程上运行,此时被采样客户端的回复已进入 ``self._received_messages``——按被采样客户端顺序排列,这正是聚合在所有 :doc:`parallel_execution` 后端上逐位一致的原因。" + +#: ../../source/howto-custom-algorithm.rst:68 +msgid "``client_cls`` and ``config_cls`` wire the custom classes together; ``@register_algorithm()`` (shown above each class) makes the algorithm usable by name." +msgstr "``client_cls`` 与 ``config_cls`` 把自定义类互相接起来; ``@register_algorithm()`` (见各类上方)让算法可按名字使用。" + +#: ../../source/howto-custom-algorithm.rst:73 +msgid "The client hooks" +msgstr "客户端钩子" + +#: ../../source/howto-custom-algorithm.rst:75 +msgid "The client customizes the **local training** (``update``) and the **uplink** (``communicate``):" +msgstr "客户端定制 **本地训练** ( ``update`` )与 **上行** ( ``communicate`` ):" + +#: ../../source/howto-custom-algorithm.rst:83 +msgid "" +"The most important contract on this page is in ``communicate``: it **builds and returns** a :class:`~fl_sim.messages.ClientToServer` and touches nothing but the client's own state. The framework " +"sends the message and maintains the communication counters. That is what makes the hook safe to run inside a worker thread when :doc:`parallel execution ` is on — a hook that " +"reached into server state would only work sequentially." +msgstr "" +"本页最重要的契约在 ``communicate`` 里:它 **构建并返回** 一个 :class:`~fl_sim.messages.ClientToServer`,除客户端自身状态外不碰任何东西。消息的发送与通信计数由框架维护。正因如此,开启 :doc:`并行执行 ` " +"时该钩子才能安全地跑在工作线程里——一个伸手去碰服务器状态的钩子只能在串行模式下工作。" + +#: ../../source/howto-custom-algorithm.rst:92 +msgid "Named exchange, by the way" +msgstr "顺带说一句:按名交换" + +#: ../../source/howto-custom-algorithm.rst:94 +msgid "" +"This example exchanges parameters **by name** (a ``model_state`` mapping from parameter name to tensor, i.e. :data:`~fl_sim.messages.ModelState`) instead of the legacy flat positional list. Names " +"survive reordering and partial modules, and the loading side (``accept_parameters``, ``update``) looks parameters up by name. The flat-list style still works everywhere; prefer names for new " +"algorithms." +msgstr "本示例按 **参数名** 交换参数( ``model_state`` 映射:参数名 → 张量,即 :data:`~fl_sim.messages.ModelState` ),而不是遗留的扁平位置列表。名字能扛住重排序与部分模块,加载侧( ``accept_parameters`` 、 ``update`` )按名查找。扁平列表写法依然处处可用;但新算法请优先用名字。" + +#: ../../source/howto-custom-algorithm.rst:102 +msgid "Register and run" +msgstr "注册与运行" + +#: ../../source/howto-custom-algorithm.rst:104 +msgid "``@register_algorithm()`` (applied to all four classes above) puts the algorithm into the registry; from then on the CLI and YAML matrices accept it like any built-in:" +msgstr "``@register_algorithm()`` (上面四个类都应用了)把算法送进注册表;此后 CLI 与 YAML 矩阵像对待内置算法一样接受它:" + +#: ../../source/howto-custom-algorithm.rst:118 +msgid "The same four classes work programmatically, exactly like :doc:`quickstart`:" +msgstr "同样这四个类也可以编程方式使用,与 :doc:`quickstart` 完全一致:" + +#: ../../source/howto-custom-algorithm.rst:129 +msgid "Safeguards for registered algorithms" +msgstr "注册算法的行为保障" + +#: ../../source/howto-custom-algorithm.rst:131 +msgid "Two layers guard a registered algorithm:" +msgstr "两层机制守护一个注册算法:" + +#: ../../source/howto-custom-algorithm.rst:133 +msgid "" +"the **golden regression** suite (``test/test_parallel_equivalence/``) pins each algorithm's observable trajectory (messages, decisions, counters, metrics positions) so refactors cannot silently " +"change behavior;" +msgstr " **golden 回归** 套件( ``test/test_parallel_equivalence/`` )钉住每个算法的可观测轨迹(消息、决策、计数器、指标位置),重构无法悄悄改变行为;" + +#: ../../source/howto-custom-algorithm.rst:137 +msgid "the **hook contract test** (``test/test_hook_contract.py``) asserts that client hooks touch no server state — the parallel-safety property ``communicate`` above was written for." +msgstr " **钩子契约测试** ( ``test/test_hook_contract.py`` )断言客户端钩子不触碰服务器状态——上面 ``communicate`` 的并行安全性质正是为此而写。" + +#: ../../source/howto-custom-algorithm.rst:141 +msgid "When you add an algorithm to the repository itself, add it to both." +msgstr "把算法合入本仓库时,两层都要接入。" + +#: ../../source/howto-custom-algorithm.rst:144 +msgid "Legacy hook style" +msgstr "遗留钩子写法" + +#: ../../source/howto-custom-algorithm.rst:146 +msgid "" +"Older code overrides ``communicate(self, target)`` on the *client* and mutates server state directly. It still works under the sequential backend (with a ``DeprecationWarning``), but it is " +"incompatible with parallel backends and will be removed; new algorithms should build and return the typed message as shown above." +msgstr "更老的代码在 *客户端* 上覆写 ``communicate(self, target)`` 并直接改动服务器状态。它在串行后端下仍然可用(会带 ``DeprecationWarning`` ),但与并行后端不兼容,终将移除;新算法请按上文所示构建并返回带类型的消息。" + +#: ../../source/howto-custom-algorithm.rst:153 +msgid "Next steps" +msgstr "下一步" + +#: ../../source/howto-custom-algorithm.rst:155 +msgid ":ref:`howto-custom-dataset` — bring your own data partitioning." +msgstr ":ref:`howto-custom-dataset`——带上你自己的数据划分。" + +#: ../../source/howto-custom-algorithm.rst:156 +msgid ":ref:`howto-custom-optimizer` — add a local optimizer." +msgstr ":ref:`howto-custom-optimizer`——添加一个本地优化器。" + +#: ../../source/howto-custom-algorithm.rst:157 +msgid ":doc:`parallel_execution` — the full hook contract and backends." +msgstr ":doc:`parallel_execution`——完整的钩子契约与各后端。" + +#: ../../source/howto-custom-algorithm.rst:158 +msgid ":doc:`api/nodes` — the complete Server/Client API." +msgstr ":doc:`api/nodes`——完整的 Server/Client API。" + +#~ msgid "When do you need this?" +#~ msgstr "什么时候需要它?" + +#~ msgid "How your algorithm is kept honest" +#~ msgstr "你的算法如何被守护" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/howto-custom-dataset.po b/docs/source/locale/zh_CN/LC_MESSAGES/howto-custom-dataset.po new file mode 100644 index 00000000..5bd71aeb --- /dev/null +++ b/docs/source/locale/zh_CN/LC_MESSAGES/howto-custom-dataset.po @@ -0,0 +1,129 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, WEN Hao +# This file is distributed under the same license as the fl-sim package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: fl-sim\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-10-29 00:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: zh_CN \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Generated-By: Babel 2.16.0\n" + +#: ../../source/howto-custom-dataset.rst:4 +msgid "How to: add a custom dataset" +msgstr "操作指南:添加自定义数据集" + +#: ../../source/howto-custom-dataset.rst:6 +msgid "" +"A federated dataset in fl-sim combines two components: the **raw data** and a **partitioning scheme** that spreads it across the simulated clients. This guide covers the complete example in ``test-" +"files/custom_dataset.py`` (a re-implementation of the FedProx paper's FEMNIST variant, exercised in CI via ``test-files/custom_conf.yml``) and then derives a minimal template starting from NumPy " +"arrays." +msgstr "fl-sim 中的联邦数据集由两部分组成: **原始数据** 与把它分散到各模拟客户端的 **划分方案** 。本指南先走完 ``test-files/custom_dataset.py`` 的完整示例(FedProx 论文所用 FEMNIST 变体的复刻,经 ``test-files/custom_conf.yml`` 在 CI 中执行),再从 NumPy 数组出发给出最小模板。" + +#: ../../source/howto-custom-dataset.rst:15 +msgid "Choose the base class" +msgstr "选择基类" + +#: ../../source/howto-custom-dataset.rst:17 +msgid ":class:`~fl_sim.data_processing.fed_dataset.FedVisionDataset` — image data with the usual ``TensorDataset`` plumbing; what the example uses;" +msgstr ":class:`~fl_sim.data_processing.fed_dataset.FedVisionDataset`——图像数据,带常规的 ``TensorDataset`` 管线;示例用的就是它;" + +#: ../../source/howto-custom-dataset.rst:19 +msgid ":class:`~fl_sim.data_processing.fed_dataset.FedDataset` — the fully abstract base, for anything non-vision." +msgstr ":class:`~fl_sim.data_processing.fed_dataset.FedDataset`——完全抽象的基类,适合一切非视觉数据。" + +#: ../../source/howto-custom-dataset.rst:22 +msgid "The required interface is small: load data in ``_preload``, hand out dataloaders per client in ``get_dataloader``, and score predictions in ``evaluate``." +msgstr "必需接口很小:在 ``_preload`` 里加载数据,在 ``get_dataloader`` 里按客户端分发dataloader,在 ``evaluate`` 里为预测打分。" + +#: ../../source/howto-custom-dataset.rst:27 +msgid "The full example" +msgstr "完整示例" + +#: ../../source/howto-custom-dataset.rst:34 +msgid "" +"``_preload`` runs once at construction: resolve the data directory, declare the defaults the framework queries (client counts, batch size, the file layout), set the ``criterion``, download if " +"needed, and read the raw files into memory. fl-sim datasets are **immutable after construction** — partitioning happens here, once." +msgstr "``_preload`` 在构造时只跑一次:解析数据目录、声明框架会查询的默认值(客户端数、批大小、文件布局)、设置 ``criterion``、按需下载,并把原始文件读入内存。fl-sim 数据集 **构造后不可变** ——划分就在这里做,且只做一次。" + +#: ../../source/howto-custom-dataset.rst:45 +msgid "" +"The contract of ``get_dataloader``: given a client index (or ``None`` for \"everything\"), return a **pair** of dataloaders ``(train, test)`` with shuffling enabled — the framework re-requests them " +"every round, and under ``rng_mode=\"per_client\"`` (:doc:`tutorial-parallel-reproducible`) their shuffling is what gets re-seeded per client." +msgstr "``get_dataloader`` 的契约:给定客户端索引(或 ``None`` 表示\"全部\"),返回一对开启洗牌的 dataloader ``(train, test)`` —— 框架每轮都会重新请求它们;在 ``rng_mode=\"per_client\"`` ( :doc:`tutorial-parallel-reproducible` )下,被按客户端重新播种的正是这里的洗牌。" + +#: ../../source/howto-custom-dataset.rst:56 +msgid "``evaluate`` turns a batch of predictions and labels into the metric dictionary that lands in the logs (``acc``, ``loss``, ``num_samples``, ... — the same keys :doc:`quickstart` read back)." +msgstr "``evaluate`` 把一批预测与标签变成写进日志的指标字典( ``acc`` 、 ``loss`` 、 ``num_samples`` 等——正是 :doc:`quickstart` 读回的那些键)。" + +#: ../../source/howto-custom-dataset.rst:65 +msgid "" +"The remaining properties are conventions rather than requirements: ``mirrors`` / ``url`` feed the download mechanism (fallback URLs tried in order — see the " +":meth:`~fl_sim.data_processing.fed_dataset.FedDataset.mirrors` API for the protocol), ``candidate_models`` declares the models that fit this data (the CLI's ``model: name:`` picks from here), " +"``label_map`` names the classes, ``doi`` carries citations." +msgstr "" +"其余 property 是惯例而非硬性要求: ``mirrors`` / ``url`` 供下载机制使用(备选 URL 按序尝试——协议见 :meth:`~fl_sim.data_processing.fed_dataset.FedDataset.mirrors` API); ``candidate_models`` 声明适合本数据的模型(CLI 的 ``model: name:`` " +"从这里选取); ``label_map`` 给类别命名; ``doi`` 携带引用。" + +#: ../../source/howto-custom-dataset.rst:73 +msgid "The minimal template" +msgstr "最小模板" + +#: ../../source/howto-custom-dataset.rst:75 +msgid "From NumPy arrays to a usable federated dataset. The template below has been verified by end-to-end execution; three conventions it encodes are easy to miss and costly to debug:" +msgstr "从 NumPy 数组到可用的联邦数据集。下述模板已通过端到端执行验证;它编码的三个约定容易漏掉、调起来又贵:" + +#: ../../source/howto-custom-dataset.rst:79 +msgid "" +"**define your own ``__init__``**: the abstract :class:`~fl_sim.data_processing.fed_dataset.FedDataset` base has no constructor of its own (the vision subclass supplies one); accept ``(datadir, " +"transform, seed, **extra_config)`` and re-seed via ``set_seed`` so every constructor re-seeds the global RNG exactly like the built-ins do;" +msgstr "" +" **自己定义 ``__init__``** :抽象基类 :class:`~fl_sim.data_processing.fed_dataset.FedDataset` 本身没有构造器(视觉子类提供了一个);接受 ``(datadir, transform, seed, **extra_config)`` 并经 ``set_seed`` 重新播种,让每个构造器都和内置数据集一样重置全局 " +"RNG;" + +#: ../../source/howto-custom-dataset.rst:85 +msgid "**the framework requests global loaders too**: ``get_dataloader`` must handle ``client_idx=None`` (\"all clients\"), not just per-client indices;" +msgstr " **框架也会请求全局 loader** : ``get_dataloader`` 必须处理 ``client_idx=None`` (\"全部客户端\"),不能只处理逐客户端的索引;" + +#: ../../source/howto-custom-dataset.rst:88 +msgid "**``candidate_models`` should return fl-sim models** (they carry the ``.device`` attribute the device-allocation step relies on)." +msgstr " **``candidate_models`` 应返回 fl-sim 模型** (它们带有设备分配步骤依赖的 ``.device`` 属性)。" + +#: ../../source/howto-custom-dataset.rst:91 +msgid "Two ``load_partition_data*`` methods appear required but are a legacy tff-style bulk-loading API that the training loop never calls — stub implementations satisfy the abstract interface:" +msgstr "两个 ``load_partition_data*`` 方法看似必需,实为训练循环从不调用的遗留 tff 风格批量加载 API——桩实现即可满足抽象接口:" + +#: ../../source/howto-custom-dataset.rst:172 +msgid "Usage" +msgstr "使用" + +#: ../../source/howto-custom-dataset.rst:174 +msgid "" +"With the ``@register_fed_dataset()`` decorator applied, the dataset is constructible by name once its module has been imported (or via the module *path* as the dataset ``name`` — see " +":class:`~fl_sim.data_processing.fed_data_args.FedDataArgs`), and every built-in works on it unchanged:" +msgstr "应用 ``@register_fed_dataset()`` 装饰器后,只要模块被导入过,数据集即可按名字构造(或以模块 *路径* 作为数据集 ``name`` ——见 :class:`~fl_sim.data_processing.fed_data_args.FedDataArgs`),且所有内置组件原样可用:" + +#: ../../source/howto-custom-dataset.rst:197 +msgid "Next steps" +msgstr "下一步" + +#: ../../source/howto-custom-dataset.rst:199 +msgid ":ref:`howto-custom-algorithm` — the algorithm side of the contract." +msgstr ":ref:`howto-custom-algorithm`——契约的算法一侧。" + +#: ../../source/howto-custom-dataset.rst:200 +msgid ":doc:`api/datasets` — the full FedDataset API reference." +msgstr ":doc:`api/datasets`——完整的 FedDataset API 参考。" + +#~ msgid "Use it" +#~ msgstr "使用" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/howto-custom-optimizer.po b/docs/source/locale/zh_CN/LC_MESSAGES/howto-custom-optimizer.po new file mode 100644 index 00000000..8b92802c --- /dev/null +++ b/docs/source/locale/zh_CN/LC_MESSAGES/howto-custom-optimizer.po @@ -0,0 +1,103 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, WEN Hao +# This file is distributed under the same license as the fl-sim package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: fl-sim\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-10-29 00:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: zh_CN \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Generated-By: Babel 2.16.0\n" + +#: ../../source/howto-custom-optimizer.rst:4 +msgid "How to: add a custom optimizer" +msgstr "操作指南:添加自定义优化器" + +#: ../../source/howto-custom-optimizer.rst:6 +msgid "" +"The local training step is pluggable: fl-sim keeps its own optimizer registry, and any optimizer registered there can be selected by name in the client config. This is the shortest of the three " +"guides; the complete example is 48 lines (``test-files/custom_optimizer.py``, exercised in CI via ``test-files/custom_conf.yml``)." +msgstr "本地训练步是可插拔的:fl-sim 维护自己的优化器注册表,任何注册过的优化器都能在客户端配置里按名字选用。这是三篇指南里最短的一篇——完整示例只有 48 行( ``test-files/custom_optimizer.py`` ,经 ``test-files/custom_conf.yml`` 在 CI 中执行)。" + +#: ../../source/howto-custom-optimizer.rst:13 +msgid "Where optimizers plug in" +msgstr "优化器接在哪里" + +#: ../../source/howto-custom-optimizer.rst:15 +msgid "" +"fl-sim's local optimizers live in :mod:`fl_sim.optimizers` and subclass :class:`~fl_sim.optimizers.base.ProxSGD` — the proximal SGD base that supports the extra local-training knobs federated " +"algorithms need (proximal terms, variance-reduction buffers passed by ``optimizer.step(...)``, per the custom algorithm in :ref:`howto-custom-algorithm`)." +msgstr "" +"fl-sim 的本地优化器都在 :mod:`fl_sim.optimizers` 里,继承 :class:`~fl_sim.optimizers.base.ProxSGD`——这个近端 SGD 基类支持联邦算法需要的额外本地训练旋钮(近端项、由 ``optimizer.step(...)`` 传入的方差削减缓冲区,用法见 :ref:`howto-custom-algorithm` " +"的自定义算法)。" + +#: ../../source/howto-custom-optimizer.rst:22 +msgid "Registration makes the optimizer discoverable by name — the same pattern as algorithms and datasets:" +msgstr "注册让优化器可按名字发现——与算法、数据集是同一模式:" + +#: ../../source/howto-custom-optimizer.rst:30 +msgid "This example wraps ``FedProxOptimizer``'s behavior under a new name; a genuinely new optimizer overrides the ``ProxSGD`` step methods instead of the constructor." +msgstr "这个例子把 ``FedProxOptimizer`` 的行为换了个名字;真正全新的优化器应覆写 ``ProxSGD`` 的 step 方法而不是构造器。" + +#: ../../source/howto-custom-optimizer.rst:35 +msgid "Usage" +msgstr "使用" + +#: ../../source/howto-custom-optimizer.rst:37 +msgid "Registered optimizers are referenced by name in the client config — programmatically:" +msgstr "注册过的优化器在客户端配置里按名字引用——编程方式:" + +#: ../../source/howto-custom-optimizer.rst:44 +msgid "or in a YAML experiment matrix:" +msgstr "或在 YAML 实验矩阵里:" + +#: ../../source/howto-custom-optimizer.rst:52 +msgid "" +"The name is resolved through :func:`~fl_sim.optimizers.get_optimizer` at client setup (a trailing ``Optimizer`` suffix is optional); an unregistered name fails there with ``Optimizer ... is not " +"registered``." +msgstr "名字在客户端初始化时经 :func:`~fl_sim.optimizers.get_optimizer` 解析(结尾的 ``Optimizer`` 后缀可省略);未注册的名字会在此报错 ``Optimizer ... is not registered`` 。" + +#: ../../source/howto-custom-optimizer.rst:57 +msgid "Caveats" +msgstr "注意事项" + +#: ../../source/howto-custom-optimizer.rst:59 +msgid "the optimizer is constructed **once per client per run**; anything stateful it keeps lives for the whole federated run;" +msgstr "优化器 **每次运行中每个客户端只构造一次** ;它保存的任何状态会贯穿整个联邦运行;" + +#: ../../source/howto-custom-optimizer.rst:61 +msgid "" +"some algorithms fix the client optimizer in their client-config validators (e.g. the FedProx family selects ``FedProx`` / ``FedProx_VR`` from its ``vr`` flag) — for those algorithms the " +"``optimizer`` config field is ignored by design;" +msgstr "某些算法在客户端配置的 validator 里固定了本地优化器(如 FedProx 家族由 ``vr`` 标志选择 ``FedProx`` / ``FedProx_VR`` )——对这些算法, ``optimizer`` 配置字段被设计性地忽略;" + +#: ../../source/howto-custom-optimizer.rst:65 +msgid "" +"the server-side optimizers (FedAdam, FedYogi, … in the FedOpt family) are a different mechanism — those are selected through the server config's ``optimizer`` field and are not part of this " +"registry." +msgstr "服务器侧优化器(FedOpt 家族的 FedAdam、FedYogi 等)是另一套机制——经服务器配置的 ``optimizer`` 字段选择,不在本注册表内。" + +#: ../../source/howto-custom-optimizer.rst:71 +msgid "Next steps" +msgstr "下一步" + +#: ../../source/howto-custom-optimizer.rst:73 +msgid ":ref:`howto-custom-algorithm` — when the *algorithm*, not just the optimizer, is what's new." +msgstr ":ref:`howto-custom-algorithm`——如果新的不只是优化器,而是 *算法本身* 。" + +#: ../../source/howto-custom-optimizer.rst:75 +msgid ":doc:`api/optimizers` — the optimizer API reference." +msgstr ":doc:`api/optimizers`——优化器 API 参考。" + +#~ msgid "Use it" +#~ msgstr "使用" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/index.po b/docs/source/locale/zh_CN/LC_MESSAGES/index.po index b515f516..a199c85d 100644 --- a/docs/source/locale/zh_CN/LC_MESSAGES/index.po +++ b/docs/source/locale/zh_CN/LC_MESSAGES/index.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: fl-sim\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-27 08:46+0800\n" +"POT-Creation-Date: 2026-10-28 08:06+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: zh_CN\n" @@ -27,11 +27,15 @@ msgstr "快速开始" msgid "Tutorials" msgstr "教程" -#: ../../source/index.rst:28 +#: ../../source/index.rst:30 +msgid "How-to guides" +msgstr "操作指南" + +#: ../../source/index.rst:38 msgid "API Reference" msgstr "API 参考" -#: ../../source/index.rst:41 +#: ../../source/index.rst:51 msgid "Advanced Topics" msgstr "进阶主题" @@ -51,18 +55,18 @@ msgstr "" "项目链接: `GitHub `_ | `gitee " "`_" -#: ../../source/index.rst:51 +#: ../../source/index.rst:61 msgid "Indices and tables" msgstr "索引和表格" -#: ../../source/index.rst:53 +#: ../../source/index.rst:63 msgid ":ref:`genindex`" msgstr "" -#: ../../source/index.rst:54 +#: ../../source/index.rst:64 msgid ":ref:`modindex`" msgstr "" -#: ../../source/index.rst:55 +#: ../../source/index.rst:65 msgid ":ref:`search`" msgstr "" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-cli-matrix.po b/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-cli-matrix.po index 1573bbaf..ba28f468 100644 --- a/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-cli-matrix.po +++ b/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-cli-matrix.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: fl-sim \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-27 08:46+0800\n" +"POT-Creation-Date: 2026-10-28 08:06+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: zh_CN\n" @@ -31,17 +31,14 @@ msgid "" "and executes every combination. This tutorial builds a four-run matrix on" " synthetic data and shows where the results land." msgstr "" -"每个实验写一个 Python 脚本是无法扩展的。fl-sim 的命令行接口能用单个 YAML 文件跑起" -"一整个 **实验矩阵** :你把配置网格描述一次,CLI 负责展开并执行每个组合。本教程在" -"合成数据上构建一个四次运行的矩阵,并展示结果落在哪里。" +"每个实验写一个 Python 脚本是无法扩展的。fl-sim 的命令行接口能用单个 YAML 文件跑起一整个 **实验矩阵** " +":你把配置网格描述一次,CLI 负责展开并执行每个组合。本教程在合成数据上构建一个四次运行的矩阵,并展示结果落在哪里。" #: ../../source/tutorial-cli-matrix.rst:12 msgid "" "The YAML syntax reference lives in :doc:`cli`; this page is the workflow " "narrative. Prerequisites: :doc:`tutorial-first-experiment`." -msgstr "" -"YAML 语法参考见 :doc:`cli` ;本页讲的是工作流程。前置阅读:" -":doc:`tutorial-first-experiment` 。" +msgstr "YAML 语法参考见 :doc:`cli` ;本页讲的是工作流程。前置阅读::doc:`tutorial-first-experiment` 。" #: ../../source/tutorial-cli-matrix.rst:16 msgid "The matrix file" @@ -61,9 +58,7 @@ msgstr "三个关键部分:" msgid "" "``strategy.matrix`` — the grid. Every combination of the listed values is" " run once: 2 sampling ratios × 2 seeds = 4 experiments." -msgstr "" -"``strategy.matrix``——网格。列出的值每个组合各跑一次:2 个采样比例 × 2 个种子" -"= 4 个实验。" +msgstr "``strategy.matrix``——网格。列出的值每个组合各跑一次:2 个采样比例 × 2 个种子= 4 个实验。" #: ../../source/tutorial-cli-matrix.rst:28 msgid "" @@ -71,8 +66,8 @@ msgid "" "the CLI substitutes the current combination's value. ``seed`` sits at the" " top level, so it seeds the dataset *and* the server config of that run." msgstr "" -"``${{ matrix. }}``——插值。该模式出现之处,CLI 都会代入当前组合的值。" -"``seed`` 位于顶层,因此它同时为该次运行的数据集 *和* 服务器配置设定种子。" +"``${{ matrix. }}``——插值。该模式出现之处,CLI 都会代入当前组合的值。``seed`` " +"位于顶层,因此它同时为该次运行的数据集 *和* 服务器配置设定种子。" #: ../../source/tutorial-cli-matrix.rst:32 msgid "" @@ -83,10 +78,10 @@ msgid "" ":class:`~fl_sim.data_processing.fed_synthetic.FedSynthetic`) are passed " "straight through." msgstr "" -"其余部分—— ``algorithm`` (名字加服务器/客户端配置)、 ``dataset`` 、 ``model`` ——" -"与你在 Python 里传入的配置(见 :doc:`quickstart` )完全一致,只是改写成 YAML。" -"数据集各自的构造参数( :class:`~fl_sim.data_processing.fed_synthetic.FedSynthetic` " -"的 ``alpha`` 、 ``beta`` 、 ``iid`` 、 ``num_clients`` )原样透传。" +"其余部分—— ``algorithm`` (名字加服务器/客户端配置)、 ``dataset`` 、 ``model`` ——与你在 Python" +" 里传入的配置(见 :doc:`quickstart` )完全一致,只是改写成 YAML。数据集各自的构造参数( " +":class:`~fl_sim.data_processing.fed_synthetic.FedSynthetic` 的 ``alpha`` 、" +" ``beta`` 、 ``iid`` 、 ``num_clients`` )原样透传。" #: ../../source/tutorial-cli-matrix.rst:40 msgid "Run it" @@ -97,9 +92,7 @@ msgid "" "You will see the four runs execute in sequence, each logging its " "combination (``ratio=0.5 seed=0``, ...). Total time: seconds — no " "downloads." -msgstr "" -"你会看到四次运行依次执行,每次都会记录自己的组合(``ratio=0.5 seed=0`` 等)。" -"总耗时:几秒——无下载。" +msgstr "你会看到四次运行依次执行,每次都会记录自己的组合(``ratio=0.5 seed=0`` 等)。总耗时:几秒——无下载。" #: ../../source/tutorial-cli-matrix.rst:51 msgid "Where the results land" @@ -114,10 +107,10 @@ msgid "" "recent run (a previous ``config.json`` is rotated to " "``config.json.bak``)." msgstr "" -"所有运行都写入 ``~/.cache/fl-sim/.logs/fedsynthetic-demo/``(本配置的 ``log_dir`` )。" -"每次运行各有一份 ``.txt`` 与一份 ``.json`` 日志——即 :doc:`quickstart` 描述过的" -"那些文件——另有一份 ``config.json`` 保存最近一次运行的确切服务器/客户端配置" -"(已有的 ``config.json`` 会被轮转为 ``config.json.bak`` )。" +"所有运行都写入 ``~/.cache/fl-sim/.logs/fedsynthetic-demo/``(本配置的 ``log_dir`` " +")。每次运行各有一份 ``.txt`` 与一份 ``.json`` 日志——即 :doc:`quickstart` 描述过的那些文件——另有一份 " +"``config.json`` 保存最近一次运行的确切服务器/客户端配置(已有的 ``config.json`` 会被轮转为 " +"``config.json.bak`` )。" #: ../../source/tutorial-cli-matrix.rst:60 msgid "" @@ -127,9 +120,9 @@ msgid "" "other. That is what the ``tag: ratio_${{ ... }}_seed_${{ ... }}`` line in" " the config above is for — give every matrix arm its own tag." msgstr "" -"一个在多次运行同一分钟内结束时就会生效的细节:日志文件名带有时间戳 **加上运行的 " -"``tag``** 。若每个矩阵组合没有独特的 tag,同名的日志会互相覆盖。上面配置里的 " -"``tag: ratio_${{ ... }}_seed_${{ ... }}`` 正是为此——给矩阵的每个分支一个专属 tag。" +"一个在多次运行同一分钟内结束时就会生效的细节:日志文件名带有时间戳 **加上运行的 ``tag``** 。若每个矩阵组合没有独特的 " +"tag,同名的日志会互相覆盖。上面配置里的 ``tag: ratio_${{ ... }}_seed_${{ ... }}`` " +"正是为此——给矩阵的每个分支一个专属 tag。" #: ../../source/tutorial-cli-matrix.rst:67 msgid "Beyond serial execution" @@ -146,11 +139,10 @@ msgid "" "two are orthogonal: process-level parallelism across matrix arms, " "executor-level parallelism across clients." msgstr "" -"长实验组成的矩阵需要不止一个进程。 ``strategy`` 节还接受并行选项(``n_parallel``," -"或 ``parallel: {mode: parallel_task, num_workers: N}`` 块),把矩阵分支派发给工作" -"进程——完整示例见 ``example-configs/multiprocessing-demo.yml``;进程内把 *单次运行* " -"的客户端并行化的执行器后端见 :doc:`parallel_execution` 。两者正交:进程级并行作用于" -"矩阵分支之间,执行器级并行作用于客户端之间。" +"长实验组成的矩阵需要不止一个进程。 ``strategy`` 节还接受并行选项(``n_parallel``,或 ``parallel: " +"{mode: parallel_task, num_workers: N}`` 块),把矩阵分支派发给工作进程——完整示例见 ``example-" +"configs/multiprocessing-demo.yml``;进程内把 *单次运行* 的客户端并行化的执行器后端见 " +":doc:`parallel_execution` 。两者正交:进程级并行作用于矩阵分支之间,执行器级并行作用于客户端之间。" #: ../../source/tutorial-cli-matrix.rst:79 msgid "Next steps" @@ -166,11 +158,21 @@ msgstr ":doc:`parallel_execution`——加速每一次单独的运行。" #: ../../source/tutorial-cli-matrix.rst:83 msgid "" -"How-to guides on custom algorithms, datasets and optimizers are on the " -"way; until then, ``test-files/custom_alg.py``, ``test-" -"files/custom_dataset.py`` and ``test-files/custom_optimizer.py`` are " -"complete, runnable examples." +":ref:`howto-custom-algorithm`, :ref:`howto-custom-dataset` and :ref" +":`howto-custom-optimizer` — extend the framework itself; the ``test-" +"files/custom_*.py`` files they walk through are complete, runnable " +"examples that the CLI accepts as algorithm/dataset paths." msgstr "" -"自定义算法、数据集与优化器的 how-to 指南正在准备中;在那之前," -"``test-files/custom_alg.py``、``test-files/custom_dataset.py`` 与 " -"``test-files/custom_optimizer.py`` 就是完整可运行的例子。" +":ref:`howto-custom-algorithm` 、 :ref:`howto-custom-dataset` 与 :ref:`howto-custom-optimizer` ——扩展框架本身;它们讲解的 ``test-files/custom_*.py`` 就是完整可运行的例子,CLI 可直接接受为算法/数据集路径。" + +#~ msgid "" +#~ "How-to guides on custom algorithms, " +#~ "datasets and optimizers are on the " +#~ "way; until then, ``test-files/custom_alg.py``," +#~ " ``test-files/custom_dataset.py`` and ``test-" +#~ "files/custom_optimizer.py`` are complete, runnable" +#~ " examples." +#~ msgstr "" +#~ "自定义算法、数据集与优化器的 how-to 指南正在准备中;在那之前,``test-" +#~ "files/custom_alg.py``、``test-files/custom_dataset.py`` 与" +#~ " ``test-files/custom_optimizer.py`` 就是完整可运行的例子。" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-first-experiment.po b/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-first-experiment.po index e6514677..bcaec410 100644 --- a/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-first-experiment.po +++ b/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-first-experiment.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: fl-sim \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-27 08:46+0800\n" +"POT-Creation-Date: 2026-10-28 07:53+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: zh_CN\n" @@ -31,9 +31,9 @@ msgid "" "ratios, then dissect what \"communication cost\" actually means in fl-" "sim." msgstr "" -":doc:`Quickstart ` 只跑了一次训练。真正的实验要在相同条件下比较不同配置," -"并把结果读回来。本教程将比较三个算法在两种采样比例下的表现,然后剖析 fl-sim 中" -"\"通信开销\"究竟指什么。" +":doc:`Quickstart ` " +"只跑了一次训练。真正的实验要在相同条件下比较不同配置,并把结果读回来。本教程将比较三个算法在两种采样比例下的表现,然后剖析 fl-sim " +"中\"通信开销\"究竟指什么。" #: ../../source/tutorial-first-experiment.rst:11 msgid "" @@ -42,17 +42,15 @@ msgid "" "downloads, seconds per run. The full script shown below lives at ``test-" "files/tutorial_first_experiment.py`` and is exercised in CI." msgstr "" -"全部运行基于 :class:`~fl_sim.data_processing.fed_synthetic.FedSynthetic`——" -"无需下载,每次运行只需几秒。下文展示的完整脚本位于 " -"``test-files/tutorial_first_experiment.py``,并在 CI 中被执行。" +"全部运行基于 " +":class:`~fl_sim.data_processing.fed_synthetic.FedSynthetic`——无需下载,每次运行只需几秒。下文展示的完整脚本位于" +" ``test-files/tutorial_first_experiment.py``,并在 CI 中被执行。" #: ../../source/tutorial-first-experiment.rst:15 msgid "" "Prerequisites: :doc:`quickstart` (the three-object model: dataset, model," " server) and :doc:`concepts` (what a round does)." -msgstr "" -"前置阅读: :doc:`quickstart` (三对象模型:数据集、模型、服务器)与 " -" :doc:`concepts` (一轮训练做了什么)。" +msgstr "前置阅读: :doc:`quickstart` (三对象模型:数据集、模型、服务器)与 :doc:`concepts` (一轮训练做了什么)。" #: ../../source/tutorial-first-experiment.rst:19 msgid "Set up the comparison" @@ -65,9 +63,8 @@ msgid "" "gradient, proximal and variance-reduction axes — each at **sampling " "ratios 0.5 and 1.0**." msgstr "" -"六次运行: **FedAvg** (朴素平均)、 **FedProx** (近端正则的本地目标)与 " -" **SCAFFOLD** (控制变量)——梯度、近端、方差缩减三条经典轴线——各自在 " -" **采样比例 0.5 与 1.0** 下运行。" +"六次运行: **FedAvg** (朴素平均)、 **FedProx** (近端正则的本地目标)与 **SCAFFOLD** " +"(控制变量)——梯度、近端、方差缩减三条经典轴线——各自在 **采样比例 0.5 与 1.0** 下运行。" #: ../../source/tutorial-first-experiment.rst:26 msgid "" @@ -77,9 +74,8 @@ msgid "" "sampling stream of everything after it. The order *dataset → config → " "server* keeps every run reproducible:" msgstr "" -"一个刻意的选择:数据集 **只构造一次,且先于任何配置或服务器** 。配置类的构造器在" -"构建时会重置全局 RNG,谁最后构造谁生效,其后的采样流都会被移动。按 " -"*数据集 → 配置 → 服务器* 的顺序构造,每次运行都可复现:" +"一个刻意的选择:数据集 **只构造一次,且先于任何配置或服务器** 。配置类的构造器在构建时会重置全局 " +"RNG,谁最后构造谁生效,其后的采样流都会被移动。按 *数据集 → 配置 → 服务器* 的顺序构造,每次运行都可复现:" #: ../../source/tutorial-first-experiment.rst:37 msgid "" @@ -89,9 +85,9 @@ msgid "" "frequency parameter later. Both are per-algorithm server keywords, " "collected in ``SERVER_KW``." msgstr "" -"``final_accuracy`` 读取一次运行的 JSON 日志——与 :doc:`quickstart` 中相同的文件" -"格式。SCAFFOLD 需要显式传入全局步长 ``lr``(它的配置没有默认值);FedPD 稍后也需要" -"一个频率参数。两者都是算法各自的服务器关键字,统一收在 ``SERVER_KW`` 里。" +"``final_accuracy`` 读取一次运行的 JSON 日志——与 :doc:`quickstart` 中相同的文件格式。SCAFFOLD" +" 需要显式传入全局步长 ``lr``(它的配置没有默认值);FedPD 稍后也需要一个频率参数。两者都是算法各自的服务器关键字,统一收在 " +"``SERVER_KW`` 里。" #: ../../source/tutorial-first-experiment.rst:44 msgid "Run and read the results" @@ -120,9 +116,9 @@ msgid "" "harder, more heterogeneous tasks (see the :doc:`algorithm theory pages " "` for when each helps)." msgstr "" -"这个合成任务 *太容易* ——全采样下 FedProx 与 SCAFFOLD 达到了和 FedAvg 相同的精度" -"天花板。它们的优势要在更难、异质性更强的任务上才显现(何时用哪个,见 " -":doc:`算法理论页 ` )。" +"这个合成任务 *太容易* ——全采样下 FedProx 与 SCAFFOLD 达到了和 FedAvg " +"相同的精度天花板。它们的优势要在更难、异质性更强的任务上才显现(何时用哪个,见 :doc:`算法理论页 " +"` )。" #: ../../source/tutorial-first-experiment.rst:74 msgid "" @@ -131,8 +127,8 @@ msgid "" "1.0 used exactly 180 — regardless of the algorithm. That number deserves " "a closer look, because it is easy to misread." msgstr "" -"精度的故事饱和了, **通信** 的故事没有:比例 0.5 的每次运行都恰好用了 90 次通信," -"比例 1.0 的都恰好 180 次——与算法无关。这个数字值得细看,因为它很容易被误读。" +"精度的故事饱和了, **通信** 的故事没有:比例 0.5 的每次运行都恰好用了 90 次通信,比例 1.0 的都恰好 180 " +"次——与算法无关。这个数字值得细看,因为它很容易被误读。" #: ../../source/tutorial-first-experiment.rst:80 msgid "What ``num_communications`` counts" @@ -147,8 +143,8 @@ msgid "" " doubles to 180." msgstr "" "``num_communications`` 统计的是 **消息数,不是轮数** :每轮对 *每个被采样客户端* " -"加一次下行广播,再加一次上行回复。30 个客户端、比例 0.5 时,每轮是 15 下行 + " -"15 上行 = 30 条消息,3 轮 = 90;比例 1.0 时翻倍为 180。" +"加一次下行广播,再加一次上行回复。30 个客户端、比例 0.5 时,每轮是 15 下行 + 15 上行 = 30 条消息,3 轮 = 90;比例 " +"1.0 时翻倍为 180。" #: ../../source/tutorial-first-experiment.rst:88 msgid "" @@ -176,8 +172,8 @@ msgid "" "round's full cost — read the final count from the server, as the scripts " "here do.)" msgstr "" -"(另注:JSON 日志里记录的 ``step`` 是 *每轮开始时* 的快照:它比最终计数少最后一轮的" -"全部开销——最终值请从服务器读取,正如本页脚本的做法。)" +"(另注:JSON 日志里记录的 ``step`` 是 *每轮开始时* " +"的快照:它比最终计数少最后一轮的全部开销——最终值请从服务器读取,正如本页脚本的做法。)" #: ../../source/tutorial-first-experiment.rst:106 msgid "Sampling is not communicating" @@ -191,9 +187,8 @@ msgid "" "every ``int(1/p)``-th round — for five rounds next to a five-round " "FedAvg:" msgstr "" -"上面三个算法每一轮都在通信。fl-sim 还内置 **可跳过通信** 的算法,它们会以很有启发性的" -"方式打破 ``2 × 采样数 × 轮数`` 的恒等式。让 FedPD(每 ``int(1/p)`` 轮才上传一次)" -"与五轮 FedAvg 并排各跑五轮:" +"上面三个算法每一轮都在通信。fl-sim 还内置 **可跳过通信** 的算法,它们会以很有启发性的方式打破 ``2 × 采样数 × 轮数`` " +"的恒等式。让 FedPD(每 ``int(1/p)`` 轮才上传一次)与五轮 FedAvg 并排各跑五轮:" #: ../../source/tutorial-first-experiment.rst:119 msgid "Output on one machine:" @@ -207,9 +202,8 @@ msgid "" "messages as three FedAvg rounds — the same communication budget buys two " "extra local iterations." msgstr "" -"分解式把话说得非常准: **下行完全相同** (75 = 5 轮 × 15 客户端——对被采样集合的广播" -"从不跳过);FedPD 省下的 *全是上行* 。五轮 FedPD 花费的 90 条消息与三轮 FedAvg 相同" -"——同样的通信预算换来了多两轮的本地迭代。" +"分解式把话说得非常准: **下行完全相同** (75 = 5 轮 × 15 客户端——对被采样集合的广播从不跳过);FedPD 省下的 " +"*全是上行* 。五轮 FedPD 花费的 90 条消息与三轮 FedAvg 相同——同样的通信预算换来了多两轮的本地迭代。" #: ../../source/tutorial-first-experiment.rst:132 msgid "Three practical details when you run this yourself:" @@ -223,9 +217,9 @@ msgid "" "frequency ``int(1/p)`` always wins — passing a ``comm_freq`` yourself is " "silently overwritten." msgstr "" -"``p`` 必须满足 **0 < p < 1** 。p = 1 时 FedPD 每轮都上传(退化为 FedAvg);p > 1 时" -"上传频率变为零,运行直接崩溃。配置暴露的是 ``p`` ,派生的上传频率 ``int(1/p)`` " -"始终说了算——自己传 ``comm_freq`` 会被静默覆写。" +"``p`` 必须满足 **0 < p < 1** 。p = 1 时 FedPD 每轮都上传(退化为 FedAvg);p > 1 " +"时上传频率变为零,运行直接崩溃。配置暴露的是 ``p`` ,派生的上传频率 ``int(1/p)`` 始终说了算——自己传 " +"``comm_freq`` 会被静默覆写。" #: ../../source/tutorial-first-experiment.rst:139 msgid "" @@ -238,11 +232,11 @@ msgid "" "appears once per (skipped client × skipped round), so it dominates the " "console. Both are how FedPD is designed to behave." msgstr "" -" **你会看到 RuntimeWarning——那是预期行为,不是错误。** 服务器对每个跳过的轮次记录 " -"``No message received from the clients...`` (\"这轮没人上传,全局模型没有动\");" -"每个跳过的客户端记录 ``No parameters received from server...`` (\"这轮没收到新的" -"全局模型,我继续用当前参数\")。教程规模下,客户端侧的消息每个(跳过客户端 × 跳过轮次)" -"出现一次,会刷满控制台。两者都是 FedPD 的设计行为。" +" **你会看到 RuntimeWarning——那是预期行为,不是错误。** 服务器对每个跳过的轮次记录 ``No message " +"received from the clients...`` (\"这轮没人上传,全局模型没有动\");每个跳过的客户端记录 ``No " +"parameters received from server...`` " +"(\"这轮没收到新的全局模型,我继续用当前参数\")。教程规模下,客户端侧的消息每个(跳过客户端 × 跳过轮次)出现一次,会刷满控制台。两者都是 " +"FedPD 的设计行为。" #: ../../source/tutorial-first-experiment.rst:147 msgid "" @@ -251,9 +245,8 @@ msgid "" "unlogged), for FedPD at 75 (its last round never uploads). The final " "counts come from ``server._num_communications``." msgstr "" -"JSON 的 ``step`` 快照落后多少因算法而异:五轮 FedAvg 终于 120(最后一轮的 30 条消息" -"未入日志),FedPD 终于 75(它的最后一轮从不上传)。最终计数请以 " -"``server._num_communications`` 为准。" +"JSON 的 ``step`` 快照落后多少因算法而异:五轮 FedAvg 终于 120(最后一轮的 30 条消息未入日志),FedPD 终于 " +"75(它的最后一轮从不上传)。最终计数请以 ``server._num_communications`` 为准。" #: ../../source/tutorial-first-experiment.rst:153 msgid "Next steps" @@ -263,12 +256,17 @@ msgstr "下一步" msgid "" ":doc:`tutorial-cli-matrix` — run the same kind of comparison from the " "command line, as a YAML-driven experiment matrix." -msgstr "" -":doc:`tutorial-cli-matrix`——用 YAML 实验矩阵从命令行跑同样的对照。" +msgstr ":doc:`tutorial-cli-matrix`——用 YAML 实验矩阵从命令行跑同样的对照。" #: ../../source/tutorial-first-experiment.rst:157 msgid "" +":doc:`tutorial-visualization` — merge multi-seed runs of this comparison " +"into mean curves with error bounds." +msgstr "" +":doc:`tutorial-visualization`——把本次对照的多 seed 运行合并成带误差带的均值曲线。" + +#: ../../source/tutorial-first-experiment.rst:159 +msgid "" ":doc:`parallel_execution` — speed these runs up with a parallel executor " "without changing the results." -msgstr "" -":doc:`parallel_execution`——用并行执行后端加速这些运行,且不改变结果。" +msgstr ":doc:`parallel_execution`——用并行执行后端加速这些运行,且不改变结果。" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-parallel-reproducible.po b/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-parallel-reproducible.po new file mode 100644 index 00000000..16a49b77 --- /dev/null +++ b/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-parallel-reproducible.po @@ -0,0 +1,173 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, WEN Hao +# This file is distributed under the same license as the fl-sim package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: fl-sim \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-10-28 07:53+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.16.0\n" + +#: ../../source/tutorial-parallel-reproducible.rst:4 +msgid "Tutorial: parallel and reproducible runs" +msgstr "教程:并行与可复现的运行" + +#: ../../source/tutorial-parallel-reproducible.rst:6 +msgid "" +"Experiments grow; wall-clock time should not. fl-sim can execute the " +"clients' local steps concurrently on a thread pool — with a one-line " +"config change — and this tutorial shows how to keep the run " +"*reproducible* while doing so: at the end, two different backends produce" +" **bitwise identical final model weights**." +msgstr "" +"实验规模会变大,墙钟时间不该跟着变。fl-sim 能在线程池上并发执行客户端的本地步——只需" +"一行配置——本教程展示这样做的同时如何保持运行 *可复现* :最后,两个不同的后端产出" +" **逐位一致的最终模型权重** 。" + +#: ../../source/tutorial-parallel-reproducible.rst:12 +msgid "" +"The full script lives at ``test-files/tutorial_parallel_check.py`` and is" +" exercised in CI. Prerequisites: :doc:`tutorial-first-experiment`." +msgstr "" +"完整脚本位于 ``test-files/tutorial_parallel_check.py``,并在 CI 中被执行。" +"前置阅读: :doc:`tutorial-first-experiment` 。" + +#: ../../source/tutorial-parallel-reproducible.rst:15 +msgid "" +"The mechanics behind this are documented in :doc:`parallel_execution` " +"(backends, guarantees, and how custom algorithms stay compatible); this " +"page is the hands-on version." +msgstr "" +"背后的机制见 :doc:`parallel_execution` (后端、保证、自定义算法如何保持兼容);" +"本页是动手版。" + +#: ../../source/tutorial-parallel-reproducible.rst:20 +msgid "One line of parallelism" +msgstr "一行开启并行" + +#: ../../source/tutorial-parallel-reproducible.rst:22 +msgid "" +"Add ``executor=\"threads\"`` to the server config. Nothing else changes —" +" algorithms, datasets, logging and the participation schedule all behave " +"identically; only the per-client local steps (evaluate → local update → " +"build upload) run on a thread pool:" +msgstr "" +"在服务器配置里加 ``executor=\"threads\"`` 。其余一切不变——算法、数据集、日志、" +"参与调度全都原样;只有每个客户端的本地步(评估 → 本地更新 → 构建上传)" +"跑在线程池上:" + +#: ../../source/tutorial-parallel-reproducible.rst:32 +msgid "" +"The helpers capture what we will compare: the final model weights " +"(``weights_of``) and an exact equality check over them (``bitwise_equal``" +" — ``torch.equal``, not ``allclose``)." +msgstr "" +"两个辅助函数负责我们要比较的东西:最终模型权重( ``weights_of`` )及其精确相等检查" +"( ``bitwise_equal`` ——用 ``torch.equal`` ,不是 ``allclose`` )。" + +#: ../../source/tutorial-parallel-reproducible.rst:37 +msgid "The two backends agree exactly" +msgstr "两种后端完全一致" + +#: ../../source/tutorial-parallel-reproducible.rst:39 +msgid "" +"Run the same experiment on both backends with the client config's " +"``rng_mode=\"per_client\"`` — each client's dataloaders get their own " +"generators seeded from ``(seed, client_id, round)`` — and compare:" +msgstr "" +"把客户端配置设为 ``rng_mode=\"per_client\"`` (每个客户端的 dataloader 拿到由 " +"``(seed, client_id, round)`` 派生种子的专属生成器),在两种后端上跑同一实验并比较:" + +#: ../../source/tutorial-parallel-reproducible.rst:48 +msgid "Output:" +msgstr "输出:" + +#: ../../source/tutorial-parallel-reproducible.rst:54 +msgid "" +"Bitwise identical — not \"close\", not \"same accuracy\": the same " +"tensors. This holds regardless of how many workers ran or how the " +"operating system scheduled them." +msgstr "" +"逐位一致——不是\"接近\",也不是\"精度相同\":就是同一批张量。无论跑了多少个 worker、" +"操作系统如何调度,这个结论都成立。" + +#: ../../source/tutorial-parallel-reproducible.rst:59 +msgid "What the global RNG mode buys (and costs)" +msgstr "全局 RNG 模式得到了什么(又付出了什么)" + +#: ../../source/tutorial-parallel-reproducible.rst:61 +msgid "" +"``rng_mode`` defaults to ``\"global\"``: clients share the process-global" +" torch RNG. The script runs the same comparison in that mode too:" +msgstr "" +"``rng_mode`` 默认为 ``\"global\"`` :客户端共享进程级全局 torch RNG。脚本在那种模式下" +"也跑了同样的比较:" + +#: ../../source/tutorial-parallel-reproducible.rst:69 +msgid "" +"Under the global mode the *participation schedule* and the *aggregation " +"arithmetic* are still backend-independent (sampling decisions and reply " +"consumption happen on the main thread in fixed order), so the " +"communication counts always match. The clients' local trajectories, " +"however, consume the shared RNG concurrently — so their interleaving " +"depends on thread scheduling, and the final weights may differ between " +"backends and between repeats:" +msgstr "" +"全局模式下, *参与调度* 与 *聚合算术* 仍与后端无关(采样决策与回复消费都在主线程按固定" +"顺序进行),因此通信计数永远一致。但客户端的本地轨迹会并发消耗共享 RNG——交叠情况取决于" +"线程调度,最终权重可能因后端不同、重复运行不同而不同:" + +#: ../../source/tutorial-parallel-reproducible.rst:81 +msgid "" +"(On a small run like this one the tasks often serialize and even the " +"weights match — treat that as luck, not a guarantee.)" +msgstr "(像这样的小规模运行,任务常常自动串行,连权重都恰好一致——那是运气,不是保证。)" + +#: ../../source/tutorial-parallel-reproducible.rst:84 +msgid "The rule of thumb:" +msgstr "经验法则:" + +#: ../../source/tutorial-parallel-reproducible.rst:86 +msgid "" +"**exploring**: global mode is fine; run-to-run variation is part of the " +"measurement (average over seeds, as in :doc:`tutorial-visualization`)." +msgstr "" +" **探索阶段** :全局模式没问题;运行间的波动本来就是测量的一部分(对多个 seed 取平均," +"见 :doc:`tutorial-visualization` )。" + +#: ../../source/tutorial-parallel-reproducible.rst:89 +msgid "" +"**paper-grade seed alignment**: either stay on ``\"sequential\"``, or set" +" ``rng_mode=\"per_client\"`` — then every backend, every parallelism " +"degree and every repeat produce the same trajectory from the same seed." +msgstr "" +" **论文级种子对齐** :要么保持 ``\"sequential\"``,要么设 ``rng_mode=\"per_client\"``" +"——之后同一种子在每个后端、每种并行度、每次重复下都走出同一条轨迹。" + +#: ../../source/tutorial-parallel-reproducible.rst:95 +msgid "Next steps" +msgstr "下一步" + +#: ../../source/tutorial-parallel-reproducible.rst:97 +msgid "" +":doc:`parallel_execution` — the full contract: guarantees per backend, " +"the ``FL_SIM_EXECUTOR``/``FL_SIM_NUM_WORKERS`` environment overrides, " +"measured speedups, and what custom algorithms must (not) touch." +msgstr "" +":doc:`parallel_execution`——完整契约:每个后端的保证、 ``FL_SIM_EXECUTOR`` / " +"``FL_SIM_NUM_WORKERS`` 环境变量覆盖、实测加速比,以及自定义算法哪些能碰哪些不能碰。" + +#: ../../source/tutorial-parallel-reproducible.rst:100 +msgid ":doc:`tutorial-visualization` — plot the runs you just made reproducible." +msgstr ":doc:`tutorial-visualization`——把刚变得可复现的这些运行画出来。" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-visualization.po b/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-visualization.po new file mode 100644 index 00000000..98cc8da1 --- /dev/null +++ b/docs/source/locale/zh_CN/LC_MESSAGES/tutorial-visualization.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, WEN Hao +# This file is distributed under the same license as the fl-sim package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: fl-sim \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-10-28 07:53+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.16.0\n" + +#: ../../source/tutorial-visualization.rst:4 +msgid "Tutorial: visualize and export results" +msgstr "教程:可视化与导出结果" + +#: ../../source/tutorial-visualization.rst:6 +msgid "" +"Every run writes ``.json`` logs (see :doc:`quickstart`); fl-sim ships a " +"visualization layer that turns them into publication-ready curves — " +"interactively in Jupyter, or programmatically from plain Python. This " +"tutorial does both: merge a multi-seed experiment into one mean curve " +"with error bounds, export it to PDF, and open the interactive dashboard." +msgstr "" +"每次运行都会写 ``.json`` 日志(见 :doc:`quickstart` );fl-sim 自带的可视化层能把它们变成" +"可直接发表的曲线——在 Jupyter 里交互式操作,或在纯 Python 里以编程方式处理。本教程两者" +"都做:把多 seed 实验合并成一条带误差带的均值曲线,导出 PDF,再打开交互式仪表盘。" + +#: ../../source/tutorial-visualization.rst:12 +msgid "" +"The full script lives at ``test-files/tutorial_visualization.py`` and is " +"exercised in CI (everything except the dashboard rendering, which needs " +"Jupyter). Prerequisites: :doc:`tutorial-first-experiment`." +msgstr "" +"完整脚本位于 ``test-files/tutorial_visualization.py``,并在 CI 中被执行(仪表盘渲染除外," +"它需要 Jupyter)。前置阅读: :doc:`tutorial-first-experiment` 。" + +#: ../../source/tutorial-visualization.rst:16 +msgid "" +"The reference for all knobs (fonts, smoothing, widgets) is :doc:`viz`; " +"this page is the workflow." +msgstr "所有可调项(字体、平滑、控件)的参考见 :doc:`viz` ;本页讲的是工作流程。" + +#: ../../source/tutorial-visualization.rst:20 +msgid "Get some logs" +msgstr "准备日志" + +#: ../../source/tutorial-visualization.rst:22 +msgid "" +"The script trains the same FedAvg experiment under two seeds — the " +"minimal \"multi-seed\" setup. Note the per-run ``tag``: log file names " +"carry it, so the two runs cannot overwrite each other (the same reasoning" +" as in :doc:`tutorial-cli-matrix`):" +msgstr "" +"脚本用两个种子训练同一个 FedAvg 实验——最小的\"多 seed\"设置。注意每次运行的 " +"``tag`` :日志文件名携带它,两次运行不会互相覆盖(道理同 :doc:`tutorial-cli-matrix` ):" + +#: ../../source/tutorial-visualization.rst:33 +msgid "Merge seeds into one curve" +msgstr "把多个 seed 合并成一条曲线" + +#: ../../source/tutorial-visualization.rst:35 +msgid "" +"``find_log_files`` collects the JSON logs; " +"``aggregate_results_from_json_log_with_custom_axis`` extracts a curve " +"(``step`` on x, ``acc`` on y here — any recorded metric works); " +"``plot_mean_curve_with_error_bounds`` merges the curves into a mean with " +"an uncertainty band (``\"sem\"``, ``\"std\"``, ``\"quartile\"`` or " +"``\"iqr\"``). Saving the figure is plain matplotlib:" +msgstr "" +"``find_log_files`` 收集 JSON 日志; ``aggregate_results_from_json_log_with_custom_axis`` " +"抽取曲线(这里 x 轴 ``step``、y 轴 ``acc``——任何记录过的指标都行); " +"``plot_mean_curve_with_error_bounds`` 把曲线合并成带不确定度带的均值( ``\"sem\"`` 、 " +"``\"std\"`` 、 ``\"quartile\"`` 或 ``\"iqr\"`` )。存图就是普通的 matplotlib:" + +#: ../../source/tutorial-visualization.rst:46 +msgid "" +"This writes ``fedavg-multi-seed.pdf`` next to your working directory. Two" +" things worth noticing:" +msgstr "这会在工作目录旁写出 ``fedavg-multi-seed.pdf`` 。两点值得注意:" + +#: ../../source/tutorial-visualization.rst:49 +msgid "" +"The x axis is the **cumulative communication count** — the metric :doc" +":`tutorial-first-experiment` dissected. Plotting against communications " +"(rather than rounds) is what makes different communication-efficiency " +"algorithms comparable in one figure." +msgstr "" +"x 轴是 **累计通信次数** ——即 :doc:`tutorial-first-experiment` 剖析过的那个量。" +"以通信(而不是轮数)为横轴,才能把通信效率不同的算法放进同一张图里比较。" + +#: ../../source/tutorial-visualization.rst:53 +msgid "" +"With only two seeds the band is a range, not a statistic — use more seeds" +" (just add more ``seed=`` runs; the merging code does not change)." +msgstr "" +"只有两个 seed 时,带子只是一个范围,还不是统计量——多加几个 ``seed=`` 运行即可" +"(合并代码一行都不用改)。" + +#: ../../source/tutorial-visualization.rst:58 +msgid "Explore interactively" +msgstr "交互式探索" + +#: ../../source/tutorial-visualization.rst:60 +msgid "" +"In a Jupyter notebook, the ``Panel`` dashboard does all of the above " +"interactively: it lists the log files it finds, plots selectable metrics," +" and gives you sliders for smoothing, fonts and figure size, plus one-" +"click export to PDF/SVG/PNG:" +msgstr "" +"在 Jupyter notebook 里, ``Panel`` 仪表盘把上面的所有事都变成交互式的:列出找到的日志" +"文件、绘制可选的指标,提供平滑、字体与图幅的滑块,以及一键导出 PDF/SVG/PNG:" + +#: ../../source/tutorial-visualization.rst:70 +msgid "" +"Run this cell in a notebook (JupyterLab, Colab, SageMaker, …) and the " +"dashboard renders inline; see :doc:`viz` for a demo animation and the " +"full feature list." +msgstr "" +"在 notebook(JupyterLab、Colab、SageMaker 等)里运行这个单元格,仪表盘就会内嵌渲染;" +"演示动画与完整功能列表见 :doc:`viz` 。" + +#: ../../source/tutorial-visualization.rst:75 +msgid "Next steps" +msgstr "下一步" + +#: ../../source/tutorial-visualization.rst:77 +msgid "" +":doc:`tutorial-parallel-reproducible` — make these runs faster (parallel " +"executor) without giving up reproducibility." +msgstr "" +":doc:`tutorial-parallel-reproducible`——让这些运行更快(并行执行器),且不放弃可复现性。" + +#: ../../source/tutorial-visualization.rst:79 +msgid ":doc:`viz` — the reference for every visualization option." +msgstr ":doc:`viz`——所有可视化选项的参考。" diff --git a/docs/source/tutorial-cli-matrix.rst b/docs/source/tutorial-cli-matrix.rst index 316748a2..6a24a07d 100644 --- a/docs/source/tutorial-cli-matrix.rst +++ b/docs/source/tutorial-cli-matrix.rst @@ -80,7 +80,7 @@ Next steps - :doc:`cli` — the full YAML syntax reference and CLI options. - :doc:`parallel_execution` — speed up each individual run. -- How-to guides on custom algorithms, datasets and optimizers are on - the way; until then, ``test-files/custom_alg.py``, - ``test-files/custom_dataset.py`` and ``test-files/custom_optimizer.py`` - are complete, runnable examples. +- :ref:`howto-custom-algorithm`, :ref:`howto-custom-dataset` and + :ref:`howto-custom-optimizer` — extend the framework itself; the + ``test-files/custom_*.py`` files they walk through are complete, + runnable examples that the CLI accepts as algorithm/dataset paths. diff --git a/docs/source/tutorial-first-experiment.rst b/docs/source/tutorial-first-experiment.rst index 98dfc219..736aa0a2 100644 --- a/docs/source/tutorial-first-experiment.rst +++ b/docs/source/tutorial-first-experiment.rst @@ -154,5 +154,7 @@ Next steps - :doc:`tutorial-cli-matrix` — run the same kind of comparison from the command line, as a YAML-driven experiment matrix. +- :doc:`tutorial-visualization` — merge multi-seed runs of this + comparison into mean curves with error bounds. - :doc:`parallel_execution` — speed these runs up with a parallel executor without changing the results. diff --git a/docs/source/tutorial-parallel-reproducible.rst b/docs/source/tutorial-parallel-reproducible.rst new file mode 100644 index 00000000..fc9521ee --- /dev/null +++ b/docs/source/tutorial-parallel-reproducible.rst @@ -0,0 +1,101 @@ +.. _tutorial-parallel-reproducible: + +Tutorial: parallel and reproducible runs +======================================== + +Experiments grow; wall-clock time should not. fl-sim can execute the +clients' local steps concurrently on a thread pool — with a one-line +config change — and this tutorial shows how to keep the run +*reproducible* while doing so: at the end, two different backends +produce **bitwise identical final model weights**. + +The full script lives at ``test-files/tutorial_parallel_check.py`` and +is exercised in CI. Prerequisites: :doc:`tutorial-first-experiment`. + +The mechanics behind this are documented in +:doc:`parallel_execution` (backends, guarantees, and how custom +algorithms stay compatible); this page is the hands-on version. + +One line of parallelism +----------------------- + +Add ``executor="threads"`` to the server config. Nothing else changes — +algorithms, datasets, logging and the participation schedule all behave +identically; only the per-client local steps (evaluate → local update → +build upload) run on a thread pool: + +.. literalinclude:: ../../test-files/tutorial_parallel_check.py + :language: python + :start-after: # --- begin: setup --- + :end-before: # --- end: setup --- + +The helpers capture what we will compare: the final model weights +(``weights_of``) and an exact equality check over them +(``bitwise_equal`` — ``torch.equal``, not ``allclose``). + +The two backends agree exactly +------------------------------ + +Run the same experiment on both backends with the client config's +``rng_mode="per_client"`` — each client's dataloaders get their own +generators seeded from ``(seed, client_id, round)`` — and compare: + +.. literalinclude:: ../../test-files/tutorial_parallel_check.py + :language: python + :start-after: # --- begin: per-client-check --- + :end-before: # --- end: per-client-check --- + +Output: + +.. code-block:: text + + per-client RNG: communications equal: True, final weights bitwise equal: True + +Bitwise identical — not "close", not "same accuracy": the same tensors. +This holds regardless of how many workers ran or how the operating +system scheduled them. + +What the global RNG mode buys (and costs) +----------------------------------------- + +``rng_mode`` defaults to ``"global"``: clients share the process-global +torch RNG. The script runs the same comparison in that mode too: + +.. literalinclude:: ../../test-files/tutorial_parallel_check.py + :language: python + :start-after: # --- begin: global-check --- + :end-before: # --- end: global-check --- + +Under the global mode the *participation schedule* and the *aggregation +arithmetic* are still backend-independent (sampling decisions and reply +consumption happen on the main thread in fixed order), so the +communication counts always match. The clients' local trajectories, +however, consume the shared RNG concurrently — so their interleaving +depends on thread scheduling, and the final weights may differ between +backends and between repeats: + +.. code-block:: text + + global RNG: communications equal: True, final weights bitwise equal: True (may be False by design) + +(On a small run like this one the tasks often serialize and even the +weights match — treat that as luck, not a guarantee.) + +The rule of thumb: + +- **exploring**: global mode is fine; run-to-run variation is part of + the measurement (average over seeds, as in + :doc:`tutorial-visualization`). +- **paper-grade seed alignment**: either stay on ``"sequential"``, or + set ``rng_mode="per_client"`` — then every backend, every parallelism + degree and every repeat produce the same trajectory from the same + seed. + +Next steps +---------- + +- :doc:`parallel_execution` — the full contract: guarantees per backend, + the ``FL_SIM_EXECUTOR``/``FL_SIM_NUM_WORKERS`` environment overrides, + measured speedups, and what custom algorithms must (not) touch. +- :doc:`tutorial-visualization` — plot the runs you just made + reproducible. diff --git a/docs/source/tutorial-visualization.rst b/docs/source/tutorial-visualization.rst new file mode 100644 index 00000000..8094abdc --- /dev/null +++ b/docs/source/tutorial-visualization.rst @@ -0,0 +1,79 @@ +.. _tutorial-visualization: + +Tutorial: visualize and export results +====================================== + +Every run writes ``.json`` logs (see :doc:`quickstart`); fl-sim ships a +visualization layer that turns them into publication-ready curves — +interactively in Jupyter, or programmatically from plain Python. This +tutorial does both: merge a multi-seed experiment into one mean curve +with error bounds, export it to PDF, and open the interactive dashboard. + +The full script lives at ``test-files/tutorial_visualization.py`` and is +exercised in CI (everything except the dashboard rendering, which needs +Jupyter). Prerequisites: :doc:`tutorial-first-experiment`. + +The reference for all knobs (fonts, smoothing, widgets) is +:doc:`viz`; this page is the workflow. + +Get some logs +------------- + +The script trains the same FedAvg experiment under two seeds — the +minimal "multi-seed" setup. Note the per-run ``tag``: log file names +carry it, so the two runs cannot overwrite each other (the same +reasoning as in :doc:`tutorial-cli-matrix`): + +.. literalinclude:: ../../test-files/tutorial_visualization.py + :language: python + :start-after: # --- begin: setup --- + :end-before: # --- end: setup --- + +Merge seeds into one curve +-------------------------- + +``find_log_files`` collects the JSON logs; ``aggregate_results_from_json_log_with_custom_axis`` +extracts a curve (``step`` on x, ``acc`` on y here — any recorded metric +works); ``plot_mean_curve_with_error_bounds`` merges the curves into a +mean with an uncertainty band (``"sem"``, ``"std"``, ``"quartile"`` or +``"iqr"``). Saving the figure is plain matplotlib: + +.. literalinclude:: ../../test-files/tutorial_visualization.py + :language: python + :start-after: # --- begin: plot-mean --- + :end-before: # --- end: plot-mean --- + +This writes ``fedavg-multi-seed.pdf`` next to your working directory. +Two things worth noticing: + +- The x axis is the **cumulative communication count** — the metric + :doc:`tutorial-first-experiment` dissected. Plotting against + communications (rather than rounds) is what makes different + communication-efficiency algorithms comparable in one figure. +- With only two seeds the band is a range, not a statistic — use more + seeds (just add more ``seed=`` runs; the merging code does not + change). + +Explore interactively +--------------------- + +In a Jupyter notebook, the ``Panel`` dashboard does all of the above +interactively: it lists the log files it finds, plots selectable +metrics, and gives you sliders for smoothing, fonts and figure size, +plus one-click export to PDF/SVG/PNG: + +.. literalinclude:: ../../test-files/tutorial_visualization.py + :language: python + :start-after: # --- begin: panel --- + :end-before: # --- end: panel --- + +Run this cell in a notebook (JupyterLab, Colab, SageMaker, …) and the +dashboard renders inline; see :doc:`viz` for a demo animation and the +full feature list. + +Next steps +---------- + +- :doc:`tutorial-parallel-reproducible` — make these runs faster + (parallel executor) without giving up reproducibility. +- :doc:`viz` — the reference for every visualization option. diff --git a/test-files/custom_alg.py b/test-files/custom_alg.py index 5de03e56..2e195bcd 100644 --- a/test-files/custom_alg.py +++ b/test-files/custom_alg.py @@ -28,6 +28,7 @@ ] +# --- begin: configs --- @register_algorithm() @add_docstring(server_config_kw_doc, "append") class CustomServerConfig(ServerConfig): @@ -93,6 +94,10 @@ def _fix_optimizer(self) -> "CustomClientConfig": return self +# --- end: configs --- + + +# --- begin: server --- @register_algorithm() @add_docstring( Server.__doc__.replace( @@ -156,6 +161,10 @@ def doi(self) -> List[str]: return ["10.48550/ARXIV.1812.06127"] +# --- end: server --- + + +# --- begin: client --- @register_algorithm() @add_docstring( Client.__doc__.replace( @@ -251,3 +260,6 @@ def train(self) -> None: # free memory del X, y, output, loss self.lr_scheduler.step() + + +# --- end: client --- diff --git a/test-files/custom_dataset.py b/test-files/custom_dataset.py index 3dac0c27..62340f27 100644 --- a/test-files/custom_dataset.py +++ b/test-files/custom_dataset.py @@ -27,6 +27,7 @@ _label_mapping = {i: c for i, c in enumerate("abcdefghijklmnopqrstuvwxyz"[:10])} +# --- begin: dataset --- @register_fed_dataset() class CustomFEMNIST(FedVisionDataset): """ @@ -62,6 +63,7 @@ class CustomFEMNIST(FedVisionDataset): __name__ = "CustomFEMNIST" + # --- begin: preload --- def _preload(self, datadir: Optional[Union[str, Path]] = None) -> None: self.datadir = Path(datadir or CUSTOM_FEMNIST_DATA_DIR).expanduser().resolve() @@ -105,6 +107,9 @@ def _preload(self, datadir: Optional[Union[str, Path]] = None) -> None: ) ) + # --- end: preload --- + + # --- begin: dataloader --- def get_dataloader( self, train_bs: Optional[int] = None, @@ -152,11 +157,14 @@ def get_dataloader( return train_dl, test_dl + # --- end: dataloader --- + def extra_repr_keys(self) -> List[str]: return [ "n_class", ] + super().extra_repr_keys() + # --- begin: evaluate --- def evaluate(self, probs: torch.Tensor, truths: torch.Tensor) -> Dict[str, float]: return { "acc": top_n_accuracy(probs, truths, 1), @@ -166,6 +174,9 @@ def evaluate(self, probs: torch.Tensor, truths: torch.Tensor) -> Dict[str, float "num_samples": probs.shape[0], } + # --- end: evaluate --- + + # --- begin: mirrors-and-models --- @property def url(self) -> str: # https://drive.google.com/file/d/1tCEcJgRJ8NdRo11UJZR6WSKMNdmox4GC/view?usp=sharing @@ -205,3 +216,8 @@ def doi(self) -> List[str]: @property def label_map(self) -> dict: return _label_mapping + + # --- end: mirrors-and-models --- + + +# --- end: dataset --- diff --git a/test-files/custom_optimizer.py b/test-files/custom_optimizer.py index 5b5a91fc..34352c2b 100644 --- a/test-files/custom_optimizer.py +++ b/test-files/custom_optimizer.py @@ -10,6 +10,7 @@ ] +# --- begin: optimizer --- @register_optimizer() class CustomOptimizer(ProxSGD): """Local optimizer for ``CustomOptimizer`` using ``ProxSGD``. @@ -46,3 +47,6 @@ def __init__( ) -> None: self.mu = mu super().__init__(params, lr=lr, prox=mu, momentum=0) + + +# --- end: optimizer --- diff --git a/test-files/tutorial_first_experiment.py b/test-files/tutorial_first_experiment.py index 18ecc91a..a614ac0f 100644 --- a/test-files/tutorial_first_experiment.py +++ b/test-files/tutorial_first_experiment.py @@ -41,20 +41,23 @@ # one synthetic dataset, split across 30 clients, shared by every run below dataset = FedSynthetic(1.0, 1.0, True, 30) -model = dataset.candidate_models["mlp_d1"] def run(algorithm, sample_ratio, num_iters, tag, **extra_kw): """Train one algorithm and return the server. - Algorithm-specific server keywords (SCAFFOLD's global step size, - FedPD's upload frequency ``p``) come from ``SERVER_KW``; anything - passed as ``extra_kw`` is merged on top. + Every run takes a FRESH model from ``dataset.candidate_models`` + (a property building a new module on each access): the server trains + the model it is given in place, so sharing one model across runs + would warm-start every run after the first and contaminate the + comparison. Algorithm-specific server keywords (SCAFFOLD's global + step size, FedPD's upload frequency ``p``) come from ``SERVER_KW``; + anything passed as ``extra_kw`` is merged on top. """ server_cls, server_config_cls, client_config_cls = ALGORITHMS[algorithm] server_kw = {**SERVER_KW[algorithm], **extra_kw} server = server_cls( - model, + dataset.candidate_models["mlp_d1"], dataset, server_config_cls( num_iters=num_iters, diff --git a/test-files/tutorial_parallel_check.py b/test-files/tutorial_parallel_check.py new file mode 100644 index 00000000..29e455aa --- /dev/null +++ b/test-files/tutorial_parallel_check.py @@ -0,0 +1,74 @@ +""" +Parallelize a run and verify it stays reproducible. + +This script is the source of the code blocks shown in the +``tutorial-parallel-reproducible`` page of the documentation +(``docs/source/tutorial-parallel-reproducible.rst``): the marked sections +are included verbatim via Sphinx ``literalinclude`` directives, so the +docs and this script can never drift apart. The runs are pinned to the +tutorial budget (``num_iters <= 5``, ``num_clients <= 30``, one local +epoch), enforced by ``test/test_tutorial_scripts.py``. +""" + +# --- begin: setup --- +import torch + +from fl_sim.algorithms.fedopt import FedAvgClientConfig, FedAvgServer, FedAvgServerConfig +from fl_sim.data_processing.fed_synthetic import FedSynthetic + +dataset = FedSynthetic(1.0, 1.0, True, 30) + + +def run(executor, tag, rng_mode): + """One FedAvg run on the given executor backend.""" + return FedAvgServer( + # a fresh model per run (see the first-experiment tutorial) + dataset.candidate_models["mlp_d1"], + dataset, + FedAvgServerConfig( + num_iters=3, + num_clients=30, + clients_sample_ratio=0.5, + executor=executor, # "sequential" (default) or "threads" + num_workers=2, + log_dir=f"t4_{tag}", + ), + FedAvgClientConfig(batch_size=32, num_epochs=1, rng_mode=rng_mode), + ) + + +def weights_of(server): + return list(server.model.state_dict().values()) + + +def bitwise_equal(a, b): + return all(torch.equal(x, y) for x, y in zip(weights_of(a), weights_of(b))) + + +# --- end: setup --- + +# --- begin: per-client-check --- +sequential = run("sequential", "seq", rng_mode="per_client") +threads = run("threads", "threads", rng_mode="per_client") + +print( + f"per-client RNG: communications equal: " + f"{sequential._num_communications == threads._num_communications}, " + f"final weights bitwise equal: {bitwise_equal(sequential, threads)}" +) +# --- end: per-client-check --- + +# --- begin: global-check --- +sequential_g = run("sequential", "seq_global", rng_mode="global") +threads_g = run("threads", "threads_global", rng_mode="global") + +# with the process-global RNG, the participation sets and the aggregation +# arithmetic stay identical, but the clients' local trajectories consume +# the shared RNG concurrently, so the final weights may differ — this is +# the documented (and expected) statistical-level behaviour +print( + f"global RNG: communications equal: " + f"{sequential_g._num_communications == threads_g._num_communications}, " + f"final weights bitwise equal: {bitwise_equal(sequential_g, threads_g)} (may be False by design)" +) +# --- end: global-check --- diff --git a/test-files/tutorial_visualization.py b/test-files/tutorial_visualization.py new file mode 100644 index 00000000..1243961f --- /dev/null +++ b/test-files/tutorial_visualization.py @@ -0,0 +1,73 @@ +""" +Visualize and export experiment results. + +This script is the source of the code blocks shown in the +``tutorial-visualization`` page of the documentation +(``docs/source/tutorial-visualization.rst``): the marked sections are +included verbatim via Sphinx ``literalinclude`` directives, so the docs +and this script can never drift apart. The training runs below are +pinned to the tutorial budget (``num_iters <= 5``, ``num_clients <= 30``, +one local epoch), enforced by ``test/test_tutorial_scripts.py``. + +The two runs at the top stand in for the experiment logs produced by +the earlier tutorials (or your own experiments): everything after them +works the same on any log directory. + +The interactive part of the tutorial (the ``Panel`` dashboard) needs a +Jupyter notebook and is therefore only constructed, not rendered, here. +""" + +# --- begin: setup --- +from fl_sim.algorithms.fedopt import FedAvgClientConfig, FedAvgServer, FedAvgServerConfig +from fl_sim.data_processing.fed_synthetic import FedSynthetic + +dataset = FedSynthetic(1.0, 1.0, True, 30) + +# two seeds of the same experiment: exactly what "multi-seed" means +for seed in (0, 1): + server = FedAvgServer( + # a fresh model per run: the server trains the model it is given + # in place, so sharing one module would warm-start the second run + dataset.candidate_models["mlp_d1"], + dataset, + FedAvgServerConfig( + num_iters=3, + num_clients=30, + clients_sample_ratio=0.5, + seed=seed, + log_dir="t3_seeds", + tag=f"seed_{seed}", # distinct tag per seed: no log overwrites + ), + FedAvgClientConfig(batch_size=32, num_epochs=1), + ) + server.train() +# --- end: setup --- + +from pathlib import Path + +# --- begin: plot-mean --- +import matplotlib.pyplot as plt + +from fl_sim.utils.viz import aggregate_results_from_json_log_with_custom_axis, find_log_files + +# the runs above logged into the t3_seeds directory under fl-sim's log root +log_dir = Path(server._logger_manager.log_dir) +files = find_log_files(directory=log_dir, filters="seed_") +curves = [aggregate_results_from_json_log_with_custom_axis(f, part="val", x_metric="step", y_metric="acc") for f in files] + +fig, ax = plt.subplots(figsize=(6, 4)) +from fl_sim.utils.viz import plot_mean_curve_with_error_bounds + +fig, ax = plot_mean_curve_with_error_bounds(curves, error_type="sem", label="FedAvg", fig_ax=(fig, ax)) +ax.set_xlabel("cumulative communications") +ax.set_ylabel("val acc") +fig.savefig("fedavg-multi-seed.pdf", bbox_inches="tight") +# --- end: plot-mean --- + +# --- begin: panel --- +from fl_sim.utils.viz import Panel + +# in a Jupyter notebook this renders an interactive dashboard: +# pick log files, tune curves, export to PDF/SVG/PNG +panel = Panel(logdir="t3_seeds") +# --- end: panel --- diff --git a/test/test_tutorial_scripts.py b/test/test_tutorial_scripts.py index 59e2ac84..7e129777 100644 --- a/test/test_tutorial_scripts.py +++ b/test/test_tutorial_scripts.py @@ -33,6 +33,8 @@ TUTORIAL_SCRIPTS = { "quickstart": REPO_ROOT / "test-files" / "quickstart.py", "tutorial_first_experiment": REPO_ROOT / "test-files" / "tutorial_first_experiment.py", + "tutorial_visualization": REPO_ROOT / "test-files" / "tutorial_visualization.py", + "tutorial_parallel_check": REPO_ROOT / "test-files" / "tutorial_parallel_check.py", } CLI_MATRIX_DEMO = REPO_ROOT / "example-configs" / "fedsynthetic-demo.yml" @@ -44,6 +46,14 @@ def isolated_log_dir(tmp_path, monkeypatch): yield tmp_path +@pytest.fixture() +def in_tmp_cwd(tmp_path, monkeypatch): + """Run scripts from a temporary working directory, so any file they + write relative to the CWD (e.g. an exported PDF) stays contained.""" + monkeypatch.chdir(tmp_path) + yield tmp_path + + def _run_script(script: Path): """Execute a tutorial script in-process, restoring ``sys.path`` afterwards. @@ -148,6 +158,49 @@ def test_cli_matrix_demo_expands_and_runs(isolated_log_dir): _check_tutorial_budget(run_dir) +def test_visualization_script_merges_seeds_and_exports(isolated_log_dir, in_tmp_cwd): + namespace = _run_script(TUTORIAL_SCRIPTS["tutorial_visualization"]) + + # two tagged runs, both within the tutorial budget + run_dir = isolated_log_dir / "t3_seeds" + jsons = [p for p in run_dir.glob("*.json") if p.name != "config.json"] + assert len(jsons) == 2, "the two seeds must not overwrite each other's logs" + _check_tutorial_budget(run_dir) + + # the merge produced one non-empty curve per seed, and the PDF export + # landed in the (temporary) working directory + curves = namespace["curves"] + assert len(curves) == 2 + for curve in curves: + assert len(curve["x"]) > 0 and len(curve["y"]) > 0 + assert (in_tmp_cwd / "fedavg-multi-seed.pdf").exists(), "the mean curve should be exported to PDF" + + # the dashboard object is constructible outside Jupyter (it prints a + # note and stays inert there; rendering is covered by manual use) + assert namespace["panel"] is not None + + +def test_parallel_check_script_verifies_bitwise_reproducibility(isolated_log_dir): + namespace = _run_script(TUTORIAL_SCRIPTS["tutorial_parallel_check"]) + + # the L1 guarantee, demonstrated end to end: same seed, per-client RNG, + # two different executor backends -> bitwise identical final weights + sequential, threads = namespace["sequential"], namespace["threads"] + assert sequential._num_communications == threads._num_communications + assert namespace["bitwise_equal"]( + sequential, threads + ), "sequential and threads with rng_mode='per_client' must agree bitwise" + + # global mode: participation stays backend-independent; local + # trajectories may interleave by design, so only the counts are asserted + sequential_g, threads_g = namespace["sequential_g"], namespace["threads_g"] + assert sequential_g._num_communications == threads_g._num_communications + + for sub in isolated_log_dir.iterdir(): + if sub.name.startswith("t4_") and sub.is_dir(): + _check_tutorial_budget(sub) + + def test_tutorial_scripts_keep_literalinclude_markers(): """The markers delimit the fragments the docs include; dropping one silently empties a documentation code block."""