Compare commits

..
3 Commits
Author SHA1 Message Date
mlpy0 21a54c5ea0 docs: request the mlx extra in the documented setup commands (#2245)
Since #2087 moved mlx, mlx-lm, mlx-vlm and mflux out of `dependencies`
into the `mlx` extra, the documented setup path never installs them, and
`[tool.uv]` sets no default extras. `uv run exo` starts the API, then
every runner crashes with `ModuleNotFoundError: No module named 'mlx'`
(#2156).

Adds the extra to the commands documented as the way to set up:

- README macOS: `uv sync --extra mlx`
- README Linux: `uv sync --extra mlx-cpu`, with the mlx-cuda12 /
mlx-cuda13 alternatives
- CONTRIBUTING.md quick start
- `just sync` / `just sync-clean`, which `just build-app` runs before
pyinstaller — the spec aborts when the mlx Metal libraries are missing

Docs plus two justfile recipes; no dependency or lock changes. Backend
choice stays explicit, since mlx-cpu / mlx-cuda12 / mlx-cuda13 /
mlx-none are declared as conflicting extras.

#2234 overlaps on the macOS README block only, via a setup script that
runs `uv sync --extra mlx`; the Linux block, CONTRIBUTING.md and the
justfile are not covered there.
2026-08-25 18:59:53 +00:00
b5375f8cee Add Kimi K2.7-Code model card (official INT4 weights + vision) (#2167)
Adds a model card for
[moonshotai/Kimi-K2.7-Code](https://huggingface.co/moonshotai/Kimi-K2.7-Code),
released 2026-06-12.

Same architecture as Kimi K2.6 (`kimi_k25`, 61 layers, official INT4),
so the card mirrors the existing `moonshotai--Kimi-K2.6.toml`. Sampling
defaults per the model card (temperature 1.0 / top_p 0.95 for thinking
mode).

**Vision:** the official repo ships MoonViT weights inline, so I
extracted the 335 `vision_tower.*` / `mm_projector.*` tensors
(unmodified bf16) into
[aidiffuser/Kimi-K2.7-Code-vision](https://huggingface.co/aidiffuser/Kimi-K2.7-Code-vision),
following the `exolabs/Kimi-K2.6-vision` format. The vision config is
byte-identical to K2.6's; the extraction script is included in the repo
for verification. Happy to have this re-hosted under the exolabs org if
you prefer — it's a one-line change to the card.

**Tested:** distributed serving on 2× Mac Studio M3 Ultra (512 GB),
tensor parallelism, text + thinking + image understanding all confirmed
working.

Co-authored-by: aidiffuser <your-noreply-email@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-22 13:38:54 +00:00
OrbisAI Security cdf1add867 fix: upgrade devalue to 5.6.2 (CVE-2026-22774) (#2150)
## Summary
Upgrade devalue from 5.5.0 to 5.6.2 to fix CVE-2026-22774.

## Vulnerability
| Field | Value |
|-------|-------|
| **ID** | CVE-2026-22774 |
| **Severity** | HIGH |
| **Scanner** | trivy |
| **Rule** | `CVE-2026-22774` |
| **File** | `dashboard/package-lock.json` |
| **Assessment** | Likely exploitable |

**Description**: devalue: devalue: Denial of Service due to excessive
resource consumption from untrusted input

## Evidence

**Scanner confirmation**: trivy rule `CVE-2026-22774` flagged this
pattern.

**Production code**: This file is in the production codebase, not
test-only code.

## Threat Model Context

This is a web service - vulnerabilities in request handlers are directly
exploitable by remote attackers.

## Changes
- `dashboard/package.json`
- `dashboard/package-lock.json`

## Verification
- [x] Build passes
- [x] Scanner re-scan confirms fix
- [x] LLM code review passed

---
*This change addresses a pattern flagged by static analysis. The code
path handles user-influenced input and the fix reduces the attack
surface against both manual and automated exploitation.*

---
*Automated security fix by [OrbisAI Security](https://orbisappsec.com)*
2026-06-22 13:29:15 +00:00
41 changed files with 710 additions and 1431 deletions

No files matched your search

+1
View File
@@ -29,6 +29,7 @@ To run EXO from source:
git clone https://github.com/exo-explore/exo.git
cd exo/dashboard
npm install && npm run build && cd ..
uv sync --extra mlx
uv run exo
```
Generated
+29 -54
View File
@@ -1007,7 +1007,6 @@ dependencies = [
"serde_json",
"tokio",
"zenoh",
"zenoh-ext",
]
[[package]]
@@ -1843,12 +1842,6 @@ dependencies = [
"spin 0.9.8",
]
[[package]]
name = "leb128"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545"
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -5114,7 +5107,7 @@ dependencies = [
[[package]]
name = "zenoh"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"ahash",
"arc-swap",
@@ -5164,7 +5157,7 @@ dependencies = [
[[package]]
name = "zenoh-buffers"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"zenoh-collections",
]
@@ -5172,7 +5165,7 @@ dependencies = [
[[package]]
name = "zenoh-codec"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"tracing",
"uhlc",
@@ -5183,7 +5176,7 @@ dependencies = [
[[package]]
name = "zenoh-collections"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"ahash",
]
@@ -5191,7 +5184,7 @@ dependencies = [
[[package]]
name = "zenoh-config"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"json5",
"nonempty-collections",
@@ -5216,7 +5209,7 @@ dependencies = [
[[package]]
name = "zenoh-core"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"lazy_static",
"tokio",
@@ -5227,7 +5220,7 @@ dependencies = [
[[package]]
name = "zenoh-crypto"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"aes",
"hmac",
@@ -5237,29 +5230,10 @@ dependencies = [
"zenoh-result",
]
[[package]]
name = "zenoh-ext"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"bincode",
"flume",
"futures",
"leb128",
"serde",
"tokio",
"tracing",
"uhlc",
"zenoh",
"zenoh-macros",
"zenoh-util",
]
[[package]]
name = "zenoh-keyexpr"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"getrandom 0.2.17",
"hashbrown 0.16.1",
@@ -5274,7 +5248,7 @@ dependencies = [
[[package]]
name = "zenoh-link"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"zenoh-config",
"zenoh-link-commons",
@@ -5292,7 +5266,7 @@ dependencies = [
[[package]]
name = "zenoh-link-commons"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"base64",
@@ -5328,7 +5302,7 @@ dependencies = [
[[package]]
name = "zenoh-link-quic"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"rustls-webpki",
@@ -5344,7 +5318,7 @@ dependencies = [
[[package]]
name = "zenoh-link-quic_datagram"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"rustls-webpki",
@@ -5360,7 +5334,7 @@ dependencies = [
[[package]]
name = "zenoh-link-tcp"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"socket2 0.5.10",
@@ -5377,7 +5351,7 @@ dependencies = [
[[package]]
name = "zenoh-link-tls"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"base64",
@@ -5406,7 +5380,7 @@ dependencies = [
[[package]]
name = "zenoh-link-udp"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"libc",
@@ -5428,7 +5402,7 @@ dependencies = [
[[package]]
name = "zenoh-link-unixsock_stream"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"nix 0.29.0",
@@ -5446,7 +5420,7 @@ dependencies = [
[[package]]
name = "zenoh-link-ws"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"futures-util",
@@ -5466,7 +5440,7 @@ dependencies = [
[[package]]
name = "zenoh-macros"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"proc-macro2",
"quote",
@@ -5477,7 +5451,8 @@ dependencies = [
[[package]]
name = "zenoh-plugin-storage-manager"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d9fd55c490af4dc4abb6d51b620ecf6f824a9285a1f9446b59eeec6d66ccf7"
dependencies = [
"async-trait",
"bincode",
@@ -5504,7 +5479,7 @@ dependencies = [
[[package]]
name = "zenoh-plugin-trait"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"git-version",
"libloading",
@@ -5521,7 +5496,7 @@ dependencies = [
[[package]]
name = "zenoh-protocol"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"const_format",
"rand 0.8.6",
@@ -5536,7 +5511,7 @@ dependencies = [
[[package]]
name = "zenoh-result"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"anyhow",
]
@@ -5544,7 +5519,7 @@ dependencies = [
[[package]]
name = "zenoh-runtime"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"lazy_static",
"ron",
@@ -5558,7 +5533,7 @@ dependencies = [
[[package]]
name = "zenoh-sync"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"arc-swap",
"event-listener",
@@ -5572,7 +5547,7 @@ dependencies = [
[[package]]
name = "zenoh-task"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"futures",
"tokio",
@@ -5585,7 +5560,7 @@ dependencies = [
[[package]]
name = "zenoh-transport"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"crossbeam-utils",
@@ -5619,7 +5594,7 @@ dependencies = [
[[package]]
name = "zenoh-util"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"const_format",
@@ -5645,7 +5620,7 @@ dependencies = [
[[package]]
name = "zenoh_backend_traits"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
dependencies = [
"async-trait",
"const_format",
-3
View File
@@ -49,7 +49,6 @@ env_logger = "0.11.10"
# networking
zenoh = "=1.9.0"
zenoh-ext = "=1.9.0"
zenoh-plugin-storage-manager = { version = "=1.9.0", default-features = false }
zenoh-plugin-trait = "=1.9.0"
netwatcher = "0.6.0"
@@ -62,7 +61,6 @@ pidfile-rs = { git = "https://github.com/AndreiCravtov/pidfile-rs" }
[patch.crates-io]
zenoh = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh-ext = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh-buffers = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh-codec = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh-collections = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
@@ -88,7 +86,6 @@ zenoh-sync = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh-task = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh-transport = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh-util = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh-plugin-storage-manager = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
zenoh_backend_traits = { git = "https://github.com/evanev7/zenoh.git", branch = "exo" }
[workspace.lints.rust]
+9 -2
View File
@@ -118,7 +118,7 @@ Then restart the Nix daemon: `sudo launchctl kickstart -k system/org.nixos.nix-d
--force
```
Clone the repo, build the dashboard, and run exo:
Clone the repo, build the dashboard, install the dependencies, and run exo:
```bash
# Clone exo
@@ -127,6 +127,9 @@ git clone https://github.com/exo-explore/exo
# Build dashboard
cd exo/dashboard && npm install && npm run build && cd ..
# Install Python dependencies, including the MLX backend
uv sync --extra mlx
# Run exo
uv run exo
```
@@ -176,7 +179,7 @@ rustup toolchain install nightly
**Note:** The `macmon` package is macOS-only and not required for Linux.
Clone the repo, build the dashboard, and run exo:
Clone the repo, build the dashboard, install the dependencies, and run exo:
```bash
# Clone exo
@@ -185,6 +188,10 @@ git clone https://github.com/exo-explore/exo
# Build dashboard
cd exo/dashboard && npm install && npm run build && cd ..
# Install Python dependencies with the MLX backend for your hardware
# (NVIDIA: --extra mlx-cuda13 or --extra mlx-cuda12)
uv sync --extra mlx-cpu
# Run exo
uv run exo
```
+4 -3
View File
@@ -8,6 +8,7 @@
"name": "exo-dashboard",
"version": "1.0.0",
"dependencies": {
"devalue": "^5.6.2",
"highlight.js": "^11.11.1",
"katex": "^0.16.27",
"marked": "^17.0.1",
@@ -2331,9 +2332,9 @@
}
},
"node_modules/devalue": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.5.0.tgz",
"integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==",
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
+3 -2
View File
@@ -11,8 +11,6 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.48.4",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
@@ -20,6 +18,8 @@
"@types/d3": "^7.4.3",
"@types/node": "^22",
"d3": "^7.9.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
@@ -28,6 +28,7 @@
"vite": "^6.0.0"
},
"dependencies": {
"devalue": "^5.6.2",
"highlight.js": "^11.11.1",
"katex": "^0.16.27",
"marked": "^17.0.1",
+2 -2
View File
@@ -16,10 +16,10 @@ check:
uv run basedpyright --project pyproject.toml
sync:
uv sync --all-packages
uv sync --all-packages --extra mlx
sync-clean:
uv sync --all-packages --force-reinstall --no-cache
uv sync --all-packages --extra mlx --force-reinstall --no-cache
rust-rebuild:
PYO3_PYTHON="$(uv run python -c 'import sys; print(sys.executable)')" cargo run --bin stub_gen
@@ -0,0 +1,36 @@
model_id = "moonshotai/Kimi-K2.7-Code"
n_layers = 61
hidden_size = 7168
num_key_value_heads = 64
supports_tensor = true
tasks = ["TextGeneration"]
family = "kimi"
quantization = ""
base_model = "Kimi K2.7 Code"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
context_length = 262144
backends = ["MlxMetal", "MlxCuda", "MlxCpu"]
[storage_size]
in_bytes = 595204986173
# Vision tower + mm_projector extracted unmodified (bf16) from the official
# repo, in the same format as exolabs/Kimi-K2.6-vision; extraction script
# included in the weights repo. Vision config is identical to Kimi-K2.6's.
[vision]
image_token_id = 163605
model_type = "kimi_vl"
weights_repo = "aidiffuser/Kimi-K2.7-Code-vision"
processor_repo = "moonshotai/Kimi-K2.7-Code"
# Source: https://huggingface.co/moonshotai/Kimi-K2.7-Code
# (recommends temperature 1.0 / top_p 0.95 for thinking mode, same as K2.6)
[sampling_defaults]
temperature = 1.0
top_p = 0.95
min_p = 0.01
[sampling_defaults.non_thinking]
temperature = 0.6
top_p = 0.95
min_p = 0.01
-1
View File
@@ -48,7 +48,6 @@ env_logger.workspace = true
# Networking
zenoh.workspace = true
zenoh-ext = { workspace = true, features = ["unstable"] }
rand.workspace = true
serde_json.workspace = true
parking_lot.workspace = true
-40
View File
@@ -2,21 +2,14 @@
# ruff: noqa: E501, F401, F403, F405
import builtins
import collections.abc
import os
import pathlib
import typing
__all__ = [
"FromSwarm",
"LVAggregator",
"LVPublisher",
"LVSubscriber",
"NetworkingHandle",
"Pidfile",
"PidfileError",
"SessionHandle",
"Storage",
"StorageGetter",
]
class FromSwarm:
@@ -38,18 +31,6 @@ class FromSwarm:
...
@typing.final
class LVAggregator:
def dump(self) -> builtins.dict[builtins.str, builtins.str]: ...
@typing.final
class LVPublisher:
def put(self, data: builtins.str) -> collections.abc.Awaitable[None]: ...
@typing.final
class LVSubscriber:
def recv(self) -> collections.abc.Awaitable[tuple[str, str] | None]: ...
@typing.final
class NetworkingHandle:
@staticmethod
@@ -130,24 +111,3 @@ class PidfileError(builtins.Exception):
def __repr__(self) -> builtins.str: ...
def __str__(self) -> builtins.str: ...
@typing.final
class SessionHandle:
@staticmethod
def new(identity: builtins.str, namespace: builtins.str, listen_port: builtins.int, discovery_service_port: builtins.int) -> tuple[SessionHandle, NetworkingHandle]: ...
def last_value_aggregator(self, prefix: builtins.str) -> LVAggregator: ...
def last_value_subscriber(self, kexpr: builtins.str) -> LVSubscriber: ...
def last_value_publisher(self, kexpr: builtins.str) -> LVPublisher: ...
def storage_interface(self) -> Storage: ...
@typing.final
class Storage:
def get(self, key: builtins.str) -> collections.abc.Awaitable[str | None]: ...
def get_many(self, key: builtins.str) -> StorageGetter: ...
def put(self, key: builtins.str, data: builtins.str) -> collections.abc.Awaitable[None]: ...
def delete(self, key: builtins.str) -> collections.abc.Awaitable[None]: ...
def dump(self, prefix: builtins.str) -> collections.abc.Awaitable[dict[str, str]]: ...
@typing.final
class StorageGetter:
def recv(self) -> collections.abc.Awaitable[tuple[str, str] | None]: ...
-181
View File
@@ -1,181 +0,0 @@
use networking::{Session, liveliness_aggregator::LivelinessAggregator};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use zenoh::{Result as ZResult, Wait};
use parking_lot::Mutex;
use pyo3::{
exceptions::{PyConnectionError, PyRuntimeError},
prelude::*,
};
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use zenoh::{
handlers::FifoChannelHandler,
sample::{Sample, SampleKind},
};
use zenoh_ext::{
AdvancedPublisher, AdvancedSubscriber, AdvancedSubscriberBuilderExt, HistoryConfig,
};
#[gen_stub_pyclass]
#[pyclass]
pub struct LVAggregator {
pub prefix: Arc<str>,
pub store: Arc<Mutex<HashMap<String, String>>>,
pub current_live: LivelinessAggregator,
}
pub fn spawn_lv_aggregator_onto(session: &Session, prefix: Arc<str>) -> ZResult<LVAggregator> {
// nota bene: config must be kept in line with SessionHandle::last_value_receiver
let store = Arc::new(Mutex::new(HashMap::default()));
session
.z
//assuming all LV aggregators are prefix/node_id/atomic_json
.declare_subscriber(format!("{prefix}/*/*"))
.advanced()
.history(
HistoryConfig::default()
.max_samples(1)
.detect_late_publishers(),
)
.callback({
let store = Arc::clone(&store);
let prefix = Arc::clone(&prefix);
move |sample| {
if let Some(s) = sample
.key_expr()
.to_string()
.strip_prefix(&*prefix)
.and_then(|it| it.strip_prefix('/'))
{
let s = s.to_string();
match sample.kind() {
SampleKind::Put => {
store.lock().insert(
s,
sample
.payload()
.try_to_string()
.expect("we only use utf8 encoded strings. someone messed up")
.to_string(),
);
}
SampleKind::Delete => {
store.lock().remove(&s);
}
}
};
}
})
.background()
.wait()?;
Ok(LVAggregator {
prefix,
store,
current_live: session.liveliness_aggregator.clone(),
})
}
#[gen_stub_pymethods]
#[pymethods]
impl LVAggregator {
pub fn dump(&self) -> HashMap<String, String> {
let mut store = self.store.lock();
let currently_alive: HashSet<String> = self.current_live.dump();
// remove any keys that are no longer live
store.retain(|key, _| {
currently_alive.iter().any(|node_id| {
key.strip_prefix(node_id)
.is_some_and(|rest| rest.starts_with("/"))
})
});
store.clone()
}
}
#[gen_stub_pyclass]
#[pyclass]
pub struct LVSubscriber {
pub subscriber: AdvancedSubscriber<FifoChannelHandler<Sample>>,
}
#[gen_stub_pymethods]
#[pymethods]
impl LVSubscriber {
#[gen_stub(override_return_type(
type_repr="collections.abc.Awaitable[tuple[str, str] | None]",
imports=("collections.abc")
))]
pub fn recv<'py>(&'py self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, {
if self.subscriber.receiver_count() != 1 {
return Err(PyRuntimeError::new_err(
"tried to receive twice on the same receiver",
));
}
let subscriber = self.subscriber.clone();
async move {
loop {
match subscriber.recv_async().await {
Ok(sample) if sample.kind() == SampleKind::Delete => continue,
Err(_) => {
return Ok(None);
}
Ok(sample) => {
return Ok(Some((
sample.key_expr().to_string(),
sample
.payload()
.try_to_string()
.expect("we only use utf8 encoded strings. someone messed up")
.to_string(),
)));
}
}
}
}
})
}
}
#[gen_stub_pyclass]
#[pyclass]
pub struct LVPublisher {
pub state: Arc<AdvancedPublisher<'static>>,
}
impl LVPublisher {
pub fn new(publisher: AdvancedPublisher<'static>) -> Self {
Self {
state: Arc::new(publisher),
}
}
}
#[gen_stub_pymethods]
#[pymethods]
impl LVPublisher {
#[gen_stub(override_return_type(
type_repr="collections.abc.Awaitable[None]",
imports=("collections.abc")
))]
pub fn put<'py>(&'py self, py: Python<'py>, data: String) -> PyResult<Bound<'py, PyAny>> {
let state = Arc::clone(&self.state);
pyo3_async_runtimes::tokio::future_into_py(py, {
// clone the data so py can have it back
async move {
state
.put(data)
.await
.map_err(|e| PyConnectionError::new_err(e.to_string()))?;
Ok(())
}
})
}
}
pub fn lv_submodule(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<LVPublisher>()?;
m.add_class::<LVSubscriber>()?;
m.add_class::<LVAggregator>()?;
Ok(())
}
+11 -13
View File
@@ -4,18 +4,13 @@
//!
//!
pub mod allow_threading;
pub mod last_value;
pub mod networking;
pub mod pidfile;
pub mod session;
mod storage;
mod allow_threading;
// mod ident;
mod networking;
mod pidfile;
use crate::last_value::lv_submodule;
use crate::networking::networking_submodule;
use crate::pidfile::pidfile_submodule;
use crate::session::session_submodule;
use crate::storage::storage_submodule;
use pyo3::prelude::PyModule;
use pyo3::{Bound, PyResult, pymodule};
use pyo3_stub_gen::define_stub_info_gatherer;
@@ -159,12 +154,15 @@ fn main_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
builder.enable_all();
pyo3_async_runtimes::tokio::init(builder);
// TODO: for now this is all NOT a submodule. KISS
// TODO: for now this is all NOT a submodule, but figure out how to make the submodule system
// work with maturin, where the types generate correctly, in the right folder, without
// too many importing issues...
pidfile_submodule(m)?;
// m.add_class::<PyKeypair>()?;
networking_submodule(m)?;
lv_submodule(m)?;
session_submodule(m)?;
storage_submodule(m)?;
// top-level constructs
// TODO: ...
Ok(())
}
-111
View File
@@ -1,111 +0,0 @@
use networking::Session;
use pyo3::{
exceptions::{PyConnectionError, PyRuntimeError, PyValueError},
prelude::*,
};
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use zenoh::Wait;
use zenoh_ext::{
AdvancedPublisherBuilderExt, AdvancedSubscriberBuilderExt, CacheConfig, HistoryConfig,
MissDetectionConfig,
};
use crate::{
last_value::{LVAggregator, LVPublisher, LVSubscriber, spawn_lv_aggregator_onto},
networking::PyNetworkingHandle,
storage::Storage,
};
#[gen_stub_pyclass]
#[pyclass]
pub struct SessionHandle {
pub session: Session,
}
#[gen_stub_pymethods]
#[pymethods]
impl SessionHandle {
#[staticmethod]
pub fn new<'py>(
identity: &str,
namespace: &str,
listen_port: u16,
discovery_service_port: u16,
) -> PyResult<(SessionHandle, PyNetworkingHandle)> {
// get identity
if !identity
.chars()
.all(|c| ('0'..='9').contains(&c) || ('a'..='f').contains(&c))
|| identity.len() > 32
{
return Err(PyValueError::new_err(format!(
"{identity} is not a valid zenoh identity"
)));
}
let cfg = networking::cfg(identity, listen_port).map_err(|e| {
PyValueError::new_err(format!("failed to write config: {}", e.to_string()))
})?;
let session = pyo3_async_runtimes::tokio::get_runtime()
.block_on(networking::open(
cfg,
namespace,
listen_port,
discovery_service_port,
))
.map_err(|e| {
PyRuntimeError::new_err(format!(
"failed to spawn networking on tokio runtime: {}",
e.to_string()
))
})?;
let legacy = PyNetworkingHandle::from_session(session.clone());
Ok((Self { session }, legacy))
}
pub fn last_value_aggregator(&self, prefix: String) -> PyResult<LVAggregator> {
spawn_lv_aggregator_onto(&self.session, prefix.into()).map_err(|e| {
PyConnectionError::new_err(format!("failed to spawn liveliness aggregator: {e}"))
})
}
pub fn last_value_subscriber(&self, kexpr: &str) -> PyResult<LVSubscriber> {
// nota bene: config must be kept in track with the LVAggregator
self.session
.z
.declare_subscriber(kexpr)
.advanced()
.history(
HistoryConfig::default()
.max_samples(1)
.detect_late_publishers(),
)
.wait()
.map_err(|e| PyConnectionError::new_err(format!("failed to declare subscriber: {e}")))
.map(|subscriber| LVSubscriber { subscriber })
}
pub fn last_value_publisher(&self, kexpr: String) -> PyResult<LVPublisher> {
self.session
.z
.declare_publisher(kexpr)
.advanced()
.publisher_detection()
.sample_miss_detection(MissDetectionConfig::default())
.cache(CacheConfig::default().max_samples(1))
.wait()
.map_err(|e| PyConnectionError::new_err(format!("failed to declare publisher: {e}")))
.map(LVPublisher::new)
}
pub fn storage_interface(&self) -> Storage {
Storage {
session: self.session.z.clone(),
}
}
}
pub fn session_submodule(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<SessionHandle>()?;
Ok(())
}
-199
View File
@@ -1,199 +0,0 @@
use std::{collections::HashMap, time::Duration};
use networking::STORAGE_PREFIX;
use pyo3::{
exceptions::{PyConnectionError, PyRuntimeError, PyValueError},
prelude::*,
};
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use zenoh::{
Session as ZSession, Wait, handlers::FifoChannelHandler, query::Reply, sample::SampleKind,
};
#[gen_stub_pyclass]
#[pyclass]
pub struct Storage {
pub session: ZSession,
}
#[gen_stub_pymethods]
#[pymethods]
impl Storage {
#[gen_stub(override_return_type(
type_repr="collections.abc.Awaitable[str | None]",
imports=("collections.abc")
))]
pub fn get<'py>(&'py self, py: Python<'py>, key: String) -> PyResult<Bound<'py, PyAny>> {
if key.contains('*') {
return Err(PyValueError::new_err(format!(
"{key} is invalid -- Storage.get only supports fixed keys"
)));
}
let session = self.session.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let subscriber = session
.get(format!("{STORAGE_PREFIX}/{key}"))
//.allowed_destination(Locality::SessionLocal)
.await
.map_err(|e| PyConnectionError::new_err(format!("failed to query storage: {e}")))?;
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(1)) => {
Ok(None)
}
reply = subscriber.recv_async() => {
Ok(reply.ok()
.and_then(|reply| reply.into_result().ok())
.and_then(|sample| {
if sample.kind() == SampleKind::Put {
Some(sample
.payload()
.try_to_string()
.expect("we only use utf8 encoded strings. someone messed up").to_string()
)
} else { None }
})
)
}
}
})
}
pub fn get_many(&self, key: String) -> PyResult<StorageGetter> {
self.session
.get(key)
.wait()
.map_err(|e| PyConnectionError::new_err(format!("failed to query storage: {e}")))
.map(StorageGetter)
}
#[gen_stub(override_return_type(
type_repr="collections.abc.Awaitable[None]",
imports=("collections.abc")
))]
pub fn put<'py>(
&'py self,
py: Python<'py>,
key: String,
data: String,
) -> PyResult<Bound<'py, PyAny>> {
if key.contains('*') {
return Err(PyValueError::new_err(format!(
"{key} is invalid -- Storage.put only supports fixed keys"
)));
}
let session = self.session.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
session
.put(format!("{STORAGE_PREFIX}/{key}"), data)
.await
.map_err(|e| PyConnectionError::new_err(format!("failed to query storage: {e}")))
})
}
#[gen_stub(override_return_type(
type_repr="collections.abc.Awaitable[None]",
imports=("collections.abc")
))]
pub fn delete<'py>(&'py self, py: Python<'py>, key: String) -> PyResult<Bound<'py, PyAny>> {
if key.contains('*') {
return Err(PyValueError::new_err(format!(
"{key} is invalid -- Storage.delete only supports fixed keys"
)));
}
let session = self.session.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
session
.delete(format!("{STORAGE_PREFIX}/{key}"))
.await
.map_err(|e| PyConnectionError::new_err(format!("failed to query storage: {e}")))
})
}
#[gen_stub(override_return_type(
type_repr="collections.abc.Awaitable[dict[str, str]]",
imports=("collections.abc")
))]
pub fn dump<'py>(&'py self, py: Python<'py>, prefix: String) -> PyResult<Bound<'py, PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
Ok(networking::read_raw_memory_storage()
.await
.into_iter()
.filter_map(|(key, value)| {
Some((
key?.as_str().strip_prefix(prefix.as_str())?.to_string(),
value
.payload
.try_to_string()
.expect("we only use utf8 encoded strings. someone messed up")
.to_string(),
))
})
.collect::<HashMap<String, String>>())
})
}
}
#[gen_stub_pyclass]
#[pyclass]
pub struct StorageGetter(FifoChannelHandler<Reply>);
#[gen_stub_pymethods]
#[pymethods]
impl StorageGetter {
#[gen_stub(override_return_type(
type_repr="collections.abc.Awaitable[tuple[str, str] | None]",
imports=("collections.abc")
))]
fn recv<'py>(&'py self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
if self.0.receiver_count() != 1 {
return Err(PyRuntimeError::new_err(
"Tried to call StorageGetter.recv twice concurrently",
));
}
let dupe = self.0.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let sample = loop {
match dupe.recv_async().await {
Err(_) => return Ok(None),
Ok(reply) => match reply.into_result() {
Err(e) => {
log::warn!("Ignoring reply error: {e}");
continue;
}
Ok(sample) => match sample.kind() {
SampleKind::Put => break sample,
SampleKind::Delete => {
log::warn!(
"Received unexpected DELETE from queryable: {}",
sample.key_expr()
);
continue;
}
},
},
};
};
let key = sample
.key_expr()
.to_string()
.strip_prefix(format!("{STORAGE_PREFIX}/").as_str())
.expect("invalid storage format encountered")
.to_string();
Ok(Some((
key,
sample
.payload()
.try_to_string()
.expect("we only use utf8 encoded strings. someone messed up")
.to_string(),
)))
})
}
}
pub fn storage_submodule(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Storage>()?;
m.add_class::<StorageGetter>()?;
Ok(())
}
-168
View File
@@ -1,168 +0,0 @@
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use zenoh_ext::{
AdvancedPublisherBuilderExt, AdvancedSubscriber, AdvancedSubscriberBuilderExt, CacheConfig,
HistoryConfig, MissDetectionConfig,
};
use zenoh::{handlers::FifoChannelHandler, sample::Sample};
// Adjust these imports to your crate/module paths.
use exo_rs::{
last_value::{LVPublisher, LVSubscriber},
session::SessionHandle,
};
async fn expect_two_values(
sub: &AdvancedSubscriber<FifoChannelHandler<Sample>>,
key_a: &str,
val_a: &str,
key_b: &str,
val_b: &str,
) {
use std::collections::HashMap;
use tokio::time::{Duration, Instant, timeout};
use zenoh::sample::SampleKind;
let deadline = Instant::now() + Duration::from_secs(10);
let mut seen: HashMap<String, String> = HashMap::new();
loop {
if seen.get(key_a).map(String::as_str) == Some(val_a)
&& seen.get(key_b).map(String::as_str) == Some(val_b)
{
return;
}
let remaining = deadline.saturating_duration_since(Instant::now());
assert!(
!remaining.is_zero(),
"timed out waiting for both historical samples; expected {key_a}={val_a}, {key_b}={val_b}; seen = {seen:?}"
);
match timeout(remaining.min(Duration::from_millis(750)), sub.recv_async()).await {
Ok(Ok(sample)) => {
if sample.kind() == SampleKind::Delete {
continue;
}
let key = sample.key_expr().to_string();
let value = sample
.payload()
.try_to_string()
.expect("payload should be UTF-8")
.to_string();
if key == key_a || key == key_b {
eprintln!("received relevant {key} = {value}");
seen.insert(key, value);
} else {
eprintln!("received unrelated {key} = {value}");
}
}
Ok(Err(e)) => panic!("subscriber receive failed: {e}"),
Err(_) => {}
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lv_subscriber_receives_last_value_from_multiple_publishers() {
let cfg =
networking::cfg(&format!("{:x}", rand::random::<u128>()), 52414).expect("create config");
let n_session = networking::open(cfg, "exo", 52414, 52413)
.await
.expect("open session");
let session = SessionHandle { session: n_session };
let run_id = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let base = format!("zenoh_advanced_history_test/{run_id}");
let key_a = format!("{base}/a");
let key_b = format!("{base}/b");
let sub_key = format!("{base}/*");
let pub1: LVPublisher = session
.last_value_publisher(key_a.clone())
.expect("declare LV publisher a");
pub1.state.put("aa").await.expect("publish aa");
let pub2: LVPublisher = session
.last_value_publisher(key_b.clone())
.expect("declare LV publisher b");
pub2.state.put("bb").await.expect("publish bb");
// Let publisher detection / cache metadata settle before the late subscriber joins.
tokio::time::sleep(Duration::from_millis(250)).await;
let sub: LVSubscriber = session
.last_value_subscriber(&*sub_key)
.expect("declare LV subscriber");
expect_two_values(&sub.subscriber, &*key_a, "aa", &*key_b, "bb").await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn wildcard_advanced_subscriber_receives_history_from_both_publishers() {
let cfg =
networking::cfg(&format!("{:x}", rand::random::<u128>()), 52412).expect("create config");
let n_session = networking::open(cfg, "exo", 52412, 52411)
.await
.expect("open session");
let session = n_session.z.clone();
// Unique prefix so the wildcard subscriber cannot accidentally see unrelated traffic.
let run_id = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let base = format!("zenoh_advanced_history_test/{run_id}");
let key_a = format!("{base}/a");
let key_b = format!("{base}/b");
let sub_key = format!("{base}/*");
let pub1 = session
.declare_publisher(key_a.clone())
.advanced()
.publisher_detection()
.sample_miss_detection(MissDetectionConfig::default())
.cache(CacheConfig::default().max_samples(1))
.await
.expect("declare advanced publisher a");
pub1.put("aa").await.expect("publish aa");
let pub2 = session
.declare_publisher(key_b.clone())
.advanced()
.sample_miss_detection(MissDetectionConfig::default())
.publisher_detection()
.cache(CacheConfig::default().max_samples(1))
.await
.expect("declare advanced publisher b");
pub2.put("bb").await.expect("publish bb");
// Give liveliness/cache declarations a brief chance to settle before declaring
// the late-joining advanced subscriber.
tokio::time::sleep(Duration::from_millis(250)).await;
let sub = session
.declare_subscriber(sub_key)
.advanced()
.history(
HistoryConfig::default()
.max_samples(1)
.detect_late_publishers(),
)
.await
.expect("declare advanced subscriber");
expect_two_values(&sub, &*key_a, "aa", &*key_b, "bb").await
}
+38 -35
View File
@@ -1,51 +1,54 @@
use core::mem::drop;
use core::option::Option::Some;
use core::time::Duration;
use tokio;
use tokio::sync::mpsc;
#[cfg(test)]
mod tests {
use core::mem::drop;
use core::option::Option::Some;
use core::time::Duration;
use tokio;
use tokio::sync::mpsc;
#[tokio::test]
async fn test_drop_channel() {
struct Ping;
#[tokio::test]
async fn test_drop_channel() {
struct Ping;
let (tx, mut rx) = mpsc::channel::<Ping>(10);
let (tx, mut rx) = mpsc::channel::<Ping>(10);
let _ = tokio::spawn(async move {
println!("TASK: entered");
let _ = tokio::spawn(async move {
println!("TASK: entered");
loop {
tokio::select! {
result = rx.recv() => {
match result {
Some(_) => {
println!("TASK: pinged");
}
None => {
println!("TASK: closing channel");
break;
loop {
tokio::select! {
result = rx.recv() => {
match result {
Some(_) => {
println!("TASK: pinged");
}
None => {
println!("TASK: closing channel");
break;
}
}
}
}
_ = tokio::time::sleep(Duration::from_secs_f32(0.1)) => {
println!("TASK: heartbeat");
_ = tokio::time::sleep(Duration::from_secs_f32(0.1)) => {
println!("TASK: heartbeat");
}
}
}
}
println!("TASK: exited");
});
println!("TASK: exited");
});
let tx2 = tx.clone();
let tx2 = tx.clone();
tokio::time::sleep(Duration::from_secs_f32(0.11)).await;
tokio::time::sleep(Duration::from_secs_f32(0.11)).await;
tx.send(Ping).await.expect("Should not fail");
drop(tx);
tx.send(Ping).await.expect("Should not fail");
drop(tx);
tokio::time::sleep(Duration::from_secs_f32(0.11)).await;
tokio::time::sleep(Duration::from_secs_f32(0.11)).await;
tx2.send(Ping).await.expect("Should not fail");
drop(tx2);
tx2.send(Ping).await.expect("Should not fail");
drop(tx2);
tokio::time::sleep(Duration::from_secs_f32(0.11)).await;
tokio::time::sleep(Duration::from_secs_f32(0.11)).await;
}
}
-69
View File
@@ -1,69 +0,0 @@
import os
import uuid
import pytest
from exo_rs import SessionHandle
ZENOH_PORT = 52414
DISCOVERY_PORT = 52413
@pytest.fixture(scope="module")
def storage():
node_id = os.urandom(16).hex().rstrip("0")
session_handle, _nh = SessionHandle.new(
node_id,
ZENOH_PORT,
DISCOVERY_PORT,
)
return session_handle.storage_interface()
@pytest.mark.asyncio
async def test_storage_get_missing_key_returns_none(storage):
key = f"tests/storage/{uuid.uuid4().hex}/missing"
value = await storage.get(key)
assert value is None
@pytest.mark.asyncio
async def test_storage_put_then_get_returns_value(storage):
key = f"tests/storage/{uuid.uuid4().hex}/value"
expected = "hello storage"
await storage.put(key, expected)
assert await storage.get(key) == expected
@pytest.mark.asyncio
async def test_storage_put_overwrites_value(storage):
key = f"tests/storage/{uuid.uuid4().hex}/overwrite"
await storage.put(key, "old")
await storage.put(key, "new")
assert await storage.get(key) == "new"
@pytest.mark.asyncio
async def test_storage_put_overwrites_value(storage):
key = f"tests/storage/{uuid.uuid4().hex}/overwrite"
await storage.put(key, "old")
await storage.delete(key)
assert await storage.get(key) == None
@pytest.mark.asyncio
async def test_storage_get_rejects_wildcard_key(storage):
with pytest.raises(ValueError, match="only supports fixed keys"):
await storage.get("tests/storage/*")
@pytest.mark.asyncio
async def test_storage_put_rejects_wildcard_key(storage):
with pytest.raises(ValueError, match="only supports fixed keys"):
await storage.put("tests/storage/*", "value")
+14 -29
View File
@@ -7,16 +7,9 @@ use zenoh_plugin_trait::PluginsManager;
pub use zenoh::{Config, config::ZenohId};
use crate::{
discovery::Discovery,
liveliness_aggregator::{LivelinessAggregator, spawn_liveliness_aggregator},
};
pub use zenoh_plugin_storage_manager::read_raw_memory_storage;
pub const STORAGE_PREFIX: &str = "storage/mem1";
use crate::discovery::Discovery;
pub mod discovery;
pub mod liveliness_aggregator;
pub mod swarm;
pub fn is_valid_zid(identity: &str) -> bool {
@@ -40,22 +33,20 @@ pub fn cfg(identity: &str, listen_port: u16) -> Result<zenoh::Config> {
cfg.insert_json5("scouting/multicast/autoconnect", "[]")?;
cfg.insert_json5("scouting/gossip/multihop", "true")?;
cfg.insert_json5("adminspace/enabled", "true")?;
cfg.insert_json5("transport/link/tx/batch_size", "9216")?;
//cfg.insert_json5("transport/link/tx/batch_size", "9216")?;
cfg.insert_json5("transport/link/rx/buffer_size", "16777216")?;
cfg.insert_json5("timestamping/enabled", "true")?;
//cfg.insert_json5("timestamping/enabled", "true")?;
cfg.insert_json5("plugins/storage_manager/__required__", "true")?;
cfg.insert_json5(
"plugins/storage_manager/storages/mem1/key_expr",
&format!("\"{STORAGE_PREFIX}/**\""),
)?;
cfg.insert_json5(
"plugins/storage_manager/storages/mem1/strip_prefix",
&format!("\"{STORAGE_PREFIX}\""),
)?;
cfg.insert_json5("plugins/storage_manager/storages/mem1/volume", "\"memory\"")?;
cfg.insert_json5(
"plugins/storage_manager/storages/mem1/replication/interval",
"2",
"plugins/storage_manager/storages/mem1",
r#"{
key_expr: "storage/mem1/**",
strip_prefix: "storage/mem1",
volume: "memory",
replication: {
interval: 2,
}
}"#,
)?;
Ok(cfg)
}
@@ -108,15 +99,10 @@ pub async fn open(
.await;
}
})));
let liveliness_aggregator = spawn_liveliness_aggregator(&z)?;
Ok(Session {
z,
liveliness_aggregator,
_jh,
})
Ok(Session { z, _jh })
}
pub struct AbortOnDrop(pub JoinHandle<()>);
struct AbortOnDrop(JoinHandle<()>);
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
@@ -126,6 +112,5 @@ impl Drop for AbortOnDrop {
#[derive(Clone)]
pub struct Session {
pub z: ZSession,
pub liveliness_aggregator: LivelinessAggregator,
_jh: Arc<AbortOnDrop>,
}
@@ -1,44 +0,0 @@
use std::{collections::HashSet, sync::Arc};
use parking_lot::Mutex;
use zenoh::{Result, Session, Wait, sample::SampleKind};
pub fn spawn_liveliness_aggregator(session: &Session) -> Result<LivelinessAggregator> {
let store = Arc::new(Mutex::new(HashSet::default()));
session
.liveliness()
.declare_subscriber("live/*")
.history(true)
.callback({
let store = Arc::clone(&store);
move |sample| {
let Some(nid) = sample
.key_expr()
.to_string()
.strip_prefix("live/")
.map(str::to_owned)
else {
return;
};
let mut mg = store.lock();
match sample.kind() {
SampleKind::Put => mg.insert(nid),
SampleKind::Delete => mg.remove(&nid),
};
}
})
.background()
.wait()?;
Ok(LivelinessAggregator { store })
}
#[derive(Clone)]
pub struct LivelinessAggregator {
// need two arcs as the sub owns an arc to the store.
store: Arc<Mutex<HashSet<String>>>,
}
impl LivelinessAggregator {
pub fn dump(&self) -> HashSet<String> {
self.store.lock().clone()
}
}
-99
View File
@@ -1,99 +0,0 @@
use std::time::Duration;
use zenoh::Wait;
fn unique_key(name: &str) -> String {
format!("test/zenoh-runtime-polling/{}/{}", std::process::id(), name)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_and_recv_work_on_tokio_baseline() {
let session = zenoh::open(zenoh::Config::default())
.await
.expect("open session");
let key = unique_key("tokio-baseline");
let reply_key = key.clone();
let _queryable = session
.declare_queryable(key.clone())
.callback(move |query| {
query
.reply(reply_key.clone(), "hello-from-queryable")
.wait()
.expect("reply from queryable");
})
.await
.expect("declare queryable");
let replies = session.get(key).await.expect("get");
let reply = tokio::time::timeout(Duration::from_secs(5), replies.recv_async())
.await
.expect("timed out waiting for reply")
.expect("reply channel closed");
let sample = reply.result().expect("reply result was error");
let payload = sample
.payload()
.try_to_string()
.expect("payload should be utf8");
assert_eq!(payload.as_ref(), "hello-from-queryable");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_and_recv_work_when_polled_by_smol_without_tokio_context() {
let session = zenoh::open(zenoh::Config::default())
.await
.expect("open session under tokio");
let key = unique_key("smol-no-tokio-context");
let reply_key = key.clone();
let _queryable = session
.declare_queryable(key.clone())
.callback(move |query| {
query
.reply(reply_key.clone(), "hello-from-queryable")
.wait()
.expect("reply from queryable");
})
.await
.expect("declare queryable under tokio");
let session_for_smol = session.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
// This thread was not entered by Tokio.
// If Zenoh's get/recv path requires an ambient Tokio Handle in the polling thread,
// this is where it should panic, hang, or error.
let result = {
smol::block_on(async move {
let replies = session_for_smol.get(key).await.expect("get under smol");
let reply = replies.recv_async().await.expect("reply channel closed");
let sample = reply.result().expect("reply result was error");
let payload = sample
.payload()
.try_to_string()
.expect("payload should be utf8");
payload.to_string()
})
};
tx.send(result).expect("send test result");
});
let result = rx
.recv_timeout(Duration::from_secs(5))
.expect("smol thread timed out; likely hung waiting for get/reply");
let payload = result;
assert_eq!(payload, "hello-from-queryable");
}
+53 -60
View File
@@ -13,7 +13,6 @@ from uuid import uuid4
import anyio
from anyio import BrokenResourceError, ClosedResourceError
from exo_rs import SessionHandle
from fastapi import FastAPI, File, Form, HTTPException, Query, Request, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
@@ -23,7 +22,6 @@ from hypercorn.config import Config
from hypercorn.typing import ASGIFramework
from hypercorn.utils import LifespanTimeoutError, ShutdownError
from loguru import logger
from pydantic import ValidationError
from exo.api.adapters.chat_completions import (
chat_request_to_text_generation,
@@ -155,11 +153,14 @@ from exo.shared.types.chunks import (
ToolCallChunk,
)
from exo.shared.types.commands import (
AddCustomModelCard,
CancelDownload,
Command,
CreateInstance,
DeleteCustomModelCard,
DeleteDownload,
DeleteInstance,
DeleteInstanceLink,
DownloadCommand,
ForwarderCommand,
ForwarderDownloadCommand,
@@ -167,6 +168,7 @@ from exo.shared.types.commands import (
ImageGeneration,
PlaceInstance,
SendInputChunk,
SetInstanceLink,
StartDownload,
TaskCancelled,
TaskFinished,
@@ -244,7 +246,6 @@ class API:
download_command_sender: Sender[ForwarderDownloadCommand],
# This lets us pause the API if an election is running
election_receiver: Receiver[ElectionMessage],
session_handle: SessionHandle,
) -> None:
self.state = State()
self._event_log = DiskEventLog(_API_EVENT_LOG_DIR)
@@ -257,8 +258,6 @@ class API:
self.last_completed_election: int = 0
self.port = port
self._sent_image_hashes: set[str] = set()
self.aggregator = session_handle.last_value_aggregator("metrics")
self.storage = session_handle.storage_interface()
self.paused: bool = False
self.paused_ev: anyio.Event = anyio.Event()
@@ -410,12 +409,10 @@ class API:
self.app.post("/onboarding")(self.complete_onboarding)
def get_state(self, path: str = ""):
state = self.state.with_aggregator(self.aggregator)
if path == "":
return state
return self.state
try:
x: Any = state.model_dump(by_alias=True)
x = self.state.model_dump(by_alias=True)
for attr in path.split("/"):
if attr != "":
if isinstance(x, dict):
@@ -479,7 +476,6 @@ class API:
model_card = await ModelCard.load(model_id)
try:
state = self.state.with_aggregator(self.aggregator)
placements = get_instance_placements(
PlaceInstance(
model_card=model_card,
@@ -487,13 +483,13 @@ class API:
instance_meta=instance_meta,
min_nodes=min_nodes,
),
node_memory=state.node_memory,
node_network=state.node_network,
node_backends=state.node_backends,
topology=state.topology,
current_instances=state.instances,
download_status=state.downloads,
node_rdma_ctl=state.node_rdma_ctl,
node_memory=self.state.node_memory,
node_network=self.state.node_network,
node_backends=self.state.node_backends,
topology=self.state.topology,
current_instances=self.state.instances,
download_status=self.state.downloads,
node_rdma_ctl=self.state.node_rdma_ctl,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@@ -518,9 +514,8 @@ class API:
seen: set[tuple[ModelId, Sharding, InstanceMeta, int]] = set()
previews: list[PlacementPreview] = []
required_nodes = set(node_ids) if node_ids else None
state = self.state.with_aggregator(self.aggregator)
if len(list(state.topology.list_nodes())) == 0:
if len(list(self.state.topology.list_nodes())) == 0:
return PlacementPreviewResponse(previews=[])
try:
@@ -535,7 +530,9 @@ class API:
instance_combinations.extend(
[
(sharding, instance_meta, i)
for i in range(1, len(list(state.topology.list_nodes())) + 1)
for i in range(
1, len(list(self.state.topology.list_nodes())) + 1
)
]
)
# TODO: PDD
@@ -550,14 +547,14 @@ class API:
instance_meta=instance_meta,
min_nodes=min_nodes,
),
node_memory=state.node_memory,
node_network=state.node_network,
node_backends=state.node_backends,
topology=state.topology,
current_instances=state.instances,
node_memory=self.state.node_memory,
node_network=self.state.node_network,
node_backends=self.state.node_backends,
topology=self.state.topology,
current_instances=self.state.instances,
required_nodes=required_nodes,
download_status=state.downloads,
node_rdma_ctl=state.node_rdma_ctl,
download_status=self.state.downloads,
node_rdma_ctl=self.state.node_rdma_ctl,
)
except ValueError as exc:
if (model_card.model_id, sharding, instance_meta, 0) not in seen:
@@ -699,16 +696,9 @@ class API:
return {"disaggregation": ENABLE_DISAGGREGATION}
async def list_instance_links(self) -> list[InstanceLink]:
links: list[InstanceLink] = []
if not ENABLE_DISAGGREGATION:
return links
for _, value in (await self.storage.dump("custom_model_cards/")).items():
try:
link = InstanceLink.model_validate_json(value)
except ValidationError:
continue
links.append(link)
return links
return []
return list(self.state.instance_links.values())
async def create_instance_link(
self, body: InstanceLinkBody
@@ -725,22 +715,25 @@ class API:
async def _set_instance_link(
self, link_id: InstanceLinkId, body: InstanceLinkBody
) -> InstanceLinkResponse:
await self.storage.put(
f"instance_links/{link_id}",
InstanceLink(
link_id=link_id,
prefill_instances=body.prefill_instances,
decode_instances=body.decode_instances,
).model_dump_json(),
command = SetInstanceLink(
link_id=link_id,
prefill_instances=list(body.prefill_instances),
decode_instances=list(body.decode_instances),
)
await self._send(command)
return InstanceLinkResponse(
message="Command received.", command_id=command.command_id
)
return InstanceLinkResponse(message="Command received.")
async def delete_instance_link(
self, link_id: InstanceLinkId
) -> InstanceLinkResponse:
_require_disaggregation_enabled()
await self.storage.delete(f"instance_links/{link_id}")
return InstanceLinkResponse(message="Command received.")
command = DeleteInstanceLink(link_id=link_id)
await self._send(command)
return InstanceLinkResponse(
message="Command received.", command_id=command.command_id
)
async def cancel_command(self, command_id: CommandId) -> CancelCommandResponse:
"""Cancel an active command by closing its stream and notifying workers."""
@@ -798,11 +791,7 @@ class API:
async def _collect_text_generation_with_stats(
self, command_id: CommandId
) -> BenchChatCompletionResponse:
sampler = PowerSampler(
get_node_system=lambda: self.state.with_aggregator(
self.aggregator
).node_system
)
sampler = PowerSampler(get_node_system=lambda: self.state.node_system)
text_parts: list[str] = []
tool_calls: list[ToolCall] = []
model: ModelId | None = None
@@ -1325,11 +1314,7 @@ class API:
num_images: int,
response_format: str,
) -> BenchImageGenerationResponse:
sampler = PowerSampler(
get_node_system=lambda: self.state.with_aggregator(
self.aggregator
).node_system
)
sampler = PowerSampler(get_node_system=lambda: self.state.node_system)
images: list[ImageData] = []
stats: ImageGenerationStats | None = None
async with anyio.create_task_group() as tg:
@@ -1793,7 +1778,7 @@ class API:
"""Calculate total available memory across all nodes in bytes."""
total_available = Memory()
for memory in self.state.with_aggregator(self.aggregator).node_memory.values():
for memory in self.state.node_memory.values():
total_available += memory.ram_available
return total_available
@@ -1842,8 +1827,11 @@ class API:
status_code=400, detail=f"Failed to fetch model: {exc}"
) from exc
await self.storage.put(
f"custom_model_cards/{card.model_id.normalize()}", card.model_dump_json()
await self.command_sender.send(
ForwarderCommand(
origin=self._system_id,
command=AddCustomModelCard(model_card=card),
)
)
# Immediately update the local cache so the subsequent GET /models
@@ -1868,7 +1856,12 @@ class API:
if card is None or not card.is_custom:
raise HTTPException(status_code=404, detail="Custom model card not found")
await self.storage.delete(f"custom_model_cards/{card.model_id.normalize()}")
await self.command_sender.send(
ForwarderCommand(
origin=self._system_id,
command=DeleteCustomModelCard(model_id=model_id),
)
)
return JSONResponse(
{"message": "Model card deleted", "model_id": str(model_id)}
+1
View File
@@ -329,6 +329,7 @@ class InstanceLinkBody(BaseModel):
class InstanceLinkResponse(BaseModel):
message: str
command_id: CommandId
ImageSize = Literal[
+11 -21
View File
@@ -10,7 +10,7 @@ from typing import Self
import anyio
from anyio.lowlevel import checkpoint as anyio_checkpoint
from daemon import DaemonContext # pyright: ignore[reportMissingTypeStubs]
from exo_rs import Pidfile, PidfileError, SessionHandle
from exo_rs import Pidfile, PidfileError
from loguru import logger
from pydantic import PositiveInt
@@ -47,21 +47,18 @@ class Node:
node_id: NodeId
offline: bool
_api_port: int
_sh: SessionHandle
_tg: TaskGroup = field(init=False, default_factory=TaskGroup)
@classmethod
async def create(cls, args: "Args") -> Self:
node_id = get_node_zid()
session_id = SessionId(master_node_id=node_id, election_clock=0)
session_handle, _nh = SessionHandle.new(
router = Router.create(
node_id,
namespace=args.namespace,
listen_port=args.zenoh_port,
discovery_service_port=args.discovery_port,
)
router = Router(_nh)
await router.register_topic(topics.GLOBAL_EVENTS)
await router.register_topic(topics.LOCAL_EVENTS)
await router.register_topic(topics.COMMANDS)
@@ -100,7 +97,6 @@ class Node:
command_sender=router.sender(topics.COMMANDS),
download_command_sender=router.sender(topics.DOWNLOAD_COMMANDS),
election_receiver=router.receiver(topics.ELECTION_MESSAGES),
session_handle=session_handle,
)
else:
api = None
@@ -112,7 +108,6 @@ class Node:
event_sender=event_router.sender(),
command_sender=router.sender(topics.COMMANDS),
download_command_sender=router.sender(topics.DOWNLOAD_COMMANDS),
session_handle=session_handle,
api_port=args.api_port,
)
else:
@@ -127,8 +122,6 @@ class Node:
local_event_receiver=router.receiver(topics.LOCAL_EVENTS),
command_receiver=router.receiver(topics.COMMANDS),
download_command_sender=router.sender(topics.DOWNLOAD_COMMANDS),
aggregator=session_handle.last_value_aggregator("metrics"),
storage=session_handle.storage_interface(),
)
er_send, er_recv = channel[ElectionResult]()
@@ -157,7 +150,6 @@ class Node:
node_id,
args.offline,
args.api_port,
session_handle,
)
async def run(self):
@@ -233,8 +225,6 @@ class Node:
download_command_sender=self.router.sender(
topics.DOWNLOAD_COMMANDS
),
aggregator=self._sh.last_value_aggregator("metrics"),
storage=self._sh.storage_interface(),
)
self._tg.start_soon(self.master.run)
elif (
@@ -274,7 +264,6 @@ class Node:
download_command_sender=self.router.sender(
topics.DOWNLOAD_COMMANDS
),
session_handle=self._sh,
api_port=self._api_port,
)
self._tg.start_soon(self.worker.run)
@@ -389,16 +378,17 @@ def main_inner(args: "Args"):
class Args(FrozenModel):
verbosity: int
force_master: bool
spawn_api: bool
api_port: PositiveInt
verbosity: int = 0
force_master: bool = False
spawn_api: bool = False
api_port: PositiveInt = 52415
tb_only: bool = False
no_worker: bool = False
no_downloads: bool = False
offline: bool
no_batch: bool
offline: bool = os.getenv("EXO_OFFLINE", "false").lower() == "true"
no_batch: bool = False
fast_synch: bool | None = None # None = auto, True = force on, False = force off
legacy_daemon: bool
legacy_daemon: bool = False
bootstrap_peers: list[str] = []
namespace: str
zenoh_port: int
@@ -486,7 +476,7 @@ class Args(FrozenModel):
type=int,
default=52414,
dest="zenoh_port",
help="Fixed port for zenoh to listen on.",
help="Fixed TCP port for zenoh to listen.",
)
parser.add_argument(
"--discovery-port",
+45 -40
View File
@@ -1,9 +1,7 @@
from datetime import datetime, timedelta, timezone
import anyio
from exo_rs import LVAggregator, Storage
from loguru import logger
from pydantic import ValidationError
from exo.master.placement import (
add_instance_to_placements,
@@ -20,8 +18,11 @@ from exo.routing.event_router import (
from exo.shared.apply import apply
from exo.shared.constants import EXO_EVENT_LOG_DIR, EXO_TRACING_ENABLED
from exo.shared.types.commands import (
AddCustomModelCard,
CreateInstance,
DeleteCustomModelCard,
DeleteInstance,
DeleteInstanceLink,
ForwarderCommand,
ForwarderDownloadCommand,
ImageEdits,
@@ -29,6 +30,7 @@ from exo.shared.types.commands import (
PlaceInstance,
RequestEventLog,
SendInputChunk,
SetInstanceLink,
TaskCancelled,
TaskFinished,
TestCommand,
@@ -36,11 +38,15 @@ from exo.shared.types.commands import (
)
from exo.shared.types.common import CommandId, NodeId, SessionId, SystemId
from exo.shared.types.events import (
CustomModelCardAdded,
CustomModelCardDeleted,
Event,
GlobalForwarderEvent,
IndexedEvent,
InputChunkReceived,
InstanceDeleted,
InstanceLinkCreated,
InstanceLinkDeleted,
LocalForwarderEvent,
NodeGatheredInfo,
NodeTimedOut,
@@ -73,9 +79,7 @@ from exo.utils.event_buffer import MultiSourceBuffer
from exo.utils.task_group import TaskGroup
def _prefill_endpoint_for(
state: State, instance_links: list[InstanceLink], decode_instance_id: InstanceId
) -> str | None:
def _prefill_endpoint_for(state: State, decode_instance_id: InstanceId) -> str | None:
decode = state.instances.get(decode_instance_id)
if decode is None:
return None
@@ -84,7 +88,7 @@ def _prefill_endpoint_for(
return None
sources: set[InstanceId] = set()
for link in instance_links:
for link in state.instance_links.values():
if decode_instance_id in link.decode_instances:
sources.update(link.prefill_instances)
sources.discard(decode_instance_id)
@@ -126,8 +130,6 @@ class Master:
local_event_receiver: Receiver[LocalForwarderEvent],
global_event_sender: Sender[GlobalForwarderEvent],
download_command_sender: Sender[ForwarderDownloadCommand],
aggregator: LVAggregator,
storage: Storage,
):
self.node_id = node_id
self.session_id = session_id
@@ -144,8 +146,6 @@ class Master:
self._event_log = DiskEventLog(EXO_EVENT_LOG_DIR / "master")
self._pending_traces: dict[TaskId, dict[int, list[TraceEventData]]] = {}
self._expected_ranks: dict[TaskId, set[int]] = {}
self.aggregator: LVAggregator = aggregator
self.storage: Storage = storage
async def run(self):
logger.info("Starting Master")
@@ -182,21 +182,10 @@ class Master:
pass
case TextGeneration():
# set-difference => prefill-only nodes
instance_links: list[InstanceLink] = []
prefill_only: set[InstanceId] = set()
for _, link in (
await self.storage.dump("instance_links/")
).items():
try:
instance_links.append(
InstanceLink.model_validate_json(link)
)
except ValidationError:
continue
for link in instance_links:
for link in self.state.instance_links.values():
prefill_only.update(link.prefill_instances)
for link in instance_links:
for link in self.state.instance_links.values():
prefill_only.difference_update(link.decode_instances)
for instance in self.state.instances.values():
@@ -236,9 +225,7 @@ class Master:
params = command.task_params.model_copy(
update={
"prefill_endpoint": _prefill_endpoint_for(
self.state.with_aggregator(self.aggregator),
instance_links,
decode_instance_id,
self.state, decode_instance_id
),
}
)
@@ -382,16 +369,15 @@ class Master:
)
generated_events.extend(transition_events)
case PlaceInstance():
state = self.state.with_aggregator(self.aggregator)
placement = place_instance(
command,
state.topology,
state.instances,
state.node_memory,
state.node_network,
state.node_backends,
download_status=state.downloads,
node_rdma_ctl=state.node_rdma_ctl,
self.state.topology,
self.state.instances,
self.state.node_memory,
self.state.node_network,
self.state.node_backends,
download_status=self.state.downloads,
node_rdma_ctl=self.state.node_rdma_ctl,
)
transition_events = get_transition_events(
self.state.instances, placement, self.state.tasks
@@ -442,6 +428,29 @@ class Master:
f"Finished command {command.finished_command_id} finished"
)
case AddCustomModelCard():
generated_events.append(
CustomModelCardAdded(model_card=command.model_card)
)
case DeleteCustomModelCard():
generated_events.append(
CustomModelCardDeleted(model_id=command.model_id)
)
case SetInstanceLink():
link = InstanceLink(
link_id=command.link_id,
prefill_instances=list(
dict.fromkeys(command.prefill_instances)
),
decode_instances=list(
dict.fromkeys(command.decode_instances)
),
)
generated_events.append(InstanceLinkCreated(link=link))
case DeleteInstanceLink():
generated_events.append(
InstanceLinkDeleted(link_id=command.link_id)
)
case RequestEventLog():
# We should just be able to send everything, since other buffers will ignore old messages
# rate limit to 1000 at a time
@@ -462,9 +471,7 @@ class Master:
async def _plan(self) -> None:
while True:
# kill broken instances
connected_node_ids = set(
self.state.with_aggregator(self.aggregator).topology.list_nodes()
)
connected_node_ids = set(self.state.topology.list_nodes())
for instance_id, instance in self.state.instances.items():
for node_id in instance.shard_assignments.node_to_runner:
if node_id not in connected_node_ids:
@@ -474,9 +481,7 @@ class Master:
break
# time out dead nodes
for node_id, time in self.state.with_aggregator(
self.aggregator
).last_seen.items():
for node_id, time in self.state.last_seen.items():
now = datetime.now(tz=timezone.utc)
if now - time > timedelta(seconds=30):
logger.info(f"Manually removing node {node_id} due to inactivity")
-21
View File
@@ -47,25 +47,6 @@ from exo.utils.channels import channel
from exo.utils.info_gatherer.info_gatherer import NodeBackends
class MockAggregator:
def dump(self) -> dict[str, str]:
return {}
class MockStorage:
async def get(self, _: str) -> None:
return None
async def put(self, _1: str, _2: str) -> None:
return None
async def delete(self, _: str) -> None:
return None
async def dump(self, _: str) -> dict[str, str]:
return {}
@pytest.mark.asyncio
async def test_master():
node_id = get_node_zid()
@@ -113,8 +94,6 @@ async def test_master():
local_event_receiver=le_receiver,
command_receiver=co_receiver,
download_command_sender=fcds,
aggregator=MockAggregator(), # pyright: ignore[reportArgumentType]
storage=MockStorage(), # pyright: ignore[reportArgumentType]
)
logger.info("run the master")
async with anyio.create_task_group() as tg:
+124 -46
View File
@@ -4,14 +4,19 @@ from datetime import datetime
from loguru import logger
from exo.shared.types.common import NodeId
from exo.shared.models.model_cards import ModelCard
from exo.shared.types.common import ModelId, NodeId
from exo.shared.types.events import (
ChunkGenerated,
CustomModelCardAdded,
CustomModelCardDeleted,
Event,
IndexedEvent,
InputChunkReceived,
InstanceCreated,
InstanceDeleted,
InstanceLinkCreated,
InstanceLinkDeleted,
NodeDownloadProgress,
NodeGatheredInfo,
NodeTimedOut,
@@ -27,6 +32,7 @@ from exo.shared.types.events import (
TracesCollected,
TracesMerged,
)
from exo.shared.types.instance_link import InstanceLink, InstanceLinkId
from exo.shared.types.profiling import (
NodeIdentity,
NodeNetworkInfo,
@@ -36,6 +42,7 @@ from exo.shared.types.profiling import (
)
from exo.shared.types.state import State
from exo.shared.types.tasks import Task, TaskId, TaskStatus
from exo.shared.types.topology import Connection, RDMAConnection
from exo.shared.types.worker.downloads import DownloadProgress
from exo.shared.types.worker.instances import Instance, InstanceId
from exo.shared.types.worker.runners import (
@@ -60,6 +67,18 @@ from exo.utils.info_gatherer.info_gatherer import (
)
def _is_rdma_ctl_enabled(
node_id: NodeId, node_rdma_ctl: Mapping[NodeId, NodeRdmaCtlStatus]
) -> bool:
"""A node is RDMA-capable only if rdma_ctl status has been observed as enabled.
Missing entries default to ``False`` — if we have not yet observed (or the node
cannot run) ``rdma_ctl``, it must not participate in an RDMA-backed instance.
"""
status = node_rdma_ctl.get(node_id)
return status is not None and status.enabled
def event_apply(event: Event, state: State) -> State:
"""Apply an event to state."""
match event:
@@ -72,6 +91,10 @@ def event_apply(event: Event, state: State) -> State:
| TracesMerged()
): # Pass-through events that don't modify state
return state
case CustomModelCardAdded():
return apply_custom_model_card_added(event, state)
case CustomModelCardDeleted():
return apply_custom_model_card_deleted(event, state)
case InstanceCreated():
return apply_instance_created(event, state)
case InstanceDeleted():
@@ -96,6 +119,10 @@ def event_apply(event: Event, state: State) -> State:
return apply_topology_edge_created(event, state)
case TopologyEdgeDeleted():
return apply_topology_edge_deleted(event, state)
case InstanceLinkCreated():
return apply_instance_link_created(event, state)
case InstanceLinkDeleted():
return apply_instance_link_deleted(event, state)
def apply(state: State, event: IndexedEvent) -> State:
@@ -195,7 +222,38 @@ def apply_instance_deleted(event: InstanceDeleted, state: State) -> State:
new_instances: Mapping[InstanceId, Instance] = {
iid: inst for iid, inst in state.instances.items() if iid != event.instance_id
}
return state.model_copy(update={"instances": new_instances})
new_links: dict[InstanceLinkId, InstanceLink] = {}
for link_id, link in state.instance_links.items():
prefill = [i for i in link.prefill_instances if i != event.instance_id]
decode = [i for i in link.decode_instances if i != event.instance_id]
if not prefill or not decode:
continue
if prefill == list(link.prefill_instances) and decode == list(
link.decode_instances
):
new_links[link_id] = link
else:
new_links[link_id] = link.model_copy(
update={"prefill_instances": prefill, "decode_instances": decode}
)
return state.model_copy(
update={"instances": new_instances, "instance_links": new_links}
)
def apply_instance_link_created(event: InstanceLinkCreated, state: State) -> State:
new_links: Mapping[InstanceLinkId, InstanceLink] = {
**state.instance_links,
event.link.link_id: event.link,
}
return state.model_copy(update={"instance_links": new_links})
def apply_instance_link_deleted(event: InstanceLinkDeleted, state: State) -> State:
new_links: Mapping[InstanceLinkId, InstanceLink] = {
lid: link for lid, link in state.instance_links.items() if lid != event.link_id
}
return state.model_copy(update={"instance_links": new_links})
def apply_runner_status_updated(event: RunnerStatusUpdated, state: State) -> State:
@@ -350,26 +408,59 @@ def apply_node_gathered_info(event: NodeGatheredInfo, state: State) -> State:
event.node_id: NodeThunderboltInfo(interfaces=info.idents),
}
case MacThunderboltConnections():
update["node_thunderbolt_connections"] = {
**state.node_thunderbolt_connections,
event.node_id: info,
conn_map = {
tb_ident.domain_uuid: (nid, tb_ident.rdma_interface)
for nid in state.node_thunderbolt
for tb_ident in state.node_thunderbolt[nid].interfaces
}
source_is_rdma_enabled = _is_rdma_ctl_enabled(
event.node_id, state.node_rdma_ctl
)
as_rdma_conns = [
Connection(
source=event.node_id,
sink=conn_map[tb_conn.sink_uuid][0],
edge=RDMAConnection(
source_rdma_iface=conn_map[tb_conn.source_uuid][1],
sink_rdma_iface=conn_map[tb_conn.sink_uuid][1],
),
)
for tb_conn in info.conns
if tb_conn.source_uuid in conn_map
if tb_conn.sink_uuid in conn_map
if source_is_rdma_enabled
and _is_rdma_ctl_enabled(
conn_map[tb_conn.sink_uuid][0], state.node_rdma_ctl
)
]
topology.replace_all_out_rdma_connections(event.node_id, as_rdma_conns)
case ThunderboltBridgeInfo():
new_tb_bridge: dict[NodeId, ThunderboltBridgeStatus] = {
**state.node_thunderbolt_bridge,
event.node_id: info.status,
}
update["node_thunderbolt_bridge"] = new_tb_bridge
update["thunderbolt_bridge_cycles"] = (
topology.get_thunderbolt_bridge_cycles(
new_tb_bridge, state.node_network
# Only recompute cycles if the enabled status changed
old_status = state.node_thunderbolt_bridge.get(event.node_id)
old_enabled = old_status.enabled if old_status else False
new_enabled = info.status.enabled
if old_enabled != new_enabled:
update["thunderbolt_bridge_cycles"] = (
topology.get_thunderbolt_bridge_cycles(
new_tb_bridge, state.node_network
)
)
)
case RdmaCtlStatus():
update["node_rdma_ctl"] = {
**state.node_rdma_ctl,
event.node_id: NodeRdmaCtlStatus(enabled=info.enabled),
}
# If RDMA just got disabled on this node, drop any RDMA edges touching it
# so placement / topology consumers cannot pick a disabled node for an
# RDMA-backed instance. (Edges will repopulate on the next
# MacThunderboltConnections poll once both endpoints are enabled again.)
if not info.enabled:
topology.remove_all_rdma_connections_touching(event.node_id)
case NodeBackends():
update["node_backends"] = {
**state.node_backends,
@@ -380,45 +471,32 @@ def apply_node_gathered_info(event: NodeGatheredInfo, state: State) -> State:
def apply_topology_edge_created(event: TopologyEdgeCreated, state: State) -> State:
source_connections = state.node_socket_connections.get(event.conn.source, {})
sink_connections = source_connections.get(event.conn.sink, [])
update = {
"node_socket_connections": {
**state.node_socket_connections,
event.conn.source: {
**source_connections,
event.conn.sink: sink_connections
if event.conn.edge in sink_connections
else [*sink_connections, event.conn.edge],
},
}
}
return state.model_copy(update=update)
topology = copy.deepcopy(state.topology)
topology.add_connection(event.conn)
return state.model_copy(update={"topology": topology})
def apply_topology_edge_deleted(event: TopologyEdgeDeleted, state: State) -> State:
inner_update = {
sink: final_edges
for sink, edges in state.node_socket_connections.get(
event.conn.source, {}
).items()
if (
final_edges := [
edge
for edge in edges
if (edge != event.conn.edge or sink != event.conn.sink)
]
)
topology = copy.deepcopy(state.topology)
topology.remove_connection(event.conn)
# TODO: Clean up removing the reverse connection
return state.model_copy(update={"topology": topology})
def apply_custom_model_card_added(event: CustomModelCardAdded, state: State) -> State:
new_cards: Mapping[ModelId, ModelCard] = {
**state.custom_model_cards,
event.model_card.model_id: event.model_card,
}
update = {
"node_socket_connections": {
source: maps
for source, maps in {
**state.node_socket_connections,
event.conn.source: inner_update,
}.items()
if maps
}
return state.model_copy(update={"custom_model_cards": new_cards})
def apply_custom_model_card_deleted(
event: CustomModelCardDeleted, state: State
) -> State:
new_cards: Mapping[ModelId, ModelCard] = {
model_id: card
for model_id, card in state.custom_model_cards.items()
if model_id != event.model_id
}
return state.model_copy(update=update)
return state.model_copy(update={"custom_model_cards": new_cards})
+1 -1
View File
@@ -55,7 +55,7 @@ class _CardCache:
except OSError as e:
logger.warning(f"failed to save custom model card ({e.strerror})")
async def delete(self, model_id: ModelId) -> "ModelCard | None":
async def pop(self, model_id: ModelId) -> "ModelCard | None":
"""Delete a user-added custom model card. Returns True if deleted."""
card_path = _custom_cards_dir / (ModelId(model_id).normalize() + ".toml")
try:
@@ -0,0 +1,46 @@
from exo.shared.apply import apply
from exo.shared.models.model_cards import ModelCard, ModelTask
from exo.shared.types.backends import Backend
from exo.shared.types.common import ModelId
from exo.shared.types.events import (
CustomModelCardAdded,
CustomModelCardDeleted,
IndexedEvent,
)
from exo.shared.types.memory import Memory
from exo.shared.types.state import State
def _model_card(model_id: ModelId) -> ModelCard:
return ModelCard(
model_id=model_id,
n_layers=1,
storage_size=Memory.from_bytes(1),
hidden_size=1,
supports_tensor=True,
tasks=[ModelTask.TextGeneration],
backends=[Backend.MlxMetal],
)
def test_custom_model_card_added_is_reduced_into_state() -> None:
card = _model_card(ModelId("custom/model"))
state = apply(
State(),
IndexedEvent(idx=0, event=CustomModelCardAdded(model_card=card)),
)
assert state.custom_model_cards == {card.model_id: card}
def test_custom_model_card_deleted_removes_card_from_state() -> None:
card = _model_card(ModelId("custom/model"))
state = State(custom_model_cards={card.model_id: card}, last_event_applied_idx=0)
state = apply(
state,
IndexedEvent(idx=1, event=CustomModelCardDeleted(model_id=card.model_id)),
)
assert state.custom_model_cards == {}
@@ -0,0 +1,72 @@
from exo.shared.apply import (
apply_instance_deleted,
apply_instance_link_created,
apply_instance_link_deleted,
)
from exo.shared.types.events import (
InstanceDeleted,
InstanceLinkCreated,
InstanceLinkDeleted,
)
from exo.shared.types.instance_link import InstanceLink, InstanceLinkId
from exo.shared.types.state import State
from exo.shared.types.worker.instances import InstanceId
def _link(
prefill: list[InstanceId],
decode: list[InstanceId],
link_id: InstanceLinkId | None = None,
) -> InstanceLink:
return InstanceLink(
link_id=link_id or InstanceLinkId(),
prefill_instances=prefill,
decode_instances=decode,
)
def test_create_link() -> None:
state = State()
link = _link([InstanceId("a")], [InstanceId("b")])
new_state = apply_instance_link_created(InstanceLinkCreated(link=link), state)
assert new_state.instance_links == {link.link_id: link}
def test_update_replaces_existing_link() -> None:
a, b, c = InstanceId("a"), InstanceId("b"), InstanceId("c")
link = _link([a], [b])
state = State(instance_links={link.link_id: link})
updated = link.model_copy(update={"decode_instances": [b, c]})
new_state = apply_instance_link_created(InstanceLinkCreated(link=updated), state)
assert set(new_state.instance_links[link.link_id].decode_instances) == {b, c}
def test_delete_link() -> None:
link = _link([InstanceId("a")], [InstanceId("b")])
state = State(instance_links={link.link_id: link})
new_state = apply_instance_link_deleted(
InstanceLinkDeleted(link_id=link.link_id), state
)
assert new_state.instance_links == {}
def test_instance_deleted_strips_from_links() -> None:
a, b, c = InstanceId("a"), InstanceId("b"), InstanceId("c")
link = _link([a, c], [b])
state = State(instance_links={link.link_id: link})
new_state = apply_instance_deleted(InstanceDeleted(instance_id=a), state)
remaining = new_state.instance_links[link.link_id]
assert remaining.prefill_instances == [c]
assert remaining.decode_instances == [b]
def test_instance_deleted_drops_link_when_role_empties() -> None:
a, b = InstanceId("a"), InstanceId("b")
link = _link([a], [b])
state = State(instance_links={link.link_id: link})
new_state = apply_instance_deleted(InstanceDeleted(instance_id=a), state)
assert link.link_id not in new_state.instance_links
@@ -217,7 +217,7 @@ def test_topology_remove_all_rdma_connections_touching_keeps_socket_edges():
)
)
socket_edge = SocketConnection(
sink_multiaddr=Multiaddr(address="/ip4/10.0.0.1/tcp/8000"),
sink_multiaddr=Multiaddr(address="/ip4/10.0.0.1/tcp/8000")
)
topology.add_connection(Connection(source=node_a, sink=node_b, edge=socket_edge))
@@ -0,0 +1,35 @@
from exo.shared.types.common import NodeId
from exo.shared.types.multiaddr import Multiaddr
from exo.shared.types.state import State
from exo.shared.types.topology import Connection, SocketConnection
def test_state_serialization_roundtrip() -> None:
"""Verify that State → JSON → State round-trip preserves topology."""
# --- build a simple state ------------------------------------------------
node_a = NodeId("node-a")
node_b = NodeId("node-b")
connection = Connection(
source=node_a,
sink=node_b,
edge=SocketConnection(
sink_multiaddr=Multiaddr(address="/ip4/127.0.0.1/tcp/10001"),
),
)
state = State()
state.topology.add_connection(connection)
json_repr = state.model_dump_json()
restored_state = State.model_validate_json(json_repr)
assert (
state.topology.to_snapshot().nodes
== restored_state.topology.to_snapshot().nodes
)
assert set(state.topology.to_snapshot().connections) == set(
restored_state.topology.to_snapshot().connections
)
assert restored_state.model_dump_json() == json_repr
+33
View File
@@ -1,8 +1,10 @@
import contextlib
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from typing import Iterable
import rustworkx as rx
from pydantic import BaseModel, ConfigDict
from exo.shared.types.common import NodeId
from exo.shared.types.profiling import (
@@ -18,6 +20,15 @@ from exo.shared.types.topology import (
)
class TopologySnapshot(BaseModel):
nodes: Sequence[NodeId]
connections: Mapping[
NodeId, Mapping[NodeId, Sequence[SocketConnection | RDMAConnection]]
]
model_config = ConfigDict(frozen=True, extra="forbid")
@dataclass
class Topology:
_graph: rx.PyDiGraph[NodeId, SocketConnection | RDMAConnection] = field(
@@ -25,6 +36,28 @@ class Topology:
)
_vertex_indices: dict[NodeId, int] = field(init=False, default_factory=dict)
def to_snapshot(self) -> TopologySnapshot:
return TopologySnapshot(
nodes=list(self.list_nodes()), connections=self.map_connections()
)
@classmethod
def from_snapshot(cls, snapshot: TopologySnapshot) -> "Topology":
topology = cls()
for node_id in snapshot.nodes:
with contextlib.suppress(ValueError):
topology.add_node(node_id)
for source in snapshot.connections:
for sink in snapshot.connections[source]:
for edge in snapshot.connections[source][sink]:
topology.add_connection(
Connection(source=source, sink=sink, edge=edge)
)
return topology
def add_node(self, node_id: NodeId) -> None:
if node_id in self._vertex_indices:
return
+23
View File
@@ -7,6 +7,7 @@ from exo.api.types import (
from exo.shared.models.model_cards import ModelCard, ModelId
from exo.shared.types.chunks import InputImageChunk
from exo.shared.types.common import CommandId, NodeId, SystemId
from exo.shared.types.instance_link import InstanceLinkId
from exo.shared.types.text_generation import TextGenerationTaskParams
from exo.shared.types.worker.instances import Instance, InstanceId, InstanceMeta
from exo.shared.types.worker.shards import Sharding, ShardMetadata
@@ -81,6 +82,24 @@ class CancelDownload(BaseCommand):
model_id: ModelId
class AddCustomModelCard(BaseCommand):
model_card: ModelCard
class DeleteCustomModelCard(BaseCommand):
model_id: ModelId
class SetInstanceLink(BaseCommand):
link_id: InstanceLinkId
prefill_instances: list[InstanceId]
decode_instances: list[InstanceId]
class DeleteInstanceLink(BaseCommand):
link_id: InstanceLinkId
DownloadCommand = StartDownload | DeleteDownload | CancelDownload
@@ -96,6 +115,10 @@ Command = (
| TaskCancelled
| TaskFinished
| SendInputChunk
| AddCustomModelCard
| DeleteCustomModelCard
| SetInstanceLink
| DeleteInstanceLink
)
+23 -1
View File
@@ -3,9 +3,11 @@ from typing import final
from pydantic import Field
from exo.shared.models.model_cards import ModelCard
from exo.shared.topology import Connection
from exo.shared.types.chunks import Chunk, InputImageChunk
from exo.shared.types.common import CommandId, Id, NodeId, SessionId, SystemId
from exo.shared.types.common import CommandId, Id, ModelId, NodeId, SessionId, SystemId
from exo.shared.types.instance_link import InstanceLink, InstanceLinkId
from exo.shared.types.tasks import Task, TaskId, TaskStatus
from exo.shared.types.worker.downloads import DownloadProgress
from exo.shared.types.worker.instances import Instance, InstanceId
@@ -106,6 +108,14 @@ class TopologyEdgeDeleted(BaseEvent):
conn: Connection
class CustomModelCardAdded(BaseEvent):
model_card: ModelCard
class CustomModelCardDeleted(BaseEvent):
model_id: ModelId
@final
class TraceEventData(FrozenModel):
name: str
@@ -128,6 +138,14 @@ class TracesMerged(BaseEvent):
traces: list[TraceEventData]
class InstanceLinkCreated(BaseEvent):
link: InstanceLink
class InstanceLinkDeleted(BaseEvent):
link_id: InstanceLinkId
Event = (
TestEvent
| TaskCreated
@@ -147,6 +165,10 @@ Event = (
| TopologyEdgeDeleted
| TracesCollected
| TracesMerged
| CustomModelCardAdded
| CustomModelCardDeleted
| InstanceLinkCreated
| InstanceLinkDeleted
)
-4
View File
@@ -16,10 +16,6 @@ class MemoryUsage(FrozenModel):
swap_total: Memory
swap_available: Memory
@classmethod
def tag(cls) -> str:
return cls.__name__
@classmethod
def from_bytes(
cls, *, ram_total: int, ram_available: int, swap_total: int, swap_available: int
+27 -91
View File
@@ -1,15 +1,15 @@
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any
from typing import Any, cast
from exo_rs import LVAggregator
from pydantic import ConfigDict, Field, model_serializer
from pydantic import ConfigDict, Field, field_serializer, field_validator
from pydantic.alias_generators import to_camel
from pydantic_core.core_schema import SerializerFunctionWrapHandler
from exo.shared.topology import Topology
from exo.shared.models.model_cards import ModelCard
from exo.shared.topology import Topology, TopologySnapshot
from exo.shared.types.backends import Backend
from exo.shared.types.common import NodeId
from exo.shared.types.common import ModelId, NodeId
from exo.shared.types.instance_link import InstanceLink, InstanceLinkId
from exo.shared.types.profiling import (
DiskUsage,
MemoryUsage,
@@ -21,15 +21,9 @@ from exo.shared.types.profiling import (
ThunderboltBridgeStatus,
)
from exo.shared.types.tasks import Task, TaskId
from exo.shared.types.topology import (
Connection,
RDMAConnection,
SocketConnection,
)
from exo.shared.types.worker.downloads import DownloadProgress
from exo.shared.types.worker.instances import Instance, InstanceId
from exo.shared.types.worker.runners import RunnerId, RunnerStatus
from exo.utils.info_gatherer.info_gatherer import MacThunderboltConnections
from exo.utils.pydantic_ext import FrozenModel
@@ -45,6 +39,7 @@ class State(FrozenModel):
alias_generator=to_camel,
validate_by_name=True,
extra="forbid",
# I want to reenable this ASAP, but it's causing an issue with TaskStatus
strict=True,
arbitrary_types_allowed=True,
)
@@ -53,6 +48,7 @@ class State(FrozenModel):
downloads: Mapping[NodeId, Sequence[DownloadProgress]] = {}
tasks: Mapping[TaskId, Task] = {}
last_seen: Mapping[NodeId, datetime] = {}
topology: Topology = Field(default_factory=Topology)
last_event_applied_idx: int = Field(default=-1, ge=-1)
# Granular node state mappings (update independently at different frequencies)
@@ -65,94 +61,34 @@ class State(FrozenModel):
node_thunderbolt_bridge: Mapping[NodeId, ThunderboltBridgeStatus] = {}
node_rdma_ctl: Mapping[NodeId, NodeRdmaCtlStatus] = {}
node_backends: Mapping[NodeId, list[Backend]] = {}
node_socket_connections: Mapping[
NodeId, Mapping[NodeId, Sequence[SocketConnection]]
] = {}
node_thunderbolt_connections: Mapping[NodeId, MacThunderboltConnections] = {}
# Detected cycles where all nodes have Thunderbolt bridge enabled (>2 nodes)
thunderbolt_bridge_cycles: Sequence[Sequence[NodeId]] = []
instance_links: Mapping[InstanceLinkId, InstanceLink] = {}
prefill_server_ports: Mapping[RunnerId, int] = {}
@model_serializer(mode="wrap")
def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
data = handler(self) # pyright: ignore[reportAny]
data["topology"] = {
"nodes": list(self.node_identities.keys()),
"connections": self.topology.map_connections(),
}
return data # pyright: ignore[reportAny]
# User-added model cards. Workers can reconcile their on-disk custom card cache
custom_model_cards: Mapping[ModelId, ModelCard] = {}
@property
def topology(self) -> Topology:
topology = Topology()
thunderbolt_by_uuid = {
ident.domain_uuid: (node_id, ident.rdma_interface)
for node_id, info in self.node_thunderbolt.items()
for ident in info.interfaces
}
for node_id in self.node_identities:
topology.add_node(node_id)
@field_serializer("topology", mode="plain")
def _encode_topology(self, value: Topology) -> TopologySnapshot:
return value.to_snapshot()
for source, data in self.node_socket_connections.items():
for sink, conns in data.items():
for conn in conns:
topology.add_connection(
Connection(source=source, sink=sink, edge=conn)
)
@field_validator("topology", mode="before")
@classmethod
def _deserialize_topology(cls, value: object) -> Topology: # noqa: D401 Pydantic validator signature
"""Convert an incoming *value* into a :class:`Topology` instance.
for source, connections in self.node_thunderbolt_connections.items():
if not self.node_rdma_ctl.get(
source, NodeRdmaCtlStatus(enabled=False)
).enabled:
continue
for connection in connections.conns:
if (
source_iface := thunderbolt_by_uuid.get(connection.source_uuid)
) is None or (
sink_iface := thunderbolt_by_uuid.get(connection.sink_uuid)
) is None:
continue
if not self.node_rdma_ctl.get(
sink_iface[0], NodeRdmaCtlStatus(enabled=False)
).enabled:
continue
assert source_iface[0] == source, "registered invalid source uuid"
topology.add_connection(
Connection(
source=source_iface[0],
sink=sink_iface[0],
edge=RDMAConnection(
source_rdma_iface=source_iface[1],
sink_rdma_iface=sink_iface[1],
),
)
)
Accepts either an already constructed :class:`Topology` or a mapping
representing :class:`~shared.topology.TopologySnapshot`.
"""
return topology
if isinstance(value, Topology):
return value
def with_aggregator(self, aggregator: LVAggregator) -> "State":
from datetime import datetime, timezone
if isinstance(value, Mapping): # likely a snapshot-dict coming from JSON
snapshot = TopologySnapshot(**cast(dict[str, Any], value)) # type: ignore[arg-type]
return Topology.from_snapshot(snapshot)
from pydantic import TypeAdapter
from exo.shared.apply import event_apply
from exo.shared.types.events import NodeGatheredInfo
from exo.utils.info_gatherer.info_gatherer import GatheredInfo
state = self.model_copy()
for key, value in aggregator.dump().items():
try:
data = TypeAdapter[GatheredInfo](GatheredInfo).validate_json(value)
node_id = NodeId(key.split("/")[0])
event = NodeGatheredInfo(
node_id=node_id, when=str(datetime.now(tz=timezone.utc)), info=data
)
state = event_apply(event, state)
except Exception as e:
print(
f"\n{'=' * 10}key: {key} with exception {str(e)}\nvalue: {value}{'=' * 10}\n"
)
return state
raise TypeError("Invalid representation for Topology field in State")
+37 -45
View File
@@ -10,13 +10,11 @@ from typing import Self, cast
import anyio
from anyio import fail_after, open_process, to_thread
from anyio.streams.buffered import BufferedByteReceiveStream
from exo_rs import LVPublisher, SessionHandle
from loguru import logger
from pydantic import ValidationError
from exo.shared.constants import EXO_CONFIG_FILE, EXO_DEFAULT_MODELS_DIR
from exo.shared.types.backends import Backend
from exo.shared.types.common import NodeId
from exo.shared.types.memory import Memory
from exo.shared.types.profiling import (
DiskUsage,
@@ -29,6 +27,7 @@ from exo.shared.types.thunderbolt import (
ThunderboltConnectivity,
ThunderboltIdentifier,
)
from exo.utils.channels import Sender
from exo.utils.pydantic_ext import TaggedModel
from exo.utils.task_group import TaskGroup
@@ -402,42 +401,10 @@ GatheredInfo = (
@dataclass
class InfoGatherer:
session_handle: SessionHandle
node_id: NodeId
info_senders: dict[str, LVPublisher] = field(init=False, default_factory=dict)
info_sender: Sender[GatheredInfo]
_tg: TaskGroup = field(init=False, default_factory=TaskGroup)
_psutil_enabled: bool = field(init=False, default=False)
async def send(self, info: GatheredInfo):
if (tag := info.tag()) not in self.info_senders:
self.info_senders[tag] = self.session_handle.last_value_publisher(
f"metrics/{self.node_id}/{tag}"
)
await self.info_senders[tag].put(info.model_dump_json())
async def run(self):
async with self._tg as tg:
if IS_DARWIN:
tg.start_soon(self._monitor_macmon, 1)
tg.start_soon(self._monitor_system_profiler_thunderbolt_data, 5)
tg.start_soon(self._monitor_thunderbolt_bridge_status, 10)
tg.start_soon(self._monitor_rdma_ctl_status, 10)
if not IS_DARWIN:
tg.start_soon(self._monitor_memory_usage, 1)
tg.start_soon(self._watch_system_info, 10)
tg.start_soon(self._monitor_misc, 60)
tg.start_soon(self._monitor_static_info, 60)
tg.start_soon(self._monitor_disk_usage, 30)
nc = await NodeConfig.gather()
if nc is not None:
await self.send(nc)
await self.send(await NodeBackends.gather())
def shutdown(self):
self._tg.cancel_tasks()
async def _can_read_macmon_metrics(self, macmon_path: str) -> bool:
try:
with fail_after(5):
@@ -474,11 +441,34 @@ class InfoGatherer:
return True
async def run(self):
async with self._tg as tg:
if IS_DARWIN:
tg.start_soon(self._monitor_macmon, 1)
tg.start_soon(self._monitor_system_profiler_thunderbolt_data, 5)
tg.start_soon(self._monitor_thunderbolt_bridge_status, 10)
tg.start_soon(self._monitor_rdma_ctl_status, 10)
if not IS_DARWIN:
tg.start_soon(self._monitor_memory_usage, 1)
tg.start_soon(self._watch_system_info, 10)
tg.start_soon(self._monitor_misc, 60)
tg.start_soon(self._monitor_static_info, 60)
tg.start_soon(self._monitor_disk_usage, 30)
nc = await NodeConfig.gather()
if nc is not None:
await self.info_sender.send(nc)
await self.info_sender.send(await NodeBackends.gather())
def shutdown(self):
self._tg.cancel_tasks()
async def _monitor_static_info(self, static_info_poll_interval: float):
while True:
try:
with fail_after(30):
await self.send(await StaticNodeInformation.gather())
await self.info_sender.send(await StaticNodeInformation.gather())
except Exception as e:
logger.opt(exception=e).warning("Error gathering static node info")
await anyio.sleep(static_info_poll_interval)
@@ -487,7 +477,7 @@ class InfoGatherer:
while True:
try:
with fail_after(10):
await self.send(await MiscData.gather())
await self.info_sender.send(await MiscData.gather())
except Exception as e:
logger.opt(exception=e).warning("Error gathering misc data")
await anyio.sleep(misc_poll_interval)
@@ -508,10 +498,12 @@ class InfoGatherer:
idents = [
it for i in data if (it := i.ident(iface_map)) is not None
]
await self.send(MacThunderboltIdentifiers(idents=idents))
await self.info_sender.send(
MacThunderboltIdentifiers(idents=idents)
)
conns = [it for i in data if (it := i.conn()) is not None]
await self.send(MacThunderboltConnections(conns=conns))
await self.info_sender.send(MacThunderboltConnections(conns=conns))
except Exception as e:
logger.opt(exception=e).warning("Error gathering Thunderbolt data")
await anyio.sleep(system_profiler_interval)
@@ -528,7 +520,7 @@ class InfoGatherer:
)
while True:
try:
await self.send(
await self.info_sender.send(
MemoryUsage.from_psutil(override_memory=override_memory)
)
except Exception as e:
@@ -540,7 +532,7 @@ class InfoGatherer:
try:
with fail_after(10):
nics = await get_network_interfaces()
await self.send(NodeNetworkInterfaces(ifaces=nics))
await self.info_sender.send(NodeNetworkInterfaces(ifaces=nics))
except Exception as e:
logger.opt(exception=e).warning("Error gathering network interfaces")
await anyio.sleep(interface_watcher_interval)
@@ -553,7 +545,7 @@ class InfoGatherer:
with fail_after(30):
curr = await ThunderboltBridgeInfo.gather()
if curr is not None:
await self.send(curr)
await self.info_sender.send(curr)
except Exception as e:
logger.opt(exception=e).warning(
"Error gathering Thunderbolt Bridge status"
@@ -565,7 +557,7 @@ class InfoGatherer:
try:
curr = await RdmaCtlStatus.gather()
if curr is not None:
await self.send(curr)
await self.info_sender.send(curr)
except Exception as e:
logger.opt(exception=e).warning("Error gathering RDMA ctl status")
await anyio.sleep(rdma_ctl_poll_interval)
@@ -574,7 +566,7 @@ class InfoGatherer:
while True:
try:
with fail_after(5):
await self.send(await NodeDiskUsage.gather())
await self.info_sender.send(await NodeDiskUsage.gather())
except Exception as e:
logger.opt(exception=e).warning("Error gathering disk usage")
await anyio.sleep(disk_poll_interval)
@@ -619,7 +611,7 @@ class InfoGatherer:
)
text = data.decode("utf-8", errors="replace").strip()
metrics = MacmonMetrics.from_raw_json(text)
await self.send(metrics)
await self.info_sender.send(metrics)
except TimeoutError:
logger.warning(
f"MacMon produced no output for {read_timeout}s, restarting"
+4 -8
View File
@@ -19,22 +19,18 @@ class FrozenModel(BaseModel):
class TaggedModel(FrozenModel):
@classmethod
def tag(cls) -> str:
return cls.__name__
@model_serializer(mode="wrap")
def _serialize(self, handler: SerializerFunctionWrapHandler):
inner = handler(self) # pyright: ignore[reportAny]
return {self.tag(): inner}
return {self.__class__.__name__: inner}
@model_validator(mode="wrap")
@classmethod
def _validate(cls, v: Any, handler: ValidatorFunctionWrapHandler) -> Self: # pyright: ignore[reportAny]
if isinstance(v, dict) and len(v) == 1 and cls.tag() in v: # pyright: ignore[reportUnknownArgumentType]
return handler(v[cls.tag()]) # pyright: ignore[reportAny]
if isinstance(v, dict) and len(v) == 1 and cls.__name__ in v: # pyright: ignore[reportUnknownArgumentType]
return handler(v[cls.__name__]) # pyright: ignore[reportAny]
return handler(v) # pyright: ignore[reportAny]
def __str__(self) -> str:
return f"{self.tag()}({super().__str__()})"
return f"{self.__class__.__name__}({super().__str__()})"
+18 -28
View File
@@ -4,9 +4,7 @@ from datetime import datetime, timezone
import anyio
from anyio import fail_after, to_thread
from exo_rs import LVAggregator, SessionHandle
from loguru import logger
from pydantic import ValidationError
from exo.api.types import ImageEditsTaskParams
from exo.download.download_utils import is_read_only_model_dir, resolve_existing_model
@@ -16,8 +14,7 @@ from exo.routing.event_router import (
)
from exo.shared.apply import apply
from exo.shared.constants import EXO_MAX_INSTANCE_RETRIES
from exo.shared.models import model_cards
from exo.shared.models.model_cards import ModelCard, ModelId
from exo.shared.models.model_cards import ModelId, card_cache
from exo.shared.types.chunks import InputImageChunk
from exo.shared.types.commands import (
DeleteInstance,
@@ -56,7 +53,7 @@ from exo.shared.types.topology import Connection, SocketConnection
from exo.shared.types.worker.downloads import DownloadCompleted
from exo.shared.types.worker.instances import InstanceId
from exo.shared.types.worker.runners import RunnerId
from exo.utils.channels import Receiver, Sender
from exo.utils.channels import Receiver, Sender, channel
from exo.utils.info_gatherer.info_gatherer import GatheredInfo, InfoGatherer
from exo.utils.info_gatherer.net_profile import check_reachable
from exo.utils.keyed_backoff import KeyedBackoff
@@ -76,7 +73,6 @@ class Worker:
# but I think it's the correct way to be thinking about commands
command_sender: Sender[ForwarderCommand],
download_command_sender: Sender[ForwarderDownloadCommand],
session_handle: SessionHandle,
api_port: int,
):
self.node_id: NodeId = node_id
@@ -102,17 +98,17 @@ class Worker:
base=0.5, cap=10.0
)
self._stopped: anyio.Event = anyio.Event()
self._sh: SessionHandle = session_handle
self.aggregator: LVAggregator = session_handle.last_value_aggregator("metrics")
async def run(self):
logger.info("Starting Worker")
info_gatherer: InfoGatherer = InfoGatherer(self._sh, self.node_id)
info_send, info_recv = channel[GatheredInfo]()
info_gatherer: InfoGatherer = InfoGatherer(info_send)
try:
async with self._tg as tg:
tg.start_soon(info_gatherer.run)
tg.start_soon(self._forward_info, info_recv)
tg.start_soon(self.plan_step)
tg.start_soon(self._event_applier)
tg.start_soon(self._poll_connection_updates)
@@ -182,24 +178,17 @@ class Worker:
] = img
async def _reconcile_custom_cards(self) -> None:
storage = self._sh.storage_interface()
while True:
await anyio.sleep(10)
target: list[ModelId] = []
for _, value in (await storage.dump("custom_model_cards/")).items():
try:
card = ModelCard.model_validate_json(value)
except ValidationError:
await anyio.sleep(1)
target = dict(self.state.custom_model_cards)
for model_id, card in target.items():
if card_cache.get(model_id) == card:
continue
target.append(card.model_id)
if model_cards.card_cache.get(card.model_id) == card:
continue
logger.info(f"Registered new custom model card for {card.model_id}")
await model_cards.card_cache.save(card)
await card_cache.save(card)
for card in await model_cards.card_cache.list_all():
for card in await card_cache.list_all():
if card.model_id not in target:
await model_cards.card_cache.delete(card.model_id)
await card_cache.pop(card.model_id)
async def plan_step(self):
while True:
@@ -399,13 +388,14 @@ class Worker:
async def _poll_connection_updates(self):
while True:
state = self.state.with_aggregator(self.aggregator)
edges = set(conn.edge for conn in state.topology.out_edges(self.node_id))
edges = set(
conn.edge for conn in self.state.topology.out_edges(self.node_id)
)
conns: defaultdict[NodeId, set[str]] = defaultdict(set)
async for ip, nid in check_reachable(
state.topology,
self.state.topology,
self.node_id,
state.node_network,
self.state.node_network,
api_port=self.api_port,
):
if ip in conns[nid]:
@@ -426,7 +416,7 @@ class Worker:
)
)
for conn in state.topology.out_edges(self.node_id):
for conn in self.state.topology.out_edges(self.node_id):
if not isinstance(conn.edge, SocketConnection):
continue
# ignore mDNS discovered connections
+1 -1
View File
@@ -42,7 +42,7 @@ i=0
for host; do
colour=${colours[i++ % 4]}
ssh -T -o BatchMode=yes -o ServerAliveInterval=30 "$host@$host" \
"ENABLE_DISAGGREGATION=true EXO_ZENOH_NAMESPACE=$commit /nix/var/nix/profiles/default/bin/nix run $remote_installable" 2>&1 |
"EXO_ZENOH_NAMESPACE=$commit /nix/var/nix/profiles/default/bin/nix run $remote_installable" 2>&1 |
awk -v p="${colour}[${host}]${reset}" '{ print p $0; fflush() }' &
done
Generated
+8 -8
View File
@@ -450,7 +450,7 @@ build = [
]
mlx = [
{ name = "mflux", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260512+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260506+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_aarch64.whl" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_x86_64.whl" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx-lm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
@@ -461,7 +461,7 @@ mlx = [
]
mlx-cpu = [
{ name = "mflux", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "mlx", version = "0.32.0.dev20260512+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-mlx-cpu') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260506+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-mlx-cpu') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_aarch64.whl" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-3-exo-mlx-cpu') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra != 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra != 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra != 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_x86_64.whl" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-3-exo-mlx-cpu') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra != 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra != 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra != 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx-cpu", marker = "sys_platform == 'linux'" },
@@ -473,7 +473,7 @@ mlx-cpu = [
]
mlx-cuda12 = [
{ name = "mflux", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "mlx", version = "0.32.0.dev20260512+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260506+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_aarch64.whl" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra != 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_x86_64.whl" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra != 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx-cuda-12", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx_cuda_12-0.32.0-py3-none-manylinux_2_35_aarch64.whl" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra != 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
@@ -487,7 +487,7 @@ mlx-cuda12 = [
]
mlx-cuda13 = [
{ name = "mflux", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "mlx", version = "0.32.0.dev20260512+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260506+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_aarch64.whl" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_x86_64.whl" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx-cuda-13", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx_cuda_13-0.32.0-py3-none-manylinux_2_35_aarch64.whl" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none')" },
@@ -1343,7 +1343,7 @@ dependencies = [
{ name = "hf-transfer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "matplotlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260512+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260506+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_aarch64.whl" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_x86_64.whl" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
@@ -1374,7 +1374,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/fa/96d4cc7ada2833571
[[package]]
name = "mlx"
version = "0.32.0.dev20260512+cc3f3e60"
version = "0.32.0.dev20260506+cc3f3e60"
source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }
resolution-markers = [
"sys_platform == 'darwin'",
@@ -1549,7 +1549,7 @@ version = "0.31.3"
source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4#6a3df6cd6b00a347ee40f12d97a182aaf86ea599" }
dependencies = [
{ name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260512+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260506+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
@@ -1565,7 +1565,7 @@ dependencies = [
{ name = "datasets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "miniaudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260512+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0.dev20260506+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_aarch64.whl" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine != 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx", version = "0.32.0", source = { url = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv/releases/download/mlx_cuda/mlx-0.32.0-cp313-cp313-manylinux_2_35_x86_64.whl" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (platform_machine == 'aarch64' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (sys_platform != 'linux' and extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },
{ name = "mlx-lm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda12') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cpu' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-cuda13') or (extra == 'extra-3-exo-mlx-cuda12' and extra == 'extra-3-exo-mlx-none') or (extra == 'extra-3-exo-mlx-cuda13' and extra == 'extra-3-exo-mlx-none')" },