ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

exo 分布式 AI 推理系统开发指南:构建运行、预提交检查与节点架构剖析(基于仓库 CLAUDE.md)

exo 分布式 AI 推理系统开发指南:构建运行、预提交检查与节点架构剖析(基于仓库 CLAUDE.md) exo 分布式 AI 推理系统开发指南构建运行、预提交检查与节点架构剖析基于仓库 CLAUDE.md【免费下载链接】exoRun frontier AI locally.项目地址: https://gitcode.com/GitHub_Trending/exo8/exo本文以仓库根目录的 CLAUDE.md与 AGENTS.md 内容一致的 AI 编码代理指南为主体系统讲解 exo 这套分布式 AI 推理系统「怎么构建、怎么跑、怎么过 CI、内部如何协作」。读完本文你可以完整掌握 exo 的构建与运行命令、提交前必过的四类检查类型检查 / Lint / 格式化 / 测试以及单节点内 Router、Worker、Master、Election、API 五大组件的协作关系与事件溯源Event Sourcing状态管理机制并了解其严格的 Python 代码风格约束与 Dashboard 截图测试流程。一、项目概览exo 是什么CLAUDE.md 对项目的定位非常明确exo is a distributed AI inference system that connects multiple devices into a cluster. It enables running large language models across multiple machines using MLX as the inference backend and zenoh for peer-to-peer networking.即exo 是一个把多台设备组建成集群的分布式 AI 推理系统以MLX 作为推理后端、以zenoh 作为点对点网络层让大语言模型可以跨多机运行。这一点可以从 pyproject.toml 的依赖配置得到印证mlx0.32.0、mlx-lm、mlx-vlm等推理依赖以可选依赖组mlx、mlx-cpu、mlx-cuda12、mlx-cuda13形式提供并通过conflicts声明这些 extra 互斥核心运行依赖则包括fastapiOpenAI 兼容 API、pydantic严格类型模型、huggingface-hub模型下载、loguru日志等。项目入口由[project.scripts]声明exo exo.main:main对应 src/exo/main.py 中的main()函数。需要说明的适用前提pyproject.toml 中requires-python 3.13.*即 Python 严格锁定 3.13当前版本为0.3.70。环境管理统一使用 uv且[tool.uv.workspace]将rust/exo_rs、bench、tools纳入同一个 workspace。二、构建与运行命令CLAUDE.md 给出的完整构建/运行命令如下本文逐条补充其背后的实现依据# 构建 dashboard运行 exo 前必须先执行 cd dashboard npm install npm run build cd .. # 运行 exo同时启动 master 与 workerAPI 位于 http://localhost:52415 uv run exo # 详细日志 uv run exo -v # 或 -vv 更详细 # 运行测试默认排除 slow 测试 uv run pytest # 运行全部测试含 slow uv run pytest -m # 运行指定测试文件 uv run pytest src/exo/shared/tests/test_election.py # 运行指定测试函数 uv run pytest src/exo/shared/tests/test_election.py::test_function_name # 类型检查strict 模式 uv run basedpyright # Lint uv run ruff check # 格式化基于 nix nix fmt这些命令与 justfile 中的 recipe 一一对应just build-dashboard执行npm install npm run buildjust test执行uv run pytest srcjust lint执行uv run ruff check --fixjust check执行uv run basedpyright --project pyproject.tomljust fmt优先使用treefmt、回退到nix fmt。2.1 为什么必须先构建 DashboardCLAUDE.md 强调 dashboard 构建是运行 exo 的前置条件。从源码结构看Dashboard 是dashboard/下的 Svelte 5 TypeScript 前端构建产物位于dashboard/build/并由 API 服务直接托管uv run exo启动后访问 http://localhost:52415 即可看到界面。若跳过构建API 侧将没有静态资源可服务。2.2uv run exo到底启动了什么从 src/exo/main.py 的Node数据类可以看到uv run exo启动的并不是「master 或 worker」二选一而是一个同时装配多个组件的Nodedataclass class Node: router: Router event_router: EventRouter download_coordinator: DownloadCoordinator | None worker: Worker | None election: Election # 每个节点都参与选举 election_result_receiver: Receiver[ElectionResult] master: Master | None api: API | NoneNode.create(args)src/exo/main.py依次完成创建 zenohRouter并注册全部 pub/sub 主题 → 建立EventRouter事件索引与分发中枢→ 按--no-downloads决定是否创建DownloadCoordinator→ 按--no-api决定是否创建 FastAPIAPI→ 按--no-worker决定是否创建Worker→无条件创建Master注释写明「We start every node with a master」→ 创建Election。之后Node.run()用 anyioTaskGroup并发跑起各组件的run()循环。值得注意的是Args中spawn_api: bool False的默认值API 是否启动由--no-api取反控制--no-api是actionstore_false默认store_true行为即默认开启 API监听 52415 端口。2.3 完整命令行参数参考CLAUDE.md 只列出了-v/-vv等少数参数而 src/exo/main.py 的Args.parse()定义了一个远比文档更完整的参数面这里补全如下参数作用默认值 / 备注-q, --quiet静默将 verbosity 置为 -1-v, --verbose可叠加详细日志默认 0-vv更详细-m, --force-master强制成为 master从源码看实现方式为把选举seniority提到 1,000,000src/exo/main.py--no-api不启动 API 服务默认启动--api-portAPI 端口默认 52415--no-worker不启动 worker默认启动--no-downloads禁用下载协调器节点不下载模型默认启用--offline离线/气隙模式跳过联网检查仅用本地预置模型也可由环境变量EXO_OFFLINEtrue控制--no-batch禁用连续批处理顺序生成置EXO_NO_BATCH1--fast-synch/--no-fast-synch强制开/关 MLX FAST_SYNCH互斥参数组默认 auto分别映射到EXO_FAST_SYNCH环境变量供 runner 子进程读取--legacy-daemonSysV 风格双 fork 守护进程化默认关闭代码中保留 stdio 到 /dev/null 以兼容 multiprocessing spawn--bootstrap-peers启动时拨号的逗号分隔 multiaddr 列表支持EXO_BOOTSTRAP_PEERS环境变量注意main_inner中该参数目前会直接raise ValueError(Bootstrap peers has been temporarily removed)--namespace发现命名空间不同 namespace 的节点互不连接默认取__version__代码还提示旧变量EXO_LIBP2P_NAMESPACE已移除应改用EXO_ZENOH_NAMESPACE--zenoh-portzenoh 监听 TCP 端口默认 52414--discovery-port发现服务 UDP 端口默认 52413另外进程启动时会通过exo_rs的Pidfile抢占 PID 文件锁src/exo/main.py拿不到锁直接退出保证同一台机器只有一个 exo 实例main_inner()还会把文件描述符上限RLIMIT_NOFILE提升到 65535并将 multiprocessing 启动方式强制设为spawn。三、预提交检查必须通过CLAUDE.md 用加粗强调提交前必须跑完以下四项检查否则 CI 会失败# 1. 类型检查 —— 必须 0 错误 uv run basedpyright # 2. Lint —— 必须通过 uv run ruff check # 3. 格式化 —— 必须已应用 nix fmt # 4. 测试 —— 必须通过 uv run pytest以及一条串行执行的组合命令uv run basedpyright uv run ruff check nix fmt uv run pytest若nix fmt改动了任何文件提交前必须把它们一并暂存。CI 会执行nix flake check其中包含格式校验、lint 以及 Rust 测试。这四项检查在 pyproject.toml 与 flake.nix 中都有对应的严格配置可以佐证basedpyrighttypeCheckingMode strict、failOnWarnings true并把reportAny、reportUnknownVariableType、reportMissingParameterType、reportMissingTypeStubs、reportInvalidCast等全部提升为 error检查范围为src、bench、toolspythonVersion 3.13。这意味着「绕过类型检查器」在 exo 里不仅是风格问题更是硬性门槛。ruff在默认规则上追加I, N, B, A, PIEE, SIM等规则族import 排序、命名、bugbear、别名、Simplifiable并排除.typings/**、rust/exo_rs/**等目录。格式化flake.nix 引入treefmt-nix聚合nixpkgs-fmt、rustfmt、shfmt等 formatter因此nix fmt是 Nix 层面的统一格式化入口。pytestpyproject.toml 的[tool.pytest.ini_options]配置了asyncio_mode auto、标记slow: marks tests as slow (deselected by default)、自动注入环境变量EXO_TESTS1以及addopts -m not slow --ignoretests --ignoretmp——这解释了 CLAUDE.md 中「默认排除 slow 测试」「用pytest -m 清空标记选择以跑全部测试」的原因。四、架构单个 Node 如何组成一个集群4.1 节点组件构成CLAUDE.md 的「Node Composition」一节指出单个 exoNodesrc/exo/main.py运行多个组件Router基于 zenoh 的 pub/sub 消息层经由 Rust 绑定exo_rs提供。对应 rust/networking/src/lib.rs该 crate 基于tokiozenohSession 构建包含discovery对等发现与swarm两个模块并内置 zidzenoh id≤32 位 hex 字符串校验与Config构造逻辑。Worker处理推理任务、下载模型、管理 runner 子进程位于 src/exo/worker/。Master协调集群状态、把模型实例放置placement到各个节点位于 src/exo/master/。ElectionCLAUDE.md 描述为「Bully 算法的 master 选举」实现见 src/exo/shared/election.py测试见 src/exo/shared/tests/test_election.py。从Node.create的注释看每个节点都参与选举——即使某节点不是 master 候选在没有候选者时它也可能被选为 master。APIFastAPI 服务提供 OpenAI 兼容的 chat completions位于 src/exo/api/含chat_completions.py、claude.py、ollama.py、responses.py等适配器。Node._elect_loop()src/exo/main.py展示了 master 身份变化时的完整编排逻辑当选主节点变化is_new_master时先重建EventRouter再分别「维持自我 / 提升自我promote/ 降级自我demote」——晋升时动态构造并start_soon一个新的Master降级时await self.master.shutdown()并置空同时按新 session 重建DownloadCoordinator与Worker并对 API 执行reset/unpause。从源码结构看这套「随选举结果重建子系统」的机制正是集群在无中心协调下自动换主的基础。4.2 消息流typed pub/sub 主题组件之间通过带类型的 pub/sub 主题通信定义于 src/exo/routing/topics.py。CLAUDE.md 列出五个主题GLOBAL_EVENTSMaster 向所有 worker 广播带索引的事件LOCAL_EVENTSWorker 向 master 发送事件以待索引COMMANDSWorker / API 向 master 发送命令ELECTION_MESSAGES选举协议消息CONNECTION_MESSAGESzenoh 连接状态更新。对照源码仓库实际还定义了第六个主题DOWNLOAD_COMMANDS下载命令的 pub/sub 通道src/exo/main.py 中亦有register_topicCLAUDE.md 的列举未覆盖它。每个主题都是一个TypedTopic实例绑定三样东西GLOBAL_EVENTS TypedTopic(global_events, PublishPolicy.Always, GlobalForwarderEvent) CONNECTION_MESSAGES TypedTopic(connection_messages, PublishPolicy.Never, ConnectionMessage)主题名字符串PublishPolicyNever/Minimal/Always是否发布到网络——例如CONNECTION_MESSAGES策略为Never纯本地消息其余均为AlwaysPydantic 模型类序列化采用model_dump_json().encode()反序列化用model_validate_json()。Node.create中的注册顺序GLOBAL_EVENTS→LOCAL_EVENTS→COMMANDS→ELECTION_MESSAGES→CONNECTION_MESSAGES→DOWNLOAD_COMMANDS与主题定义一一对应每个主题都会同时创建 sender发与 receiver收通道交给各组件。4.3 事件溯源State 与 apply()CLAUDE.md 指出系统用事件溯源event sourcing管理状态三个关键要素全部可以在源码中核对State不可变状态对象src/exo/shared/types/state.py 中State(FrozenModel)使用strictTrue、extraforbid的 Pydantic 配置字段涵盖instances、runners、downloads、tasks、topology、last_seen、各类节点维度映射内存/磁盘/系统/网络/雷雳/rdma_ctl/后端以及last_event_applied_idx等——正是集群拓扑、节点画像与任务状态的完整快照。apply()纯函数src/exo/shared/apply.py 中event_apply(event, state)用 Pythonmatch语句对 20 余种事件InstanceCreated、TaskStatusUpdated、NodeTimedOut、TopologyEdgeCreated、RunnerStatusUpdated等逐一分发到apply_*纯函数全部通过state.model_copy(update...)生成新状态从不原地修改外层apply()还做顺序断言state.last_event_applied_idx必须等于event.idx - 1即事件只能按索引顺序被应用——这与 Master「索引事件再广播」、Worker「按序回放」的分工完全吻合。索引与广播src/exo/routing/event_router.py 的EventRouter持有OrderedBuffer事件缓冲与 session 概念承担「本地事件上行LOCAL_EVENTS、全局事件下行GLOBAL_EVENTS、内部按序分发internal_outbound」的枢纽职责并带有 NACK 重试_nack_base_seconds、_nack_cap_seconds等退避参数。4.4 关键类型层级CLAUDE.md 列出的共享类型位于 src/exo/shared/types/均为 Pydantic 模型events.py事件类型discriminated union即按字段区分的联合类型commands.py命令类型tasks.pyworker 执行的任务类型state.py集群状态模型。此外该目录还包含worker/runners、instances、shards、downloads 等子模块、topology.py、profiling.py、thunderbolt.py等构成 Master 放置决策见 src/exo/master/placement.py 及其测试 src/exo/master/tests/test_placement.py所依赖的数据模型层。4.5 Rust 组件CLAUDE.md 列出rust/目录下的组件为networkingzenoh 组网gossipsub、对等发现、exo_rsPyO3 绑定把 Rust 暴露给 Python以及system_custodian系统级操作。以当前仓库快照为准rust/ 下可以确认的两个 crate 是rust/networkingzenoh 组网实现含discovery、swarm模块与 examples/heartbeat.rs、put_string.rs、serve_storage.rs、z_get.rs等可直接运行的示例rust/exo_rsPyO3 绑定rust/exo_rs/src/lib.rs 中注册了networking与pidfile两个子模块Python 侧from exo_rs import Pidfile, PidfileError即由此而来并通过pyo3_stub_gen生成.pyi存根rust/exo_rs/exo_rs.pyi供 basedpyright 严格检查使用。justfile 中的just rust-rebuild展示了重建流程stub_gen生成存根后uv sync --reinstall-package exo_rs。4.6 Dashboarddashboard/是 Svelte 5 TypeScript 前端路由位于 dashboard/src/routes/含advanced、downloads、integrations、traces等页面组件库在 dashboard/src/lib/components/拓扑图TopologyGraph.svelte、模型选择ModelPickerModal.svelte、预填充/解码分离视图PrefillDecodeDisaggregation.svelte等stores 使用 Svelte 5 的.svelte.ts响应式 store。构建输出到dashboard/build/并由 API 托管。五、代码风格要求CLAUDE.md 引述自.cursorrules的风格约定注意该规则文件本身未包含在当前仓库快照中以下以文档引述为准严格、穷尽的类型标注——绝不绕过类型检查器枚举式集合用Literal[...]基础类型别名用typing.NewTypePydantic 模型使用frozenTrue与strictTrue与 State 的实际配置一致副作用通过「纯函数 可注入的 effect handler」组织命名具描述性禁止缩写或三字母缩写只在能有意义地处理异常时才捕获异常尽可能使用final与不可变性。这些约定与 basedpyright 的 strict 配置、FrozenModel见 src/exo/utils/pydantic_ext.py在工程上形成了闭环风格不是口号而是每次uv run basedpyright都会被机器验证。六、测试约定测试框架为 pytest pytest-asyncioasyncio_mode autoasync 测试无需手写pytest.mark.asyncio测试目录与被测代码并列如 src/exo/shared/tests/test_election.py、src/exo/master/tests/、src/exo/routing/tests/、tests/ 下的多节点集成测试test_2node.py、test_4node.py测试期间自动设置环境变量EXO_TESTS1由[tool.pytest.ini_options]的env项注入慢速测试用pytest.mark.slow标记默认被-m not slow排除。选举逻辑的测试 test_election.py 提供了一个很好的细节样本其辅助函数构造的ElectionMessage携带clock、seniority与提议的SessionIdmaster_node_id election_clock与 src/exo/shared/election.py 中「提议带 session 的选举协议」实现相互印证。七、Dashboard UI 测试与截图CLAUDE.md 用单独一节描述了无头浏览器截图流程这里完整保留并稍作说明。7.1 构建并运行# 构建 dashboard运行 exo 前必须完成 cd dashboard npm install npm run build cd .. # 启动 exodashboard 位于 http://localhost:52415 uv run exo sleep 8 # 等待服务启动7.2 用 Playwright 做无头截图使用 headless Chromium 完成程序化截图无需手工操作浏览器。一次性安装npx --yes playwright install chromium cd /tmp npm init -y npm install playwright截图脚本在安装了 playwright 的/tmp目录下用node -e运行// 从 /tmp 运行cd /tmp node -e ... const { chromium } require(playwright); (async () { const browser await chromium.launch({ headless: true }); const page await browser.newPage({ viewport: { width: 1280, height: 800 } }); await page.goto(http://localhost:52415, { waitUntil: networkidle }); await page.waitForTimeout(2000); // 需要时可向 localStorage 注入测试数据例如最近使用的模型 await page.evaluate(() { localStorage.setItem(exo-recent-models, JSON.stringify([ { modelId: mlx-community/Qwen3-30B-A3B-4bit, launchedAt: Date.now() }, ])); }); await page.reload({ waitUntil: networkidle }); await page.waitForTimeout(2000); // 与 UI 元素交互 await page.locator(textSELECT MODEL).click(); await page.waitForTimeout(1000); // 截图 await page.screenshot({ path: /tmp/screenshot.png, fullPage: false }); await browser.close(); })();要点waitUntil: networkidle等待 dashboard 与 API 的轮询流量平息通过localStorage注入「最近模型」等状态可以让截图呈现可控的数据形态page.locator(...).click()则演示了与 Svelte 组件如模型选择器的程序化交互。7.3 将截图附到 PR 评论由于 GitHub 的 API 不支持直接向 PR 评论上传图片CLAUDE.md 给出的绕行方案是「把图片提交到分支 → 用永久 commit SHA 发评论 → 从分支删除图片评论仍能渲染」# 1) 临时把图片提交到分支 cp /tmp/screenshot.png . git add screenshot.png git commit -m temp: add screenshots for PR git push origin branch COMMIT_SHA$(git rev-parse HEAD) # 2) 发 PR 评论引用基于 commit SHA 的 raw 图片地址 gh pr comment PR_NUMBER --body ![Screenshot](https://raw.githubusercontent.com/exo-explore/exo/${COMMIT_SHA}/screenshot.png) # 3) 从分支移除图片评论中的图片因引用永久 SHA 而仍可见 git rm screenshot.png git commit -m chore: remove temporary screenshot files git push origin branch八、小结CLAUDE.md 表面上是一份面向 AI 编码代理的操作手册实质上浓缩了 exo 的完整工程契约入口是uv run exo装配出的多组件Node网络层是 zenoh 六个 typed 主题状态层是「Master 索引 apply()纯函数回放」的事件溯源质量门槛是 basedpyright strict ruff nix fmt pytest 四项预提交检查。对贡献者或代理而言照本文第二、三节执行命令即可跑通开发闭环对希望深入阅读源码的读者建议从 src/exo/main.py 的Node.create/_elect_loop、src/exo/routing/topics.py、src/exo/shared/apply.py 三个文件入手即可把「一条uv run exo命令」到「集群状态机」的整条链路串起来。【免费下载链接】exoRun frontier AI locally.项目地址: https://gitcode.com/GitHub_Trending/exo8/exo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进