Compare commits

...
2 Commits
Author SHA1 Message Date
Evan 72897dd9da custom model cards + instance links 2026-06-03 17:09:18 +01:00
Evan 150fa21b2b move metrics to zenoh Last Value semantics 2026-06-03 17:07:58 +01:00
35 changed files with 1422 additions and 655 deletions

No files matched your search

Generated
+54 -29
View File
@@ -1007,6 +1007,7 @@ dependencies = [
"serde_json",
"tokio",
"zenoh",
"zenoh-ext",
]
[[package]]
@@ -1842,6 +1843,12 @@ 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"
@@ -5107,7 +5114,7 @@ dependencies = [
[[package]]
name = "zenoh"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"ahash",
"arc-swap",
@@ -5157,7 +5164,7 @@ dependencies = [
[[package]]
name = "zenoh-buffers"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"zenoh-collections",
]
@@ -5165,7 +5172,7 @@ dependencies = [
[[package]]
name = "zenoh-codec"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"tracing",
"uhlc",
@@ -5176,7 +5183,7 @@ dependencies = [
[[package]]
name = "zenoh-collections"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"ahash",
]
@@ -5184,7 +5191,7 @@ dependencies = [
[[package]]
name = "zenoh-config"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"json5",
"nonempty-collections",
@@ -5209,7 +5216,7 @@ dependencies = [
[[package]]
name = "zenoh-core"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"lazy_static",
"tokio",
@@ -5220,7 +5227,7 @@ dependencies = [
[[package]]
name = "zenoh-crypto"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"aes",
"hmac",
@@ -5230,10 +5237,29 @@ 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#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"getrandom 0.2.17",
"hashbrown 0.16.1",
@@ -5248,7 +5274,7 @@ dependencies = [
[[package]]
name = "zenoh-link"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"zenoh-config",
"zenoh-link-commons",
@@ -5266,7 +5292,7 @@ dependencies = [
[[package]]
name = "zenoh-link-commons"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"base64",
@@ -5302,7 +5328,7 @@ dependencies = [
[[package]]
name = "zenoh-link-quic"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"rustls-webpki",
@@ -5318,7 +5344,7 @@ dependencies = [
[[package]]
name = "zenoh-link-quic_datagram"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"rustls-webpki",
@@ -5334,7 +5360,7 @@ dependencies = [
[[package]]
name = "zenoh-link-tcp"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"socket2 0.5.10",
@@ -5351,7 +5377,7 @@ dependencies = [
[[package]]
name = "zenoh-link-tls"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"base64",
@@ -5380,7 +5406,7 @@ dependencies = [
[[package]]
name = "zenoh-link-udp"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"libc",
@@ -5402,7 +5428,7 @@ dependencies = [
[[package]]
name = "zenoh-link-unixsock_stream"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"nix 0.29.0",
@@ -5420,7 +5446,7 @@ dependencies = [
[[package]]
name = "zenoh-link-ws"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"futures-util",
@@ -5440,7 +5466,7 @@ dependencies = [
[[package]]
name = "zenoh-macros"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"proc-macro2",
"quote",
@@ -5451,8 +5477,7 @@ dependencies = [
[[package]]
name = "zenoh-plugin-storage-manager"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d9fd55c490af4dc4abb6d51b620ecf6f824a9285a1f9446b59eeec6d66ccf7"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"bincode",
@@ -5479,7 +5504,7 @@ dependencies = [
[[package]]
name = "zenoh-plugin-trait"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"git-version",
"libloading",
@@ -5496,7 +5521,7 @@ dependencies = [
[[package]]
name = "zenoh-protocol"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"const_format",
"rand 0.8.6",
@@ -5511,7 +5536,7 @@ dependencies = [
[[package]]
name = "zenoh-result"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"anyhow",
]
@@ -5519,7 +5544,7 @@ dependencies = [
[[package]]
name = "zenoh-runtime"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"lazy_static",
"ron",
@@ -5533,7 +5558,7 @@ dependencies = [
[[package]]
name = "zenoh-sync"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"arc-swap",
"event-listener",
@@ -5547,7 +5572,7 @@ dependencies = [
[[package]]
name = "zenoh-task"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"futures",
"tokio",
@@ -5560,7 +5585,7 @@ dependencies = [
[[package]]
name = "zenoh-transport"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"crossbeam-utils",
@@ -5594,7 +5619,7 @@ dependencies = [
[[package]]
name = "zenoh-util"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"const_format",
@@ -5620,7 +5645,7 @@ dependencies = [
[[package]]
name = "zenoh_backend_traits"
version = "1.9.0"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#aa219b84e55dfbbf55ebd827b492626a470b8ac8"
source = "git+https://github.com/evanev7/zenoh.git?branch=exo#2fed58ef264a8415b29452cc37054b2e04c10396"
dependencies = [
"async-trait",
"const_format",
+3
View File
@@ -49,6 +49,7 @@ 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"
@@ -61,6 +62,7 @@ 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" }
@@ -86,6 +88,7 @@ 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]
+1
View File
@@ -48,6 +48,7 @@ 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,14 +2,21 @@
# 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:
@@ -31,6 +38,18 @@ 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
@@ -111,3 +130,24 @@ 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
@@ -0,0 +1,181 @@
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(())
}
+13 -11
View File
@@ -4,13 +4,18 @@
//!
//!
mod allow_threading;
// mod ident;
mod networking;
mod pidfile;
pub mod allow_threading;
pub mod last_value;
pub mod networking;
pub mod pidfile;
pub mod session;
mod storage;
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;
@@ -154,15 +159,12 @@ 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, 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...
// TODO: for now this is all NOT a submodule. KISS
pidfile_submodule(m)?;
// m.add_class::<PyKeypair>()?;
networking_submodule(m)?;
// top-level constructs
// TODO: ...
lv_submodule(m)?;
session_submodule(m)?;
storage_submodule(m)?;
Ok(())
}
+111
View File
@@ -0,0 +1,111 @@
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
@@ -0,0 +1,199 @@
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
@@ -0,0 +1,168 @@
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
}
+35 -38
View File
@@ -1,54 +1,51 @@
#[cfg(test)]
mod tests {
use core::mem::drop;
use core::option::Option::Some;
use core::time::Duration;
use tokio;
use tokio::sync::mpsc;
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
@@ -0,0 +1,69 @@
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")
+29 -14
View File
@@ -7,9 +7,16 @@ use zenoh_plugin_trait::PluginsManager;
pub use zenoh::{Config, config::ZenohId};
use crate::discovery::Discovery;
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";
pub mod discovery;
pub mod liveliness_aggregator;
pub mod swarm;
pub fn is_valid_zid(identity: &str) -> bool {
@@ -33,20 +40,22 @@ 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",
r#"{
key_expr: "storage/mem1/**",
strip_prefix: "storage/mem1",
volume: "memory",
replication: {
interval: 2,
}
}"#,
"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",
)?;
Ok(cfg)
}
@@ -99,10 +108,15 @@ pub async fn open(
.await;
}
})));
Ok(Session { z, _jh })
let liveliness_aggregator = spawn_liveliness_aggregator(&z)?;
Ok(Session {
z,
liveliness_aggregator,
_jh,
})
}
struct AbortOnDrop(JoinHandle<()>);
pub struct AbortOnDrop(pub JoinHandle<()>);
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
@@ -112,5 +126,6 @@ impl Drop for AbortOnDrop {
#[derive(Clone)]
pub struct Session {
pub z: ZSession,
pub liveliness_aggregator: LivelinessAggregator,
_jh: Arc<AbortOnDrop>,
}
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,99 @@
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");
}
+60 -53
View File
@@ -13,6 +13,7 @@ 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
@@ -22,6 +23,7 @@ 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,
@@ -153,14 +155,11 @@ from exo.shared.types.chunks import (
ToolCallChunk,
)
from exo.shared.types.commands import (
AddCustomModelCard,
CancelDownload,
Command,
CreateInstance,
DeleteCustomModelCard,
DeleteDownload,
DeleteInstance,
DeleteInstanceLink,
DownloadCommand,
ForwarderCommand,
ForwarderDownloadCommand,
@@ -168,7 +167,6 @@ from exo.shared.types.commands import (
ImageGeneration,
PlaceInstance,
SendInputChunk,
SetInstanceLink,
StartDownload,
TaskCancelled,
TaskFinished,
@@ -246,6 +244,7 @@ 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)
@@ -258,6 +257,8 @@ 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()
@@ -409,10 +410,12 @@ 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 self.state
return state
try:
x = self.state.model_dump(by_alias=True)
x: Any = state.model_dump(by_alias=True)
for attr in path.split("/"):
if attr != "":
if isinstance(x, dict):
@@ -476,6 +479,7 @@ 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,
@@ -483,13 +487,13 @@ class API:
instance_meta=instance_meta,
min_nodes=min_nodes,
),
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,
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,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@@ -514,8 +518,9 @@ 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(self.state.topology.list_nodes())) == 0:
if len(list(state.topology.list_nodes())) == 0:
return PlacementPreviewResponse(previews=[])
try:
@@ -530,9 +535,7 @@ class API:
instance_combinations.extend(
[
(sharding, instance_meta, i)
for i in range(
1, len(list(self.state.topology.list_nodes())) + 1
)
for i in range(1, len(list(state.topology.list_nodes())) + 1)
]
)
# TODO: PDD
@@ -547,14 +550,14 @@ class API:
instance_meta=instance_meta,
min_nodes=min_nodes,
),
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,
node_memory=state.node_memory,
node_network=state.node_network,
node_backends=state.node_backends,
topology=state.topology,
current_instances=state.instances,
required_nodes=required_nodes,
download_status=self.state.downloads,
node_rdma_ctl=self.state.node_rdma_ctl,
download_status=state.downloads,
node_rdma_ctl=state.node_rdma_ctl,
)
except ValueError as exc:
if (model_card.model_id, sharding, instance_meta, 0) not in seen:
@@ -696,9 +699,16 @@ class API:
return {"disaggregation": ENABLE_DISAGGREGATION}
async def list_instance_links(self) -> list[InstanceLink]:
links: list[InstanceLink] = []
if not ENABLE_DISAGGREGATION:
return []
return list(self.state.instance_links.values())
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
async def create_instance_link(
self, body: InstanceLinkBody
@@ -715,25 +725,22 @@ class API:
async def _set_instance_link(
self, link_id: InstanceLinkId, body: InstanceLinkBody
) -> InstanceLinkResponse:
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
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(),
)
return InstanceLinkResponse(message="Command received.")
async def delete_instance_link(
self, link_id: InstanceLinkId
) -> InstanceLinkResponse:
_require_disaggregation_enabled()
command = DeleteInstanceLink(link_id=link_id)
await self._send(command)
return InstanceLinkResponse(
message="Command received.", command_id=command.command_id
)
await self.storage.delete(f"instance_links/{link_id}")
return InstanceLinkResponse(message="Command received.")
async def cancel_command(self, command_id: CommandId) -> CancelCommandResponse:
"""Cancel an active command by closing its stream and notifying workers."""
@@ -791,7 +798,11 @@ class API:
async def _collect_text_generation_with_stats(
self, command_id: CommandId
) -> BenchChatCompletionResponse:
sampler = PowerSampler(get_node_system=lambda: self.state.node_system)
sampler = PowerSampler(
get_node_system=lambda: self.state.with_aggregator(
self.aggregator
).node_system
)
text_parts: list[str] = []
tool_calls: list[ToolCall] = []
model: ModelId | None = None
@@ -1314,7 +1325,11 @@ class API:
num_images: int,
response_format: str,
) -> BenchImageGenerationResponse:
sampler = PowerSampler(get_node_system=lambda: self.state.node_system)
sampler = PowerSampler(
get_node_system=lambda: self.state.with_aggregator(
self.aggregator
).node_system
)
images: list[ImageData] = []
stats: ImageGenerationStats | None = None
async with anyio.create_task_group() as tg:
@@ -1778,7 +1793,7 @@ class API:
"""Calculate total available memory across all nodes in bytes."""
total_available = Memory()
for memory in self.state.node_memory.values():
for memory in self.state.with_aggregator(self.aggregator).node_memory.values():
total_available += memory.ram_available
return total_available
@@ -1827,11 +1842,8 @@ class API:
status_code=400, detail=f"Failed to fetch model: {exc}"
) from exc
await self.command_sender.send(
ForwarderCommand(
origin=self._system_id,
command=AddCustomModelCard(model_card=card),
)
await self.storage.put(
f"custom_model_cards/{card.model_id.normalize()}", card.model_dump_json()
)
# Immediately update the local cache so the subsequent GET /models
@@ -1856,12 +1868,7 @@ class API:
if card is None or not card.is_custom:
raise HTTPException(status_code=404, detail="Custom model card not found")
await self.command_sender.send(
ForwarderCommand(
origin=self._system_id,
command=DeleteCustomModelCard(model_id=model_id),
)
)
await self.storage.delete(f"custom_model_cards/{card.model_id.normalize()}")
return JSONResponse(
{"message": "Model card deleted", "model_id": str(model_id)}
-1
View File
@@ -329,7 +329,6 @@ class InstanceLinkBody(BaseModel):
class InstanceLinkResponse(BaseModel):
message: str
command_id: CommandId
ImageSize = Literal[
+21 -11
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
from exo_rs import Pidfile, PidfileError, SessionHandle
from loguru import logger
from pydantic import PositiveInt
@@ -47,18 +47,21 @@ 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)
router = Router.create(
session_handle, _nh = SessionHandle.new(
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)
@@ -97,6 +100,7 @@ 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
@@ -108,6 +112,7 @@ 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:
@@ -122,6 +127,8 @@ 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]()
@@ -150,6 +157,7 @@ class Node:
node_id,
args.offline,
args.api_port,
session_handle,
)
async def run(self):
@@ -225,6 +233,8 @@ 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 (
@@ -264,6 +274,7 @@ 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)
@@ -378,17 +389,16 @@ def main_inner(args: "Args"):
class Args(FrozenModel):
verbosity: int = 0
force_master: bool = False
spawn_api: bool = False
api_port: PositiveInt = 52415
tb_only: bool = False
verbosity: int
force_master: bool
spawn_api: bool
api_port: PositiveInt
no_worker: bool = False
no_downloads: bool = False
offline: bool = os.getenv("EXO_OFFLINE", "false").lower() == "true"
no_batch: bool = False
offline: bool
no_batch: bool
fast_synch: bool | None = None # None = auto, True = force on, False = force off
legacy_daemon: bool = False
legacy_daemon: bool
bootstrap_peers: list[str] = []
namespace: str
zenoh_port: int
@@ -476,7 +486,7 @@ class Args(FrozenModel):
type=int,
default=52414,
dest="zenoh_port",
help="Fixed TCP port for zenoh to listen.",
help="Fixed port for zenoh to listen on.",
)
parser.add_argument(
"--discovery-port",
+40 -45
View File
@@ -1,7 +1,9 @@
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,
@@ -18,11 +20,8 @@ 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,
@@ -30,7 +29,6 @@ from exo.shared.types.commands import (
PlaceInstance,
RequestEventLog,
SendInputChunk,
SetInstanceLink,
TaskCancelled,
TaskFinished,
TestCommand,
@@ -38,15 +36,11 @@ 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,
@@ -79,7 +73,9 @@ from exo.utils.event_buffer import MultiSourceBuffer
from exo.utils.task_group import TaskGroup
def _prefill_endpoint_for(state: State, decode_instance_id: InstanceId) -> str | None:
def _prefill_endpoint_for(
state: State, instance_links: list[InstanceLink], decode_instance_id: InstanceId
) -> str | None:
decode = state.instances.get(decode_instance_id)
if decode is None:
return None
@@ -88,7 +84,7 @@ def _prefill_endpoint_for(state: State, decode_instance_id: InstanceId) -> str |
return None
sources: set[InstanceId] = set()
for link in state.instance_links.values():
for link in instance_links:
if decode_instance_id in link.decode_instances:
sources.update(link.prefill_instances)
sources.discard(decode_instance_id)
@@ -130,6 +126,8 @@ 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
@@ -146,6 +144,8 @@ 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,10 +182,21 @@ class Master:
pass
case TextGeneration():
# set-difference => prefill-only nodes
instance_links: list[InstanceLink] = []
prefill_only: set[InstanceId] = set()
for link in self.state.instance_links.values():
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:
prefill_only.update(link.prefill_instances)
for link in self.state.instance_links.values():
for link in instance_links:
prefill_only.difference_update(link.decode_instances)
for instance in self.state.instances.values():
@@ -225,7 +236,9 @@ class Master:
params = command.task_params.model_copy(
update={
"prefill_endpoint": _prefill_endpoint_for(
self.state, decode_instance_id
self.state.with_aggregator(self.aggregator),
instance_links,
decode_instance_id,
),
}
)
@@ -369,15 +382,16 @@ class Master:
)
generated_events.extend(transition_events)
case PlaceInstance():
state = self.state.with_aggregator(self.aggregator)
placement = place_instance(
command,
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,
state.topology,
state.instances,
state.node_memory,
state.node_network,
state.node_backends,
download_status=state.downloads,
node_rdma_ctl=state.node_rdma_ctl,
)
transition_events = get_transition_events(
self.state.instances, placement, self.state.tasks
@@ -428,29 +442,6 @@ 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
@@ -471,7 +462,9 @@ class Master:
async def _plan(self) -> None:
while True:
# kill broken instances
connected_node_ids = set(self.state.topology.list_nodes())
connected_node_ids = set(
self.state.with_aggregator(self.aggregator).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:
@@ -481,7 +474,9 @@ class Master:
break
# time out dead nodes
for node_id, time in self.state.last_seen.items():
for node_id, time in self.state.with_aggregator(
self.aggregator
).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,6 +47,25 @@ 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()
@@ -94,6 +113,8 @@ 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:
+46 -124
View File
@@ -4,19 +4,14 @@ from datetime import datetime
from loguru import logger
from exo.shared.models.model_cards import ModelCard
from exo.shared.types.common import ModelId, NodeId
from exo.shared.types.common import NodeId
from exo.shared.types.events import (
ChunkGenerated,
CustomModelCardAdded,
CustomModelCardDeleted,
Event,
IndexedEvent,
InputChunkReceived,
InstanceCreated,
InstanceDeleted,
InstanceLinkCreated,
InstanceLinkDeleted,
NodeDownloadProgress,
NodeGatheredInfo,
NodeTimedOut,
@@ -32,7 +27,6 @@ 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,
@@ -42,7 +36,6 @@ 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 (
@@ -67,18 +60,6 @@ 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:
@@ -91,10 +72,6 @@ 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():
@@ -119,10 +96,6 @@ 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:
@@ -222,38 +195,7 @@ 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
}
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})
return state.model_copy(update={"instances": new_instances})
def apply_runner_status_updated(event: RunnerStatusUpdated, state: State) -> State:
@@ -408,59 +350,26 @@ def apply_node_gathered_info(event: NodeGatheredInfo, state: State) -> State:
event.node_id: NodeThunderboltInfo(interfaces=info.idents),
}
case MacThunderboltConnections():
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
update["node_thunderbolt_connections"] = {
**state.node_thunderbolt_connections,
event.node_id: info,
}
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
# 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
)
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,
@@ -471,32 +380,45 @@ def apply_node_gathered_info(event: NodeGatheredInfo, state: State) -> State:
def apply_topology_edge_created(event: TopologyEdgeCreated, state: State) -> State:
topology = copy.deepcopy(state.topology)
topology.add_connection(event.conn)
return state.model_copy(update={"topology": topology})
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)
def apply_topology_edge_deleted(event: TopologyEdgeDeleted, state: State) -> State:
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,
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)
]
)
}
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
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})
return state.model_copy(update=update)
+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 pop(self, model_id: ModelId) -> "ModelCard | None":
async def delete(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:
@@ -1,46 +0,0 @@
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 == {}
@@ -1,72 +0,0 @@
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))
@@ -1,35 +0,0 @@
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,10 +1,8 @@
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 (
@@ -20,15 +18,6 @@ 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(
@@ -36,28 +25,6 @@ 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,7 +7,6 @@ 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
@@ -82,24 +81,6 @@ 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
@@ -115,10 +96,6 @@ Command = (
| TaskCancelled
| TaskFinished
| SendInputChunk
| AddCustomModelCard
| DeleteCustomModelCard
| SetInstanceLink
| DeleteInstanceLink
)
+1 -23
View File
@@ -3,11 +3,9 @@ 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, ModelId, NodeId, SessionId, SystemId
from exo.shared.types.instance_link import InstanceLink, InstanceLinkId
from exo.shared.types.common import CommandId, Id, NodeId, SessionId, SystemId
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
@@ -108,14 +106,6 @@ class TopologyEdgeDeleted(BaseEvent):
conn: Connection
class CustomModelCardAdded(BaseEvent):
model_card: ModelCard
class CustomModelCardDeleted(BaseEvent):
model_id: ModelId
@final
class TraceEventData(FrozenModel):
name: str
@@ -138,14 +128,6 @@ class TracesMerged(BaseEvent):
traces: list[TraceEventData]
class InstanceLinkCreated(BaseEvent):
link: InstanceLink
class InstanceLinkDeleted(BaseEvent):
link_id: InstanceLinkId
Event = (
TestEvent
| TaskCreated
@@ -165,10 +147,6 @@ Event = (
| TopologyEdgeDeleted
| TracesCollected
| TracesMerged
| CustomModelCardAdded
| CustomModelCardDeleted
| InstanceLinkCreated
| InstanceLinkDeleted
)
+4
View File
@@ -16,6 +16,10 @@ 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
+91 -27
View File
@@ -1,15 +1,15 @@
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any, cast
from typing import Any
from pydantic import ConfigDict, Field, field_serializer, field_validator
from exo_rs import LVAggregator
from pydantic import ConfigDict, Field, model_serializer
from pydantic.alias_generators import to_camel
from pydantic_core.core_schema import SerializerFunctionWrapHandler
from exo.shared.models.model_cards import ModelCard
from exo.shared.topology import Topology, TopologySnapshot
from exo.shared.topology import Topology
from exo.shared.types.backends import Backend
from exo.shared.types.common import ModelId, NodeId
from exo.shared.types.instance_link import InstanceLink, InstanceLinkId
from exo.shared.types.common import NodeId
from exo.shared.types.profiling import (
DiskUsage,
MemoryUsage,
@@ -21,9 +21,15 @@ 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
@@ -39,7 +45,6 @@ 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,
)
@@ -48,7 +53,6 @@ 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)
@@ -61,34 +65,94 @@ 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] = {}
# User-added model cards. Workers can reconcile their on-disk custom card cache
custom_model_cards: Mapping[ModelId, ModelCard] = {}
@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]
@field_serializer("topology", mode="plain")
def _encode_topology(self, value: Topology) -> TopologySnapshot:
return value.to_snapshot()
@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_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, 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)
)
Accepts either an already constructed :class:`Topology` or a mapping
representing :class:`~shared.topology.TopologySnapshot`.
"""
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],
),
)
)
if isinstance(value, Topology):
return value
return topology
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)
def with_aggregator(self, aggregator: LVAggregator) -> "State":
from datetime import datetime, timezone
raise TypeError("Invalid representation for Topology field in State")
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
+45 -37
View File
@@ -10,11 +10,13 @@ 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,
@@ -27,7 +29,6 @@ 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
@@ -401,10 +402,42 @@ GatheredInfo = (
@dataclass
class InfoGatherer:
info_sender: Sender[GatheredInfo]
session_handle: SessionHandle
node_id: NodeId
info_senders: dict[str, LVPublisher] = field(init=False, default_factory=dict)
_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):
@@ -441,34 +474,11 @@ 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.info_sender.send(await StaticNodeInformation.gather())
await self.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)
@@ -477,7 +487,7 @@ class InfoGatherer:
while True:
try:
with fail_after(10):
await self.info_sender.send(await MiscData.gather())
await self.send(await MiscData.gather())
except Exception as e:
logger.opt(exception=e).warning("Error gathering misc data")
await anyio.sleep(misc_poll_interval)
@@ -498,12 +508,10 @@ class InfoGatherer:
idents = [
it for i in data if (it := i.ident(iface_map)) is not None
]
await self.info_sender.send(
MacThunderboltIdentifiers(idents=idents)
)
await self.send(MacThunderboltIdentifiers(idents=idents))
conns = [it for i in data if (it := i.conn()) is not None]
await self.info_sender.send(MacThunderboltConnections(conns=conns))
await self.send(MacThunderboltConnections(conns=conns))
except Exception as e:
logger.opt(exception=e).warning("Error gathering Thunderbolt data")
await anyio.sleep(system_profiler_interval)
@@ -520,7 +528,7 @@ class InfoGatherer:
)
while True:
try:
await self.info_sender.send(
await self.send(
MemoryUsage.from_psutil(override_memory=override_memory)
)
except Exception as e:
@@ -532,7 +540,7 @@ class InfoGatherer:
try:
with fail_after(10):
nics = await get_network_interfaces()
await self.info_sender.send(NodeNetworkInterfaces(ifaces=nics))
await self.send(NodeNetworkInterfaces(ifaces=nics))
except Exception as e:
logger.opt(exception=e).warning("Error gathering network interfaces")
await anyio.sleep(interface_watcher_interval)
@@ -545,7 +553,7 @@ class InfoGatherer:
with fail_after(30):
curr = await ThunderboltBridgeInfo.gather()
if curr is not None:
await self.info_sender.send(curr)
await self.send(curr)
except Exception as e:
logger.opt(exception=e).warning(
"Error gathering Thunderbolt Bridge status"
@@ -557,7 +565,7 @@ class InfoGatherer:
try:
curr = await RdmaCtlStatus.gather()
if curr is not None:
await self.info_sender.send(curr)
await self.send(curr)
except Exception as e:
logger.opt(exception=e).warning("Error gathering RDMA ctl status")
await anyio.sleep(rdma_ctl_poll_interval)
@@ -566,7 +574,7 @@ class InfoGatherer:
while True:
try:
with fail_after(5):
await self.info_sender.send(await NodeDiskUsage.gather())
await self.send(await NodeDiskUsage.gather())
except Exception as e:
logger.opt(exception=e).warning("Error gathering disk usage")
await anyio.sleep(disk_poll_interval)
@@ -611,7 +619,7 @@ class InfoGatherer:
)
text = data.decode("utf-8", errors="replace").strip()
metrics = MacmonMetrics.from_raw_json(text)
await self.info_sender.send(metrics)
await self.send(metrics)
except TimeoutError:
logger.warning(
f"MacMon produced no output for {read_timeout}s, restarting"
+8 -4
View File
@@ -19,18 +19,22 @@ 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.__class__.__name__: inner}
return {self.tag(): 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.__name__ in v: # pyright: ignore[reportUnknownArgumentType]
return handler(v[cls.__name__]) # 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]
return handler(v) # pyright: ignore[reportAny]
def __str__(self) -> str:
return f"{self.__class__.__name__}({super().__str__()})"
return f"{self.tag()}({super().__str__()})"
+28 -18
View File
@@ -4,7 +4,9 @@ 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
@@ -14,7 +16,8 @@ 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.model_cards import ModelId, card_cache
from exo.shared.models import model_cards
from exo.shared.models.model_cards import ModelCard, ModelId
from exo.shared.types.chunks import InputImageChunk
from exo.shared.types.commands import (
DeleteInstance,
@@ -53,7 +56,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, channel
from exo.utils.channels import Receiver, Sender
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
@@ -73,6 +76,7 @@ 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
@@ -98,17 +102,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_send, info_recv = channel[GatheredInfo]()
info_gatherer: InfoGatherer = InfoGatherer(info_send)
info_gatherer: InfoGatherer = InfoGatherer(self._sh, self.node_id)
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)
@@ -178,17 +182,24 @@ class Worker:
] = img
async def _reconcile_custom_cards(self) -> None:
storage = self._sh.storage_interface()
while True:
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:
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:
continue
await card_cache.save(card)
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)
for card in await card_cache.list_all():
for card in await model_cards.card_cache.list_all():
if card.model_id not in target:
await card_cache.pop(card.model_id)
await model_cards.card_cache.delete(card.model_id)
async def plan_step(self):
while True:
@@ -388,14 +399,13 @@ class Worker:
async def _poll_connection_updates(self):
while True:
edges = set(
conn.edge for conn in self.state.topology.out_edges(self.node_id)
)
state = self.state.with_aggregator(self.aggregator)
edges = set(conn.edge for conn in state.topology.out_edges(self.node_id))
conns: defaultdict[NodeId, set[str]] = defaultdict(set)
async for ip, nid in check_reachable(
self.state.topology,
state.topology,
self.node_id,
self.state.node_network,
state.node_network,
api_port=self.api_port,
):
if ip in conns[nid]:
@@ -416,7 +426,7 @@ class Worker:
)
)
for conn in self.state.topology.out_edges(self.node_id):
for conn in 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" \
"EXO_ZENOH_NAMESPACE=$commit /nix/var/nix/profiles/default/bin/nix run $remote_installable" 2>&1 |
"ENABLE_DISAGGREGATION=true 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.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.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", 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.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.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", 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.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.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", 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.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.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", 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.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.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", 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.dev20260506+cc3f3e60"
version = "0.32.0.dev20260512+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.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.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 = "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.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.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", 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')" },