From 466cb6982f0a5f9a03ec3e5644112a43861dabfa Mon Sep 17 00:00:00 2001 From: WEN Hao Date: Mon, 26 Oct 2026 14:33:23 +0800 Subject: [PATCH] docs: add quickstart and concepts pages with drift-proof code blocks - quickstart.rst: a first federated training run; the shown code is embedded via literalinclude from the runnable script test-files/quickstart.py, so documentation blocks cannot drift from the executable source - concepts.rst: the four building blocks (dataset, model, server, client), algorithm hooks, and the fixed per-round order - index.rst: Getting started now lists install, quickstart, concepts - examples.rst: the empty placeholder page becomes an orphan redirect stub pointing to quickstart - cli.rst: replace the hand-copied YAML block, which had drifted from example-configs/all-alg-fedprox-femnist.yml and was not parseable, with a literalinclude of the source file - test/test_tutorial_scripts.py: behavioral smoke tests for the tutorial scripts, executed under runpy with an isolated log directory; asserts the tutorial runtime budget from each run's config.json and that every literalinclude target in the docs exists - zh_CN translations for the new and updated pages --- docs/source/cli.rst | 52 +-- docs/source/concepts.rst | 154 ++++++++ docs/source/examples.rst | 10 +- docs/source/index.rst | 3 +- .../locale/zh_CN/LC_MESSAGES/concepts.po | 343 ++++++++++++++++++ .../locale/zh_CN/LC_MESSAGES/examples.po | 25 +- .../locale/zh_CN/LC_MESSAGES/quickstart.po | 183 ++++++++++ docs/source/quickstart.rst | 91 +++++ test-files/quickstart.py | 52 +++ test/test_tutorial_scripts.py | 106 ++++++ 10 files changed, 957 insertions(+), 62 deletions(-) create mode 100644 docs/source/concepts.rst create mode 100644 docs/source/locale/zh_CN/LC_MESSAGES/concepts.po create mode 100644 docs/source/locale/zh_CN/LC_MESSAGES/quickstart.po create mode 100644 docs/source/quickstart.rst create mode 100644 test-files/quickstart.py create mode 100644 test/test_tutorial_scripts.py diff --git a/docs/source/cli.rst b/docs/source/cli.rst index 87d5de48..7faa9c30 100644 --- a/docs/source/cli.rst +++ b/docs/source/cli.rst @@ -9,56 +9,8 @@ For example, in the `all-alg-fedprox-femnist.yml ` — every config field + of every algorithm. +- :doc:`viz` — explore the produced logs interactively. diff --git a/docs/source/examples.rst b/docs/source/examples.rst index d47fcda4..edcb1a0f 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -1,4 +1,10 @@ +:orphan: + Usage examples -^^^^^^^^^^^^^^^^ +============== + +.. seealso:: -to write.... + The usage examples now live in :doc:`quickstart`, with + :doc:`concepts` as the conceptual map and task-oriented tutorials + building on both. diff --git a/docs/source/index.rst b/docs/source/index.rst index 86d33fc4..ce2517b2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,7 +15,8 @@ Project Links: `GitHub `_ | `gitee , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: fl-sim \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-10-26 13:08+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/concepts.rst:4 +msgid "Core concepts" +msgstr "核心概念" + +#: ../../source/concepts.rst:6 +msgid "" +"This page is the map of fl-sim: the four building blocks of every " +"experiment and what happens during one round of federated training. It " +"explains *how the framework thinks* — for a hands-on walk-through, start " +"with :doc:`quickstart`." +msgstr "" +"本页面是 fl-sim 的地图:每次实验的四个构件,以及一轮联邦训练中发生的事。它解释的是" +"框架的*思考方式*——若想先动手体验,请从 :doc:`quickstart` 开始。" + +#: ../../source/concepts.rst:12 +msgid "Four building blocks" +msgstr "四个构件" + +#: ../../source/concepts.rst:15 +msgid "FedDataset — the data and how it is split" +msgstr "FedDataset——数据及其划分方式" + +#: ../../source/concepts.rst:17 +msgid "" +"A :class:`~fl_sim.data_processing.fed_dataset.FedDataset` bundles a raw " +"dataset with a **partitioning scheme**: it decides how the data is spread" +" across the simulated clients and hands each client its own dataloaders " +"through ``get_dataloader(client_idx)``. Built-in datasets (FEMNIST, " +"CIFAR10, …) download on first use; " +":class:`~fl_sim.data_processing.fed_synthetic.FedSynthetic` generates " +"synthetic data locally, which is why every tutorial here runs in seconds." +" Each dataset also declares ``candidate_models`` — the models it is meant" +" to be paired with (``dataset.candidate_models[\"mlp_d1\"]``, as in the " +"quickstart). Datasets are **immutable during training**: partitioning " +"happens once, up front. Custom datasets are covered in a how-to guide." +msgstr "" +":class:`~fl_sim.data_processing.fed_dataset.FedDataset` 把原始数据集与 **划分方案** " +"打包在一起:它决定数据如何在模拟的客户端之间分布,并通过 ``get_dataloader(client_idx)`` " +"为每个客户端提供专属的 dataloader。内置数据集(FEMNIST、CIFAR10 等)在首次使用时下载;" +" :class:`~fl_sim.data_processing.fed_synthetic.FedSynthetic` 在本地生成合成数据," +"这也是本教程都能在几秒内跑完的原因。每个数据集还声明了 ``candidate_models``——" +"适合与它搭配的模型(如 quickstart 中的 ``dataset.candidate_models[\"mlp_d1\"]``)。" +"数据集在 **训练期间不可变** :划分只做一次,且在训练开始前完成。自定义数据集见相应的" +" how-to 指南。" + +#: ../../source/concepts.rst:29 +msgid "Model — a plain PyTorch module" +msgstr "模型——普通的 PyTorch 模块" + +#: ../../source/concepts.rst:31 +msgid "" +"Models are ordinary :class:`torch.nn.Module` objects; fl-sim does not " +"wrap them. Whatever ``candidate_models`` offers or you bring yourself " +"works. The server holds the *global* copy; every sampled client gets its " +"own copy each round, trains it on its local data slice, and sends the " +"result back." +msgstr "" +"模型就是普通的 :class:`torch.nn.Module` 对象,fl-sim 不做任何包装。" +"``candidate_models`` 提供的或你自己带来的模型都可以用。服务器持有 *全局* 副本;" +"每轮每个被采样的客户端拿到自己的副本,在本地数据切片上训练,再把结果发回。" + +#: ../../source/concepts.rst:38 +msgid "Server — the orchestrator" +msgstr "服务器——调度者" + +#: ../../source/concepts.rst:40 +msgid "" +"The :class:`~fl_sim.nodes.Server` owns the global model and the federated" +" schedule (how many rounds, how many clients, how they are sampled), and " +"drives the loop you will see below. It never touches client-local data. " +"Algorithm-specific behavior lives in subclasses: " +":class:`~fl_sim.algorithms.fedopt.FedAvgServer`, " +":class:`~fl_sim.algorithms.fedprox.FedProxServer`, and 15+ others — each " +"customizes a handful of well-defined hooks:" +msgstr "" +":class:`~fl_sim.nodes.Server` 持有全局模型与联邦调度(多少轮、多少客户端、如何采样)," +"并驱动下文所示的循环。它从不触碰客户端本地数据。算法特定的行为都在子类里:" +":class:`~fl_sim.algorithms.fedopt.FedAvgServer`、" +":class:`~fl_sim.algorithms.fedprox.FedProxServer` 等 15 个以上——每个算法只定制" +"少数几个定义良好的钩子:" + +#: ../../source/concepts.rst:48 +msgid "" +"``communicate(target)`` — the **downlink**: what the server sends to a " +"sampled client (typically the current global model);" +msgstr "" +"``communicate(target)``——**下行**:服务器发给被采样客户端的内容(通常是当前全局模型);" + +#: ../../source/concepts.rst:50 +msgid "" +"``update()`` — the **aggregation**: how client replies are turned into " +"the new global model." +msgstr "``update()``——**聚合**:客户端回复如何变成新的全局模型。" + +#: ../../source/concepts.rst:54 +msgid "Client — one participant's local state" +msgstr "客户端——单个参与方的本地状态" + +#: ../../source/concepts.rst:56 +msgid "" +"Each :class:`~fl_sim.nodes.Client` holds its own model copy, its data " +"slice, and its training state. Its hooks are the mirror image of the " +"server's:" +msgstr "" +"每个 :class:`~fl_sim.nodes.Client` 持有自己的模型副本、数据切片与训练状态。" +"它的钩子与服务器互为镜像:" + +#: ../../source/concepts.rst:60 +msgid "" +"``update()`` — the **local training**: fit the received model on local " +"data (local epochs, local optimizer — all from the client config);" +msgstr "" +"``update()``——**本地训练**:在本地数据上拟合收到的模型(本地 epoch 数、本地优化器" +"——全部来自客户端配置);" + +#: ../../source/concepts.rst:62 +msgid "" +"``communicate()`` — the **uplink**: build the message sent back to the " +"server. Messages are typed objects " +"(:class:`~fl_sim.messages.ClientToServer`) carrying the updated model " +"state plus algorithm-specific extras." +msgstr "" +"``communicate()``——**上行**:构建发回服务器的消息。消息是带类型的对象" +"(:class:`~fl_sim.messages.ClientToServer`),携带更新后的模型状态与算法特有的附加内容。" + +#: ../../source/concepts.rst:67 +msgid "Algorithms customize the hooks, not the loop" +msgstr "算法定制钩子,而不是循环" + +#: ../../source/concepts.rst:69 +msgid "" +"fl-sim ships 20 registered algorithms — FedAvg, FedProx, SCAFFOLD, FedPD," +" ProxSkip, Ditto, … — and they all run in the *same* loop. An algorithm " +"is only a pair of config classes plus hook overrides: FedProx changes the" +" client's local objective; SCAFFOLD adds control variates to the uplink; " +"FedPD lets clients *skip* some uplinks entirely. The framework calls the " +"hooks in a fixed order (below), so algorithms compose with the " +"infrastructure — logging, evaluation, reproducibility, parallel execution" +" — without any of them knowing about each other." +msgstr "" +"fl-sim 内置 20 个注册算法——FedAvg、FedProx、SCAFFOLD、FedPD、ProxSkip、Ditto 等——" +"它们全部运行在*同一个*循环里。一个算法只是一对配置类加上若干钩子覆写:FedProx 改动" +"客户端的本地目标函数;SCAFFOLD 在上行里加入控制变量;FedPD 允许客户端*完全跳过*某些" +"上行。框架按固定顺序(见下)调用这些钩子,因此算法与基础设施——日志、评估、可复现性、" +"并行执行——正交组合,互不知晓。" + +#: ../../source/concepts.rst:78 +msgid "" +"Registering your own makes it usable from the CLI and experiment matrices" +" by name:" +msgstr "注册自己的算法后,就能在 CLI 与实验矩阵中按名字使用它:" + +#: ../../source/concepts.rst:91 +msgid "" +"The how-to guide on custom algorithms walks through a complete, runnable " +"example; :doc:`parallel_execution` documents the hook contract that keeps" +" custom algorithms compatible with every executor." +msgstr "" +"自定义算法的 how-to 指南给出了完整可运行的例子;:doc:`parallel_execution` 记录了" +"钩子契约,保证自定义算法与所有执行后端兼容。" + +#: ../../source/concepts.rst:96 +msgid "One round of training" +msgstr "一轮训练" + +#: ../../source/concepts.rst:98 +msgid "" +"``server.train()`` runs ``num_iters`` rounds. Every round proceeds in a " +"fixed order:" +msgstr "``server.train()`` 运行 ``num_iters`` 轮。每轮按固定顺序进行:" + +#: ../../source/concepts.rst:101 +msgid "" +"**Sampling** — the server picks this round's participants (the " +"``clients_sample_ratio`` fraction of ``num_clients``)." +msgstr "" +"**采样**——服务器选出本轮的参与方(``num_clients`` 中按 ``clients_sample_ratio`` " +"比例抽取)。" + +#: ../../source/concepts.rst:103 +msgid "" +"**Uplink decision** — per algorithm: most algorithms have every sampled " +"client respond; communication-skipping algorithms (FedPD, ProxSkip) " +"decide *now*, on the main thread, which clients will upload this round." +msgstr "" +"**上行决策**——因算法而异:多数算法让每个被采样客户端都响应;通信跳过型算法" +"(FedPD、ProxSkip)在 *此刻* 于主线程上决定哪些客户端本轮上传。" + +#: ../../source/concepts.rst:107 +msgid "" +"**Downlink broadcast** — the server sends the global model to each " +"sampled client (``server.communicate(target)``)." +msgstr "" +"**下行广播**——服务器把全局模型发给每个被采样客户端(``server.communicate(target)``)。" + +#: ../../source/concepts.rst:109 +msgid "" +"**Local steps** — each selected client runs evaluate → local training → " +"build the uplink message. Clients never see each other or the global " +"state; they touch only their own slice." +msgstr "" +"**本地步**——每个被选中的客户端依次执行评估 → 本地训练 → 构建上行消息。" +"客户端之间互不可见,也看不到全局状态;它们只触碰自己的数据切片。" + +#: ../../source/concepts.rst:112 +msgid "" +"**Uplink + aggregation** — replies are consumed **in sampled-client " +"order** and the algorithm's ``update()`` produces the new global model. " +"Metrics (server-side and per-client) are logged here." +msgstr "" +"**上行与聚合**——回复 **按被采样客户端的顺序** 被消费,算法的 ``update()`` 产出新的" +"全局模型。指标(服务器侧与每个客户端的)也在这里记录。" + +#: ../../source/concepts.rst:115 +msgid "" +"**Repeat** — for ``num_iters`` rounds; logs and metrics land in " +"``~/.cache/fl-sim/.logs/`` (see :doc:`quickstart`)." +msgstr "" +"**重复**——共 ``num_iters`` 轮;日志与指标写入 ``~/.cache/fl-sim/.logs/`` " +"(参见 :doc:`quickstart`)。" + +#: ../../source/concepts.rst:118 +msgid "" +"Because steps 1–2 and 5 happen on the main thread in fixed order, the " +"*aggregation arithmetic* is identical no matter how the local steps of " +"step 4 are executed — sequentially, or concurrently on a thread pool " +"(:doc:`parallel_execution`). That invariant is what makes parallel runs " +"trustworthy." +msgstr "" +"由于第 1–2 步与第 5 步在主线程上按固定顺序执行,无论第 4 步的本地步是串行执行还是" +"在线程池上并发执行(:doc:`parallel_execution`), *聚合算术* 都完全一致。" +"正是这条不变式让并行运行值得信赖。" + +#: ../../source/concepts.rst:125 +msgid "The cast in one table" +msgstr "一张表看全所有角色" + +#: ../../source/concepts.rst:130 +msgid "Object" +msgstr "对象" + +#: ../../source/concepts.rst:131 +msgid "Configured by" +msgstr "由谁配置" + +#: ../../source/concepts.rst:132 +msgid "Knows about" +msgstr "知晓什么" + +#: ../../source/concepts.rst:133 +msgid "Never touches" +msgstr "绝不触碰" + +#: ../../source/concepts.rst:134 +msgid ":class:`~fl_sim.data_processing.fed_dataset.FedDataset`" +msgstr "" + +#: ../../source/concepts.rst:135 +msgid "constructor args" +msgstr "构造参数" + +#: ../../source/concepts.rst:136 +msgid "raw data, partitioning, dataloaders" +msgstr "原始数据、划分、dataloader" + +#: ../../source/concepts.rst:137 +msgid "training state" +msgstr "训练状态" + +#: ../../source/concepts.rst:138 +msgid ":class:`~fl_sim.nodes.Client`" +msgstr "" + +#: ../../source/concepts.rst:139 +msgid "``ClientConfig`` subclass" +msgstr "``ClientConfig`` 子类" + +#: ../../source/concepts.rst:140 +msgid "its own data slice and model copy" +msgstr "自己的数据切片与模型副本" + +#: ../../source/concepts.rst:141 +msgid "other clients, the global model's evolution" +msgstr "其他客户端、全局模型的演化" + +#: ../../source/concepts.rst:142 +msgid ":class:`~fl_sim.nodes.Server`" +msgstr "" + +#: ../../source/concepts.rst:143 +msgid "``ServerConfig`` subclass" +msgstr "``ServerConfig`` 子类" + +#: ../../source/concepts.rst:144 +msgid "global model, sampling, aggregation, logging" +msgstr "全局模型、采样、聚合、日志" + +#: ../../source/concepts.rst:145 +msgid "client-local data" +msgstr "客户端本地数据" + +#: ../../source/concepts.rst:148 +msgid "Where to go next" +msgstr "接下来去哪里" + +#: ../../source/concepts.rst:150 +msgid ":doc:`quickstart` — run all of this in five rounds on synthetic data." +msgstr ":doc:`quickstart`——在合成数据上用五轮跑通以上全部。" + +#: ../../source/concepts.rst:151 +msgid ":doc:`api/nodes` — the full Server/Client API reference." +msgstr ":doc:`api/nodes`——完整的 Server/Client API 参考。" + +#: ../../source/concepts.rst:152 +msgid "" +":ref:`config reference ` — every config field of every " +"algorithm." +msgstr ":ref:`config reference `——每个算法的全部配置字段。" + +#: ../../source/concepts.rst:154 +msgid ":doc:`viz` — explore the produced logs interactively." +msgstr ":doc:`viz`——交互式地探索产出的日志。" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/examples.po b/docs/source/locale/zh_CN/LC_MESSAGES/examples.po index b0d852eb..e16071f5 100644 --- a/docs/source/locale/zh_CN/LC_MESSAGES/examples.po +++ b/docs/source/locale/zh_CN/LC_MESSAGES/examples.po @@ -6,23 +6,30 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: fl-sim\n" +"Project-Id-Version: fl-sim\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-01 02:12+0800\n" +"POT-Creation-Date: 2026-10-26 13:08+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" -"Language-Team: zh \n" "Language: zh_CN\n" +"Language-Team: zh \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" -"Plural-Forms: nplurals=1; plural=0;\n" -"Generated-By: Babel 2.12.1\n" +"Generated-By: Babel 2.16.0\n" -#: ../../source/examples.rst:2 +#: ../../source/examples.rst:4 msgid "Usage examples" msgstr "使用示例" -#: ../../source/examples.rst:4 -msgid "to write...." -msgstr "待补充……" +#: ../../source/examples.rst:8 +msgid "" +"The usage examples now live in :doc:`quickstart`, with :doc:`concepts` as" +" the conceptual map and task-oriented tutorials building on both." +msgstr "" +"使用示例现在位于 :doc:`quickstart`,:doc:`concepts` 是概念地图," +"任务导向的教程在两者基础上展开。" + +#~ msgid "to write...." +#~ msgstr "待补充……" diff --git a/docs/source/locale/zh_CN/LC_MESSAGES/quickstart.po b/docs/source/locale/zh_CN/LC_MESSAGES/quickstart.po new file mode 100644 index 00000000..81d00924 --- /dev/null +++ b/docs/source/locale/zh_CN/LC_MESSAGES/quickstart.po @@ -0,0 +1,183 @@ +# 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-26 13:08+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/quickstart.rst:4 +msgid "Quickstart" +msgstr "快速上手" + +#: ../../source/quickstart.rst:6 +msgid "" +"This page walks you through your first federated training run with fl-" +"sim: install the package, train :class:`~fl_sim.algorithms.fedopt.FedAvg`" +" on a synthetic dataset, and look at the logs it produces. The whole run " +"takes seconds on a laptop — no dataset download, no GPU required." +msgstr "" +"本页面带你完成第一次联邦训练:安装 fl-sim,在合成数据集上训练 " +":class:`~fl_sim.algorithms.fedopt.FedAvg`,并查看产出的日志。整个过程在笔记本电脑上" +"几秒即可完成——无需下载数据集,也无需 GPU。" + +#: ../../source/quickstart.rst:11 +msgid "" +"If you prefer understanding the concepts first, read :doc:`concepts` and " +"come back." +msgstr "如果你更愿意先了解概念,可以先阅读 :doc:`concepts` 再回来。" + +#: ../../source/quickstart.rst:15 +msgid "Prerequisites" +msgstr "准备条件" + +#: ../../source/quickstart.rst:17 +msgid "" +"Install fl-sim following :doc:`install` (``pip install " +"git+https://github.com/wenh06/fl-sim.git``). Python 3.10+ with PyTorch is" +" all you need." +msgstr "" +"按照 :doc:`install` 安装 fl-sim(``pip install " +"git+https://github.com/wenh06/fl-sim.git``)。只需 Python 3.10+ 与 PyTorch。" + +#: ../../source/quickstart.rst:21 +msgid "Train your first model" +msgstr "训练第一个模型" + +#: ../../source/quickstart.rst:23 +msgid "" +"Every federated run in fl-sim is built from three objects: a **dataset** " +"(the data plus how it is split across clients), a **model** (a PyTorch " +"module, here taken from the dataset's built-in candidates), and a " +"**server** (which owns the global model, samples clients, and " +"aggregates). The server config carries the federated schedule (rounds, " +"client count, sampling ratio); the client config carries the local " +"training recipe." +msgstr "" +"fl-sim 中的每次联邦训练都由三个对象构成: **数据集** (数据本身以及在客户端之间的" +"划分方式)、 **模型** (一个 PyTorch 模块,这里取自数据集内置的候选模型)和" +" **服务器** (持有全局模型,负责采样客户端与聚合)。服务器配置承载联邦训练的调度" +"(轮数、客户端数量、采样比例);客户端配置承载本地训练方案。" + +#: ../../source/quickstart.rst:35 +msgid "" +"Paste this into a script (or a REPL) and run it. ``FedSynthetic`` " +"generates a synthetic classification dataset split across 30 clients — " +"nothing is downloaded. ``FedAvgServerConfig(num_iters=5, num_clients=10, " +"clients_sample_ratio=1.0)`` runs **5 rounds**, each sampling **10 of the " +"30 clients** with all of them responding. ``server.train()`` runs the " +"full federated loop; you will see per-round progress and metrics in the " +"console." +msgstr "" +"把这段代码粘贴到脚本(或 REPL)里运行。``FedSynthetic`` 会在本地生成一个划分给 " +"30 个客户端的合成分类数据集——不下载任何东西。" +"``FedAvgServerConfig(num_iters=5, num_clients=10, clients_sample_ratio=1.0)`` " +"运行 **5 轮**,每轮从 30 个客户端中采样 **10 个**,且全部参与。" +"``server.train()`` 运行完整的联邦循环;你会在控制台看到每轮的进度与指标。" + +#: ../../source/quickstart.rst:44 +msgid "" +"``server.train()`` is ``server.train(\"federated\")``. fl-sim can also " +"run ``\"centralized\"`` and ``\"local\"`` baselines with the same objects" +" — see the :meth:`~fl_sim.nodes.Server.train` API." +msgstr "" +"``server.train()`` 即 ``server.train(\"federated\")``。fl-sim 也可以用同样的对象运行 " +"``\"centralized\"`` 与 ``\"local\"`` 基线——参见 :meth:`~fl_sim.nodes.Server.train` API。" + +#: ../../source/quickstart.rst:49 +msgid "Where do the logs go?" +msgstr "日志在哪里?" + +#: ../../source/quickstart.rst:51 +msgid "" +"Every run writes its logs to ``~/.cache/fl-sim/.logs/`` (override the " +"subdirectory with ``log_dir=`` in the server config, as done here). The " +"script above reads the metrics back and prints the result:" +msgstr "" +"每次运行都会把日志写入 ``~/.cache/fl-sim/.logs/`` (可通过服务器配置里的 " +"``log_dir=`` 指定子目录,如本例所示)。上面的脚本会把指标读回来并打印结果:" + +#: ../../source/quickstart.rst:62 +msgid "Three files per run:" +msgstr "每次运行产生三个文件:" + +#: ../../source/quickstart.rst:64 +msgid "a ``.txt`` log with the human-readable console history;" +msgstr "一个 ``.txt`` 日志,保存人类可读的控制台历史;" + +#: ../../source/quickstart.rst:65 +msgid "" +"a ``.json`` log with the full metrics history, structured as " +"``{\"train\": {...}, \"val\": {\"Server\": [...], \"Client0\": [...], " +"...}}`` — one dict per evaluation, holding ``step``, ``acc``, ``loss``, " +"etc. This is the file you will plot when visualizing results;" +msgstr "" +"一个 ``.json`` 日志,保存完整的指标历史,结构为 " +"``{\"train\": {...}, \"val\": {\"Server\": [...], \"Client0\": [...], " +"...}}``——每次评估对应一个字典,包含 ``step``、``acc``、``loss`` 等字段。" +"可视化结果时用的就是这个文件;" + +#: ../../source/quickstart.rst:69 +msgid "" +"``config.json`` — the exact server and client configs of the run, so " +"every experiment can be reproduced later." +msgstr "" +"``config.json``——本次运行的确切服务器与客户端配置,便于日后复现每个实验。" + +#: ../../source/quickstart.rst:74 +msgid "" +"The ``step`` in the logs counts **communications**, not rounds: each " +"round adds one downlink broadcast plus one uplink reply per sampled " +"client (10 sampled clients → step advances by 20 per round here). The " +"json snapshot is taken at the start of each round, so the final " +"communication count is higher than the last logged step." +msgstr "" +"日志中的 ``step`` 统计的是**通信次数**,而不是轮数:每轮对每个被采样客户端包含一次" +"下行广播与一次上行回复(这里每轮采样 10 个客户端,因此 step 每轮前进 20)。json " +"中的快照记录于每轮开始时,因此最终通信次数会大于最后一条日志的 step。" + +#: ../../source/quickstart.rst:80 +msgid "" +"(The exact accuracy value may differ across machines and package " +"versions; on one machine this run ends at 0.8767, up from 0.16 at " +"initialization.)" +msgstr "" +"(具体精度数值可能因机器与包版本而异;在一种机器上,该运行的最终精度为 0.8767," +"初始时为 0.16。)" + +#: ../../source/quickstart.rst:84 +msgid "Next steps" +msgstr "下一步" + +#: ../../source/quickstart.rst:86 +msgid "" +":doc:`concepts` — what actually happens in each round: sampling, " +"broadcast, local updates, aggregation." +msgstr ":doc:`concepts`——每一轮实际发生了什么:采样、广播、本地更新、聚合。" + +#: ../../source/quickstart.rst:88 +msgid "" +":doc:`cli` — run experiments from the command line with YAML config files" +" (a full tutorial on experiment batches is on the way)." +msgstr "" +":doc:`cli`——用 YAML 配置文件从命令行运行实验(关于实验批次的完整教程也在准备中)。" + +#: ../../source/quickstart.rst:90 +msgid "" +":doc:`parallel_execution` — speed training up with a parallel executor " +"backend while keeping results reproducible." +msgstr "" +":doc:`parallel_execution`——用并行执行后端加速训练,同时保持结果可复现。" diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 00000000..896e3610 --- /dev/null +++ b/docs/source/quickstart.rst @@ -0,0 +1,91 @@ +.. _quickstart: + +Quickstart +========== + +This page walks you through your first federated training run with fl-sim: +install the package, train :class:`~fl_sim.algorithms.fedopt.FedAvg` on a +synthetic dataset, and look at the logs it produces. The whole run takes +seconds on a laptop — no dataset download, no GPU required. + +If you prefer understanding the concepts first, read :doc:`concepts` and +come back. + +Prerequisites +------------- + +Install fl-sim following :doc:`install` (``pip install git+https://github.com/wenh06/fl-sim.git``). +Python 3.10+ with PyTorch is all you need. + +Train your first model +---------------------- + +Every federated run in fl-sim is built from three objects: a **dataset** +(the data plus how it is split across clients), a **model** (a PyTorch +module, here taken from the dataset's built-in candidates), and a +**server** (which owns the global model, samples clients, and aggregates). +The server config carries the federated schedule (rounds, client count, +sampling ratio); the client config carries the local training recipe. + +.. literalinclude:: ../../test-files/quickstart.py + :language: python + :start-after: # --- begin: core --- + :end-before: # --- end: core --- + +Paste this into a script (or a REPL) and run it. ``FedSynthetic`` generates +a synthetic classification dataset split across 30 clients — nothing is +downloaded. ``FedAvgServerConfig(num_iters=5, num_clients=10, +clients_sample_ratio=1.0)`` runs **5 rounds**, each sampling **10 of the 30 +clients** with all of them responding. ``server.train()`` runs the full +federated loop; you will see per-round progress and metrics in the console. + +.. note:: + + ``server.train()`` is ``server.train("federated")``. fl-sim can also run + ``"centralized"`` and ``"local"`` baselines with the same objects — see + the :meth:`~fl_sim.nodes.Server.train` API. + +Where do the logs go? +--------------------- + +Every run writes its logs to ``~/.cache/fl-sim/.logs/`` (override the +subdirectory with ``log_dir=`` in the server config, as done here). The +script above reads the metrics back and prints the result: + +.. code-block:: text + + log file: FedAvg-FedSynthetic-MLP_10-26_12-52.json + log file: FedAvg-FedSynthetic-MLP_10-26_12-52.txt + log file: config.json + Latest logged val/acc: 0.8767 (step 80) + +Three files per run: + +- a ``.txt`` log with the human-readable console history; +- a ``.json`` log with the full metrics history, structured as + ``{"train": {...}, "val": {"Server": [...], "Client0": [...], ...}}`` + — one dict per evaluation, holding ``step``, ``acc``, ``loss``, etc. + This is the file you will plot when visualizing results; +- ``config.json`` — the exact server and client configs of the run, so + every experiment can be reproduced later. + +.. note:: + + The ``step`` in the logs counts **communications**, not rounds: each + round adds one downlink broadcast plus one uplink reply per sampled + client (10 sampled clients → step advances by 20 per round here). The + json snapshot is taken at the start of each round, so the final + communication count is higher than the last logged step. + +(The exact accuracy value may differ across machines and package versions; +on one machine this run ends at 0.8767, up from 0.16 at initialization.) + +Next steps +---------- + +- :doc:`concepts` — what actually happens in each round: sampling, + broadcast, local updates, aggregation. +- :doc:`cli` — run experiments from the command line with YAML config + files (a full tutorial on experiment batches is on the way). +- :doc:`parallel_execution` — speed training up with a parallel executor + backend while keeping results reproducible. diff --git a/test-files/quickstart.py b/test-files/quickstart.py new file mode 100644 index 00000000..9adedbb4 --- /dev/null +++ b/test-files/quickstart.py @@ -0,0 +1,52 @@ +""" +Minimal federated training run: FedAvg on synthetic data. + +This script is the source of the code blocks shown in the Quickstart +page of the documentation (``docs/source/quickstart.rst``): the marked +sections are included verbatim via Sphinx ``literalinclude`` directives, +so the docs and this script can never drift apart. The size is pinned to +the tutorial budget (``num_iters <= 5``, ``num_clients <= 30``, one local +epoch), enforced by ``test/test_tutorial_scripts.py``. +""" + +# --- begin: core --- +from fl_sim.algorithms.fedopt import FedAvgClientConfig, FedAvgServer, FedAvgServerConfig +from fl_sim.data_processing.fed_synthetic import FedSynthetic + +# a synthetic dataset split across 30 clients (no download needed) +dataset = FedSynthetic(1.0, 1.0, True, 30) +model = dataset.candidate_models["mlp_d1"] + +server = FedAvgServer( + model, + dataset, + FedAvgServerConfig( + num_iters=5, + num_clients=10, + clients_sample_ratio=1.0, + log_dir="quickstart", # logs land in ~/.cache/fl-sim/.logs/quickstart/ + ), + FedAvgClientConfig(batch_size=32, num_epochs=1), +) +server.train() +# --- end: core --- + +# --- begin: read-logs --- +import json +from pathlib import Path + +# the JSON metrics logger buffers in memory; flush it to materialize the file +server._logger_manager.flush() + +log_dir = Path(server._logger_manager.log_dir) +for log_file in sorted(log_dir.iterdir()): + print("log file:", log_file.name) + +# one json file holds the metrics history, config.json holds the run configs +metrics_file = next(p for p in log_dir.glob("*.json") if p.name != "config.json") +with metrics_file.open() as f: + metrics = json.load(f) + +latest = metrics["val"]["Server"][-1] +print(f"Latest logged val/acc: {latest['acc']:.4f} (step {latest['step']})") +# --- end: read-logs --- diff --git a/test/test_tutorial_scripts.py b/test/test_tutorial_scripts.py new file mode 100644 index 00000000..b731820d --- /dev/null +++ b/test/test_tutorial_scripts.py @@ -0,0 +1,106 @@ +"""Behavioral smoke tests for the tutorial scripts in ``test-files/``. + +The scripts are the source of truth for the code blocks shown in the +documentation: ``docs/source/*.rst`` embeds them via Sphinx +``literalinclude`` directives, so a green run here means the code the +documentation shows actually executes and produces what the docs promise. + +The runs are pinned to the tutorial budget (``server.num_iters <= 5``, +``server.num_clients <= 30``, ``client.num_epochs == 1``), asserted on +the *runtime* values each run writes to its ``config.json`` — so the +"tutorial-sized runs" property is guarded even if someone edits the +scripts later. + +Each run is isolated: fl-sim's log directory is redirected into a +temporary directory, and ``sys.path`` is restored after ``runpy``. +""" + +import json +import re +import runpy +import sys +from pathlib import Path + +import pytest + +import fl_sim.utils.loggers as loggers_mod + +REPO_ROOT = Path(__file__).resolve().parents[1] +DOCS_SOURCE = REPO_ROOT / "docs" / "source" + +TUTORIAL_SCRIPTS = { + "quickstart": REPO_ROOT / "test-files" / "quickstart.py", +} + + +@pytest.fixture() +def isolated_log_dir(tmp_path, monkeypatch): + """Redirect fl-sim's default log directory into a temporary directory.""" + monkeypatch.setattr(loggers_mod, "DEFAULT_LOG_DIR", tmp_path) + yield tmp_path + + +def _run_script(script: Path) -> None: + """Execute a tutorial script in-process, restoring ``sys.path`` afterwards. + + ``run_name="__main__"`` is required: without it the scripts' + module-level code still runs under ``runpy``, but any + ``if __name__ == "__main__":`` guard would silently not execute. + """ + saved_sys_path = list(sys.path) + try: + runpy.run_path(str(script), run_name="__main__") + finally: + sys.path[:] = saved_sys_path + + +def test_quickstart_script_runs_and_respects_tutorial_budget(isolated_log_dir): + script = TUTORIAL_SCRIPTS["quickstart"] + _run_script(script) + + run_dir = isolated_log_dir / "quickstart" + assert run_dir.is_dir(), "the quickstart run should log into its own subdirectory" + + # tutorial budget, asserted on the values the run actually used + config = json.loads((run_dir / "config.json").read_text()) + assert config["server"]["num_iters"] <= 5 + assert config["server"]["num_clients"] <= 30 + assert config["client"]["num_epochs"] == 1 + + # the docs promise one metrics json + one txt log per run + metrics_files = [p for p in run_dir.glob("*.json") if p.name != "config.json"] + assert len(metrics_files) == 1 + assert metrics_files[0].with_suffix(".txt").exists() + + # and the script's own promise: a readable latest val accuracy + metrics = json.loads(metrics_files[0].read_text()) + latest = metrics["val"]["Server"][-1] + assert 0.0 <= latest["acc"] <= 1.0 + + +def test_tutorial_scripts_keep_literalinclude_markers(): + """The markers delimit the fragments the docs include; dropping one + silently empties a documentation code block.""" + for name, script in TUTORIAL_SCRIPTS.items(): + src = script.read_text() + markers = re.findall(r"^# --- (begin|end): ([a-z-]+) ---$", src, re.MULTILINE) + begins = [tag for mode, tag in markers if mode == "begin"] + ends = [tag for mode, tag in markers if mode == "end"] + assert begins, f"{name}: no literalinclude markers" + assert begins == ends, f"{name}: unbalanced begin/end markers {begins} vs {ends}" + + +def test_documented_literalinclude_targets_exist(): + """Every ``literalinclude`` in the docs must point at an existing file — + a moved script would otherwise leave an empty code block on the site.""" + pattern = re.compile(r"^\.\.\s+literalinclude::\s+(\S+)", re.MULTILINE) + checked = 0 + for rst in DOCS_SOURCE.rglob("*.rst"): + for match in pattern.finditer(rst.read_text()): + target = match.group(1) + if target.startswith(("http://", "https://")): + continue + resolved = (rst.parent / target).resolve() + assert resolved.exists(), f"{rst.relative_to(DOCS_SOURCE)}: literalinclude target missing: {target}" + checked += 1 + assert checked >= 1, "expected at least one literalinclude in the docs"