Compare commits

..
Author SHA1 Message Date
Andrei Cravtov 11f9d000e5 Merge branch 'main' into andrei/force_oom 2026-05-15 13:53:11 +01:00
Andrei Cravtov 551e94ed38 fin 2026-05-15 13:52:53 +01:00
Andrei Cravtov b0834c8a2e ff 2026-05-15 13:46:26 +01:00
Andrei Cravtov d545a4ea25 fff 2026-05-15 13:42:02 +01:00
Andrei Cravtov 0e7721ba0e ffff 2026-05-15 13:28:08 +01:00
Andrei Cravtov abb4402036 fff 2026-05-15 13:26:13 +01:00
Andrei Cravtov e8b9c808a6 ff 2026-05-15 13:23:11 +01:00
Andrei Cravtov cf00cd83ed fff 2026-05-15 13:20:13 +01:00
Andrei Cravtov 41ff366b1d fff 2026-05-15 13:01:55 +01:00
Andrei Cravtov 303600616a ff 2026-05-15 12:45:12 +01:00
Andrei Cravtov 51d9912fcf ff 2026-05-15 12:37:04 +01:00
Andrei Cravtov d5da78b39b ff 2026-05-15 12:32:16 +01:00
Andrei Cravtov 88027475a5 ff 2026-05-15 12:30:50 +01:00
Andrei Cravtov 423dc407f8 ff 2026-05-15 12:24:42 +01:00
Andrei Cravtov f62b3b09d0 ff 2026-05-15 11:58:47 +01:00
Andrei Cravtov 94424d92c3 ff 2026-05-15 11:56:39 +01:00
Andrei Cravtov e029a1ea65 print 2026-05-15 11:51:13 +01:00
Andrei Cravtov 49e5350b17 ff 2026-05-15 11:47:56 +01:00
Andrei Cravtov feaf05f82d ooom 2 2026-05-15 11:43:08 +01:00
Andrei Cravtov 280031d431 Use MemoryUsage for MLX OOM fallback 2026-05-15 11:05:54 +01:00
Andrei Cravtov 58551f2c32 nix-direnv 2026-05-15 10:57:00 +01:00
Andrei Cravtov cecc37085a Move one-shot macmon sampling helper 2026-05-14 20:25:34 +01:00
Andrei Cravtov a73be3c2bb Scope MLX OOM ceil division helper 2026-05-14 20:13:28 +01:00
Andrei Cravtov acec94316d Inline one-off MLX OOM helpers 2026-05-14 20:05:33 +01:00
Andrei Cravtov 5c7068387d yes 2026-05-14 19:57:53 +01:00
Andrei Cravtov d62b333405 Fix MLX utils type ignores 2026-05-14 19:51:31 +01:00
Andrei Cravtov eeedc6c17a Add adaptive MLX force OOM helper 2026-05-14 19:49:40 +01:00
Andrei Cravtov 2d11b6520f yes 2026-05-14 19:45:07 +01:00
Andrei Cravtov 42b26af63b yes 2026-05-14 17:56:56 +01:00
Andrei Cravtov 1649708ccb yes 2026-05-14 17:55:51 +01:00
Andrei Cravtov fbe962b08e Merge branch 'main' into andrei/error-handling 2026-05-14 17:44:53 +01:00
Andrei Cravtov 38258fb5f3 Merge branch 'main' into andrei/error-handling
# Conflicts:
#	uv.lock
2026-05-14 17:33:33 +01:00
Andrei Cravtov 86398a2bd1 ffff 2026-05-14 17:31:47 +01:00
Andrei Cravtov bab245759d fff 2026-05-14 17:26:24 +01:00
Andrei Cravtov 2f506167e0 fff 2026-05-14 17:24:20 +01:00
Andrei Cravtov 88d4c37bfa fff 2026-05-14 17:12:41 +01:00
Andrei Cravtov 9ec09ec59d ff 2026-05-14 17:07:46 +01:00
Andrei Cravtov cd22516186 ff 2026-05-14 16:54:37 +01:00
Andrei Cravtov ddc6e95e68 fff 2026-05-14 16:51:20 +01:00
Andrei Cravtov feef63b98c fff 2026-05-14 16:50:39 +01:00
Andrei Cravtov 3611c33b61 ff 2026-05-14 16:06:11 +01:00
Andrei Cravtov 547b58bf48 ff 2026-05-14 15:51:02 +01:00
Andrei Cravtov 36b1b82b48 ff 2026-05-14 15:49:32 +01:00
Andrei Cravtov b1d09b026d ff 2026-05-14 15:29:40 +01:00
Andrei Cravtov faafc6f4ad yes 2026-05-14 15:17:34 +01:00
Andrei Cravtov 2969b7d4c6 yes 2026-05-14 15:16:48 +01:00
Andrei Cravtov 6c13b4550f yes 2026-05-14 15:11:17 +01:00
Andrei Cravtov 3fc56b2da5 yes 2026-05-14 15:08:58 +01:00
Andrei Cravtov 9c4cc084db yes 2026-05-14 12:36:14 +01:00
Andrei Cravtov 49d7555ea6 yes 2026-05-14 12:31:49 +01:00
Andrei Cravtov 32155bdf00 yes 2026-05-14 12:31:10 +01:00
Andrei Cravtov 163ef39bbb yes 2026-05-14 12:29:34 +01:00
Andrei Cravtov 9636e86855 yes 2026-05-14 12:25:10 +01:00
Andrei Cravtov 4ea2532dd6 yes 2026-05-14 12:14:09 +01:00
Andrei Cravtov 1d17f4de18 yes 2026-05-14 12:09:34 +01:00
Andrei Cravtov 1c7f514ca3 yes 2026-05-14 12:04:43 +01:00
Andrei Cravtov c3aa1f47fc yes 2026-05-14 11:56:00 +01:00
Andrei Cravtov 9686c02251 yes 2026-05-14 11:50:21 +01:00
Andrei Cravtov 2f9ba7d1de yes 2026-05-14 11:14:39 +01:00
Andrei Cravtov ab172322c5 Merge branch 'main' into andrei/error-handling 2026-05-12 11:49:17 +01:00
Andrei Cravtov 278bea11ad fmt 2026-05-12 11:40:07 +01:00
Andrei Cravtov dbd5ca00eb fmt 2026-05-11 20:04:58 +01:00
Andrei Cravtov 197cbf4f9d fmt 2026-05-11 19:44:38 +01:00
Andrei Cravtov d58ab113d1 fmt 2026-05-11 19:32:47 +01:00
Andrei Cravtov 9c7908f247 integrated it 2026-05-11 19:32:25 +01:00
Andrei Cravtov ad2c1e7c66 initial changes 2026-05-11 19:19:47 +01:00
60 changed files with 1338 additions and 2072 deletions

No files matched your search

View File
Whitespace-only changes.
+12
View File
@@ -1 +1,13 @@
# installs nix-direnv if doesn't exist (speeds up evaluation)
if ! has nix_direnv_version || ! nix_direnv_version 3.1.1; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.1/direnvrc" "sha256-p+fzQdrms/hDa7g+soShAybJNo4bN4SIAeSfqNKgD5I="
fi
use flake
# creates .venv if doesn't exist and loads its environment
export VIRTUAL_ENV=".venv"
if ! [ -d "./$VIRTUAL_ENV" ]; then
uv venv
fi
layout python
-2
View File
@@ -38,8 +38,6 @@ bench/**/*.json
# tmp
tmp/models
/build/exo
/.agents
/.claude/skills
/.claude
/.codex
skills-lock.json
Generated
+712 -810
View File
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = ["rust/networking", "rust/exo_rs", "rust/util"]
members = ["rust/networking", "rust/exo_pyo3_bindings", "rust/util"]
[workspace.package]
version = "0.0.1"
@@ -28,7 +28,6 @@ delegate = "0.13"
# Utility dependencies
keccak-const = "0.2"
nix = "0.31"
# Async dependencies
async-stream = "0.3"
-18
View File
@@ -201,12 +201,6 @@ This starts the exo dashboard and API at http://localhost:52415/
uv run exo --no-worker
```
- `--legacy-daemon`: Run exo as a legacy SysV-style background daemon using double-fork daemonization. This is intended for legacy init scripts; systemd and launchd should run exo in the foreground without this flag.
```bash
uv run exo --legacy-daemon
```
**File Locations (Linux):**
exo follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) on Linux:
@@ -401,18 +395,6 @@ Sample response:
}
```
This command is asynchronous. Before sending inference requests, wait until the
API sees the new instance for this model:
```bash
curl -N "http://localhost:52415/instance/await?model_id=mlx-community/Llama-3.2-1B-Instruct-4bit"
```
The endpoint returns an SSE stream. A successful wait emits a message with
`"type": "ready"` and the matching instance; a timeout emits `"type": "timeout"`.
By default it waits indefinitely. Set `timeout_seconds` to a positive value to
bound the wait.
---
**3. Send a chat completion**
@@ -37,7 +37,7 @@ final class ClusterStateService: ObservableObject {
/// gain nothing from being cached on disk. Use an ephemeral session
/// with `urlCache = nil` so neither response bodies nor metadata
/// touch disk.
nonisolated private static func makeNonCachingSession() -> URLSession {
private static func makeNonCachingSession() -> URLSession {
let config = URLSessionConfiguration.ephemeral
config.urlCache = nil
config.requestCachePolicy = .reloadIgnoringLocalCacheData
+1 -2
View File
@@ -125,7 +125,7 @@ A background thread polls each node at 1 Hz, collecting:
- System power draw (W)
- CPU cluster usage (performance and efficiency cores)
**Energy** is computed via trapezoidal integration of the power samples over each inference window (the wall-clock span of each benchmark request or concurrent batch). Average power is `total_joules / total_inference_seconds`. The server additionally returns a `power_usage` block in each non-stream `/bench/chat/completions` response that splits energy into prefill and generation phases, with the boundary anchored to the first non-`PrefillProgressChunk` from the runner.
**Energy** is computed via trapezoidal integration of the power samples over each inference window (the wall-clock span of each benchmark request or concurrent batch). Average power is `total_joules / total_inference_seconds`.
---
@@ -136,7 +136,6 @@ Results are written as JSON with three top-level keys:
- **`runs`**: Array of per-request result objects, each containing:
- `elapsed_s`, `output_text_preview` (first 200 chars)
- `stats`: `{ prompt_tps, generation_tps, prompt_tokens, generation_tokens, peak_memory_usage }`
- `power_usage`: server-side total + prefill/generation split, per-node breakdown (non-stream requests only)
- Placement metadata: `model_id`, `placement_sharding`, `placement_instance_meta`, `placement_nodes`
- Run metadata: `pp_tokens`, `tg`, `repeat_index`, `concurrency`, `concurrent_index`
- `download_duration_s` (if model was freshly downloaded)
-26
View File
@@ -295,7 +295,6 @@ def run_one_completion(
elapsed = time.perf_counter() - t0
stats = out.get("generation_stats")
power_usage = out.get("power_usage")
choices = out.get("choices") or [{}]
message = choices[0].get("message", {}) if choices else {}
content = message.get("content") or ""
@@ -331,7 +330,6 @@ def run_one_completion(
elapsed = time.perf_counter() - t0
preview = "".join(text_parts)[:200]
power_usage = None
if not stats:
ttft = (first_token_time - t0) if first_token_time else elapsed
@@ -350,7 +348,6 @@ def run_one_completion(
"elapsed_s": elapsed,
"output_text_preview": preview,
"stats": stats,
"power_usage": power_usage,
}, pp_tokens
@@ -767,7 +764,6 @@ def main() -> int:
out = c.post_bench_chat_completions(_payload)
elapsed = time.perf_counter() - t0
stats = out.get("generation_stats")
power_usage = out.get("power_usage")
choices = out.get("choices") or [{}]
message = (
choices[0].get("message", {}) if choices else {}
@@ -777,7 +773,6 @@ def main() -> int:
"elapsed_s": elapsed,
"output_text_preview": text[:200],
"stats": stats,
"power_usage": power_usage,
}, _actual_pp
inf_t0 = time.monotonic()
@@ -873,27 +868,6 @@ def main() -> int:
inf_seconds = sum(t1 - t0 for t0, t1 in inference_windows)
avg_watts = joules / inf_seconds if inf_seconds > 0 else 0
summary += f" energy={joules:.1f}J ({avg_watts:.1f}W avg over {inf_seconds:.1f}s inference)"
# mean() not sum() across concurrent runs: each
# request's PowerSampler observes the same shared
# cluster state, so they all report the same figure.
prefill_energies = [
(x.get("power_usage") or {}).get("prefill_energy_joules")
for x in runs
]
gen_energies = [
(x.get("power_usage") or {}).get("generation_energy_joules")
for x in runs
]
prefill_vals = [e for e in prefill_energies if e is not None]
gen_vals = [e for e in gen_energies if e is not None]
if prefill_vals and gen_vals:
avg_pref = mean(prefill_vals)
avg_gen = mean(gen_vals)
summary += (
f" prefill_energy={avg_pref:.1f}J "
f"gen_energy={avg_gen:.1f}J"
)
logger.info(f"{summary}\n")
time.sleep(2)
finally:
+5 -11
View File
@@ -2253,9 +2253,10 @@ class AppStore {
* @returns The model ID to use, or null if none available
*/
private getModelForRequest(modelId?: string): string | null {
const requestedModelId = modelId || this.selectedChatModel;
if (modelId) return modelId;
if (this.selectedChatModel) return this.selectedChatModel;
// Only models with a placed instance can receive requests; disk downloads alone are not enough.
// Try to get model from first running instance
for (const [, instanceWrapper] of Object.entries(this.instances)) {
if (instanceWrapper && typeof instanceWrapper === "object") {
const keys = Object.keys(instanceWrapper as Record<string, unknown>);
@@ -2263,15 +2264,8 @@ class AppStore {
const instance = (instanceWrapper as Record<string, unknown>)[
keys[0]
] as { shardAssignments?: { modelId?: string } };
const instanceModelId = instance?.shardAssignments?.modelId;
// ensure to only return requestedModelId that matches an instance
// or fall back to first instance
if (
instanceModelId &&
(!requestedModelId || requestedModelId === instanceModelId)
) {
return instanceModelId;
if (instance?.shardAssignments?.modelId) {
return instance.shardAssignments.modelId;
}
}
}
+8 -18
View File
@@ -1461,9 +1461,6 @@
addToast({ type: "info", message: `Launching model...` });
// Always auto-select the newly launched model so the user chats to what they just launched
setSelectedChatModel(modelId);
userForcedIdle = false;
pendingChatModelId = modelId;
chatLaunchState = "launching";
// Record the launch in recent models history
recordRecentLaunch(modelId);
@@ -2550,10 +2547,12 @@
];
// ── Seamless chat: launch models from chat view ──
type InFlightChatLaunchState = "launching" | "downloading" | "loading";
type ReadyLikeChatLaunchState = "idle" | "ready";
type ChatLaunchState = InFlightChatLaunchState | ReadyLikeChatLaunchState;
type ChatLaunchState =
| "idle"
| "launching"
| "downloading"
| "loading"
| "ready";
let chatLaunchState = $state<ChatLaunchState>("idle");
let pendingChatModelId = $state<string | null>(null);
let selectedChatCategory = $state<string | null>(null);
@@ -3130,15 +3129,6 @@
if (model) {
pendingAutoMessage = { content, files };
userForcedIdle = false;
// The selected model is already being placed or loaded; keep the queued
// message and let the existing launch state effects send it once ready.
if (
pendingChatModelId === model &&
chatLaunchState !== "idle" &&
chatLaunchState !== "ready"
) {
return;
}
launchModelForChat(model, "picker", messages().length > 0);
return;
}
@@ -4613,7 +4603,7 @@
type="button"
onclick={() => {
completeOnboarding();
handleChatSend(chip);
sendMessage(chip, undefined, thinkingEnabled());
}}
class="px-4 py-2 rounded-full border border-white/10 bg-white/5 text-sm text-white/60 hover:bg-white/10 hover:text-white/80 hover:border-white/20 transition-all duration-200 cursor-pointer"
>
@@ -6110,7 +6100,7 @@
onclick={() => {
chatLaunchState = "idle";
selectedChatCategory = null;
handleChatSend(prompt);
sendMessage(prompt, undefined, thinkingEnabled());
}}
class="text-left px-3 py-2.5 text-xs text-exo-light-gray hover:text-white font-mono rounded-lg border border-exo-medium-gray/30 hover:border-exo-yellow/30 bg-exo-dark-gray/30 hover:bg-exo-dark-gray/60 transition-all duration-200 cursor-pointer"
>
+6 -35
View File
@@ -66,9 +66,7 @@ Creates a new model instance in the cluster.
```
**Response:**
Command acknowledgement. Instance creation is asynchronous; clients should wait
for the model to appear through `/instance/await` before sending inference
requests for that model.
JSON description of the created instance.
### Delete Instance
@@ -96,31 +94,6 @@ Returns details of a specific instance.
**Response:**
JSON description of the instance.
### Await Instance
**GET** `/instance/await?model_id=...&timeout_seconds=0`
Waits until API state contains an instance for the requested model. The response
is an SSE stream so clients receive keep-alive comments while waiting.
**Query parameters:**
* `model_id`: string, required
* `timeout_seconds`: float, optional, default `0`. `0` waits indefinitely;
positive values time out after that many seconds. Maximum positive value:
`300`.
**Stream messages:**
```text
data: {"type": "ready", "instance": {...}}
data: {"type": "timeout", "message": "No instance found for model ..."}
```
The HTTP status is `200` for both messages because the stream starts before the
final result is known. The `type` field disambiguates the terminal message.
### Preview Placements
**GET** `/instance/previews?model_id=...`
@@ -150,18 +123,17 @@ Computes a placement for a potential instance without creating it.
**Response:**
JSON object describing the proposed placement / instance configuration.
### Place Instance
### Place Instance (Dry Operation)
**POST** `/place_instance`
Places an instance for a model using the server's placement logic.
Performs a placement operation for an instance (planning step), without necessarily creating it.
**Request body:**
JSON describing the instance to be placed.
**Response:**
Command acknowledgement. The instance may not be ready immediately; wait for it
to appear through `/instance/await` before sending inference requests.
Placement result.
## 3. Models
@@ -667,11 +639,10 @@ GET /events
# Instance Management
POST /instance
GET /instance/await
GET /instance/previews
GET /instance/placement
GET /instance/{instance_id}
DELETE /instance/{instance_id}
GET /instance/previews
GET /instance/placement
POST /place_instance
# Models
+1 -1
View File
@@ -110,7 +110,7 @@
nixpkgs-fmt.enable = true;
ruff-format = {
enable = true;
excludes = [ "rust/exo_rs/exo_rs.pyi" ];
excludes = [ "rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi" ];
};
rustfmt = {
enable = true;
+2 -2
View File
@@ -23,7 +23,7 @@ sync-clean:
rust-rebuild:
PYO3_PYTHON="$(uv run python -c 'import sys; print(sys.executable)')" cargo run --bin stub_gen
uv sync --reinstall-package exo_rs
uv sync --reinstall-package exo_pyo3_bindings
build-dashboard:
#!/usr/bin/env bash
@@ -37,7 +37,7 @@ package: build-dashboard
rm -rf build
build-app: rust-rebuild sync-clean package
env -u LD xcodebuild build -project app/EXO/EXO.xcodeproj -scheme EXO -configuration Debug -derivedDataPath app/EXO/build
xcodebuild build -project app/EXO/EXO.xcodeproj -scheme EXO -configuration Debug -derivedDataPath app/EXO/build
@echo "\nBuild complete. Run with:\n open {{justfile_directory()}}/app/EXO/build/Build/Products/Debug/EXO.app"
clean:
+5 -5
View File
@@ -15,7 +15,7 @@ dependencies = [
"huggingface-hub>=1.8.0",
"psutil>=7.0.0",
"loguru>=0.7.3",
"exo-rs", # rust bindings
"exo-pyo3-bindings", # rust bindings
"anyio==4.11.0",
"tiktoken>=0.12.0", # required for kimi k2 tokenizer
"hypercorn>=0.18.0",
@@ -26,7 +26,6 @@ dependencies = [
"msgspec>=0.19.0",
"zstandard>=0.23.0",
"transformers>=5.6.2",
"python-daemon>=3.1.2",
]
[project.scripts]
@@ -76,14 +75,15 @@ mlx-cuda13 = [
###
[tool.uv.workspace]
members = ["rust/exo_rs", "bench", "tools"]
members = ["rust/exo_pyo3_bindings", "bench", "tools"]
[tool.uv.sources]
exo-rs = { workspace = true }
exo-pyo3-bindings = { workspace = true }
mlx = [
{ git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git", branch = "address-rdma-gpu-locks", marker = "sys_platform == 'darwin'" },
{ 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 = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
{ 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 = "sys_platform == 'linux' and platform_machine != 'aarch64'" },
]
mlx-lm = { git = "https://github.com/rltakashige/mlx-lm", branch = "leo/deepseek-v4" }
mflux = { git = "https://github.com/evanev7/mflux", branch = "exo2" }
@@ -240,7 +240,7 @@ torchaudio = ["torch"]
###
[tool.ruff]
extend-exclude = [".typings/**", "rust/exo_rs/**", "bench/vendor/**"]
extend-exclude = [".typings/**", "rust/exo_pyo3_bindings/**", "bench/vendor/**"]
[tool.ruff.lint]
extend-select = ["I", "N", "B", "A", "PIE", "SIM"]
+8 -9
View File
@@ -44,21 +44,20 @@ let
paths = builtins.concatMap (p: [ (lib.getBin p) (lib.getLib p) (lib.getDev p) ]) (cudaLibs ++ [ cudaPackages.cuda_nvcc cuda_cccl_compat ]);
};
exoOverlay = final: prev: {
# Replace workspace exo_rs with Nix-built wheel.
# Replace workspace exo_pyo3_bindings with Nix-built wheel.
# Preserve passthru so mkVirtualEnv can resolve dependency groups.
# Copy .pyi stub + py.typed marker so basedpyright can find the types.
exo-rs = pkgs.stdenv.mkDerivation {
pname = "exo-rs";
exo-pyo3-bindings = pkgs.stdenv.mkDerivation {
pname = "exo-pyo3-bindings";
version = "0.1.0";
src = self'.packages.exo-rs;
src = self'.packages.exo_pyo3_bindings;
# Install from pre-built wheel
nativeBuildInputs = [ final.pyprojectWheelHook ];
dontStrip = true;
passthru = prev.exo-rs.passthru or { };
passthru = prev.exo-pyo3-bindings.passthru or { };
postInstall = ''
local siteDir=$out/${final.python.sitePackages}/exo_rs
cp ${inputs.self}/rust/exo_rs/exo_rs.pyi $siteDir/
local siteDir=$out/${final.python.sitePackages}/exo_pyo3_bindings
cp ${inputs.self}/rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi $siteDir/
touch $siteDir/py.typed
'';
};
@@ -224,7 +223,7 @@ let
++ lib.optionals isDarwin [ pkgs.macmon ];
passthru = {
venv = venv name;
evenv = ((pythonSet.overrideScope editableOverlay).mkVirtualEnv "${name}-evenv" (members // { exo = (members.exo or [ ]) ++ [ "dev" ]; exo-rs = [ ]; })).overrideAttrs (_: {
evenv = ((pythonSet.overrideScope editableOverlay).mkVirtualEnv "${name}-evenv" (members // { exo = (members.exo or [ ]) ++ [ "dev" ]; })).overrideAttrs (_: {
venvSkip = [ "lib/python${python.pythonVersion}/site-packages/mlx/share/cmake/*" "lib/python${python.pythonVersion}/site-packages/build_backend.py" ];
});
} // lib.optionalAttrs cudaSupport {
@@ -1,5 +1,5 @@
[package]
name = "exo_rs"
name = "exo_pyo3_bindings"
version = { workspace = true }
edition = { workspace = true }
publish = false
@@ -7,7 +7,7 @@ publish = false
[lib]
doctest = false
path = "src/lib.rs"
name = "exo_rs"
name = "exo_pyo3_bindings"
# "cdylib" needed to produce shared library for Python to import
# "rlib" needed for stub-gen to run
@@ -25,7 +25,7 @@ workspace = true
networking = { workspace = true }
# interop
pyo3 = { version = "0.28.3", features = [
pyo3 = { version = "0.27.2", features = [
# "abi3-py313", # tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.13
# "nightly", # enables better-supported GIL integration
"experimental-async", # async support in #[pyfunction] & #[pymethods]
@@ -38,15 +38,15 @@ pyo3 = { version = "0.28.3", features = [
# "ordered-float", "rust_decimal", "smallvec",
# "anyhow", "chrono", "chrono-local", "chrono-tz", "eyre", "jiff-02", "lock_api", "parking-lot", "time", "serde",
] }
pyo3-stub-gen = { version = "0.22.3" }
pyo3-async-runtimes = { version = "0.28.0", features = [
pyo3-stub-gen = { version = "0.17.2" }
pyo3-async-runtimes = { version = "0.27.0", features = [
"attributes",
"tokio-runtime",
"testing",
] }
pyo3-log = "0.13.3"
pyo3-log = "0.13.2"
pidfile-rs = { git = "https://github.com/AndreiCravtov/pidfile-rs" }
pidfile-rs = "0.3"
# macro dependencies
extend = { workspace = true }
File renamed without changes.
@@ -1,20 +1,10 @@
# This file is automatically generated by pyo3_stub_gen
# ruff: noqa: E501, F401, F403, F405
# ruff: noqa: E501, F401
import builtins
import os
import pathlib
import typing
__all__ = [
"AllQueuesFullError",
"FromSwarm",
"Keypair",
"MessageTooLargeError",
"NetworkingHandle",
"NoPeersSubscribedToTopicError",
"Pidfile",
"PidfileError",
]
@typing.final
class AllQueuesFullError(builtins.Exception):
@@ -22,29 +12,6 @@ class AllQueuesFullError(builtins.Exception):
def __repr__(self) -> builtins.str: ...
def __str__(self) -> builtins.str: ...
class FromSwarm:
@typing.final
class Connection(FromSwarm):
__match_args__ = ("peer_id", "connected",)
@property
def peer_id(self) -> builtins.str: ...
@property
def connected(self) -> builtins.bool: ...
def __new__(cls, peer_id: builtins.str, connected: builtins.bool) -> FromSwarm.Connection: ...
@typing.final
class Message(FromSwarm):
__match_args__ = ("origin", "topic", "data",)
@property
def origin(self) -> builtins.str: ...
@property
def topic(self) -> builtins.str: ...
@property
def data(self) -> bytes: ...
def __new__(cls, origin: builtins.str, topic: builtins.str, data: bytes) -> FromSwarm.Message: ...
...
@typing.final
class Keypair:
r"""
@@ -78,7 +45,6 @@ class MessageTooLargeError(builtins.Exception):
@typing.final
class NetworkingHandle:
def __new__(cls, identity: Keypair, bootstrap_peers: typing.Sequence[builtins.str], listen_port: builtins.int) -> NetworkingHandle: ...
def recv(self) -> typing.Awaitable[FromSwarm]: ...
async def gossipsub_subscribe(self, topic: builtins.str) -> builtins.bool:
r"""
Subscribe to a `GossipSub` topic.
@@ -97,6 +63,7 @@ class NetworkingHandle:
If no peers are found that subscribe to this topic, throws `NoPeersSubscribedToTopicError` exception.
"""
async def recv(self) -> PyFromSwarm: ...
@typing.final
class NoPeersSubscribedToTopicError(builtins.Exception):
@@ -110,7 +77,7 @@ class Pidfile:
A PID file protected with a lock.
An instance of `Pidfile` can be used to manage a PID file: create it,
lock it, detect already running daemons. It is backed by [`pidfile`]
lock it, detect already running daemons. It is backed by [`pidfile`][]
functions of `libbsd`/`libutil` which use `flopen` to lock the PID
file.
@@ -140,23 +107,32 @@ class Pidfile:
The file is truncated before writing.
"""
def as_raw_fd(self) -> builtins.int:
r"""
Extracts the raw file descriptor.
This function is typically used to **borrow** an owned file descriptor.
When used in this way, this method does **not** pass ownership of the
raw file descriptor to the caller, and the file descriptor is only
guaranteed to be valid while the original object has not yet been
destroyed.
"""
def close(self) -> None:
r"""
Closes the PID file and releases associated resources.
"""
@typing.final
class PidfileError(builtins.Exception):
def __repr__(self) -> builtins.str: ...
def __str__(self) -> builtins.str: ...
class PyFromSwarm:
@typing.final
class Connection(PyFromSwarm):
__match_args__ = ("peer_id", "connected",)
@property
def peer_id(self) -> builtins.str: ...
@property
def connected(self) -> builtins.bool: ...
def __new__(cls, peer_id: builtins.str, connected: builtins.bool) -> PyFromSwarm.Connection: ...
@typing.final
class Message(PyFromSwarm):
__match_args__ = ("origin", "topic", "data",)
@property
def origin(self) -> builtins.str: ...
@property
def topic(self) -> builtins.str: ...
@property
def data(self) -> bytes: ...
def __new__(cls, origin: builtins.str, topic: builtins.str, data: bytes) -> PyFromSwarm.Message: ...
...
@@ -3,8 +3,8 @@ requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "exo_rs"
version = "0.2.16"
name = "exo_pyo3_bindings"
version = "0.2.2"
description = "Add your description here"
readme = "README.md"
authors = [
@@ -15,17 +15,14 @@ requires-python = ">=3.13"
dependencies = []
[dependency-groups]
dev = ["exo_rs", "pytest>=8.4.0", "pytest-asyncio>=1.0.0"]
dev = ["exo_pyo3_bindings", "pytest>=8.4.0", "pytest-asyncio>=1.0.0"]
[tool.maturin]
#purelib = true
#python-source = "python"
module-name = "exo_rs"
module-name = "exo_pyo3_bindings"
features = ["pyo3/extension-module", "pyo3/experimental-async"]
[tool.pyo3-stub-gen]
generate-init-py = true
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "INFO"
@@ -2,7 +2,7 @@ use pyo3_stub_gen::Result;
fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", "info")).init();
let stub = exo_rs::stub_info()?;
let stub = exo_pyo3_bindings::stub_info()?;
stub.generate()?;
Ok(())
}
File renamed without changes.
@@ -153,7 +153,7 @@ pub(crate) mod ext {
/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule(name = "exo_rs", gil_used = true)]
#[pymodule(name = "exo_pyo3_bindings")]
fn main_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
// install logger
pyo3_log::init();
@@ -16,7 +16,9 @@ use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::{PyModule, PyModuleMethods as _};
use pyo3::types::PyBytes;
use pyo3::{Bound, Py, PyAny, PyErr, PyResult, Python, pymethods};
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyclass_complex_enum, gen_stub_pymethods};
use pyo3_stub_gen::derive::{
gen_methods_from_python, gen_stub_pyclass, gen_stub_pyclass_complex_enum, gen_stub_pymethods,
};
use tokio::sync::{Mutex, mpsc, oneshot};
mod exception {
@@ -136,7 +138,7 @@ struct PyNetworkingHandle {
}
#[gen_stub_pyclass_complex_enum]
#[pyclass(name = "FromSwarm")]
#[pyclass]
enum PyFromSwarm {
Connection {
peer_id: String,
@@ -202,11 +204,9 @@ impl PyNetworkingHandle {
})
}
#[gen_stub(override_return_type(
type_repr="typing.Awaitable[FromSwarm]", imports=("typing")
))]
#[gen_stub(skip)]
fn recv<'py>(&'py self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let swarm = self.swarm.clone();
let swarm = Arc::clone(&self.swarm);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
swarm
.try_lock()
@@ -297,6 +297,15 @@ impl PyNetworkingHandle {
}
}
pyo3_stub_gen::inventory::submit! {
gen_methods_from_python! {
r#"
class PyNetworkingHandle:
async def recv() -> PyFromSwarm: ...
"#
}
}
pub fn networking_submodule(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<exception::PyNoPeersSubscribedToTopicError>()?;
m.add_class::<exception::PyAllQueuesFullError>()?;
@@ -3,9 +3,7 @@ use pyo3::exceptions::PyException;
use pyo3::prelude::{PyModule, PyModuleMethods};
use pyo3::{Bound, PyErr, PyResult, Python, pyclass, pymethods};
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use std::fs;
use std::fs::Permissions;
use std::os::fd::{AsRawFd, RawFd};
use std::os::unix::prelude::PermissionsExt;
use std::path::PathBuf;
@@ -38,7 +36,7 @@ impl PyPidfileError {
/// A PID file protected with a lock.
///
/// An instance of `Pidfile` can be used to manage a PID file: create it,
/// lock it, detect already running daemons. It is backed by [`pidfile`]
/// lock it, detect already running daemons. It is backed by [`pidfile`][]
/// functions of `libbsd`/`libutil` which use `flopen` to lock the PID
/// file.
///
@@ -55,23 +53,7 @@ impl PyPidfileError {
/// [`daemon`(3)]: https://linux.die.net/man/3/daemon
#[gen_stub_pyclass]
#[pyclass(name = "Pidfile")]
pub struct PyPidfile(Option<Pidfile>);
impl PyPidfile {
#[inline(always)]
fn get(&self) -> &Pidfile {
self.0
.as_ref()
.expect("cannot use resource after exiting context")
}
#[inline(always)]
fn get_mut(&mut self) -> &mut Pidfile {
self.0
.as_mut()
.expect("cannot use resource after exiting context")
}
}
pub struct PyPidfile(Pidfile);
#[gen_stub_pymethods]
#[pymethods]
@@ -83,40 +65,17 @@ impl PyPidfile {
/// the PID file yet.
#[new]
fn py_new(py: Python, path: PathBuf, mode: u32) -> PyResult<Self> {
// create all parent directories if don't exist
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| PyPidfileError(PidfileError::Io(e)).into_pyerr(py))?;
}
let pidfile = Pidfile::new(&path, Permissions::from_mode(mode))
.map_err(|e| PyPidfileError(e).into_pyerr(py))?;
Ok(Self(Some(pidfile)))
Ok(Self(
Pidfile::new(&path, Permissions::from_mode(mode))
.map_err(|e| PyPidfileError(e).into_pyerr(py))?,
))
}
/// Writes the current process ID to the PID file.
///
/// The file is truncated before writing.
fn write<'py>(&mut self, py: Python<'py>) -> PyResult<()> {
self.get_mut()
.write()
.map_err(|e| PyPidfileError(e).into_pyerr(py))
}
/// Extracts the raw file descriptor.
///
/// This function is typically used to **borrow** an owned file descriptor.
/// When used in this way, this method does **not** pass ownership of the
/// raw file descriptor to the caller, and the file descriptor is only
/// guaranteed to be valid while the original object has not yet been
/// destroyed.
fn as_raw_fd(&self) -> RawFd {
self.get().as_raw_fd()
}
/// Closes the PID file and releases associated resources.
fn close(&mut self) {
self.0 = None;
self.0.write().map_err(|e| PyPidfileError(e).into_pyerr(py))
}
}
File renamed without changes.
@@ -2,12 +2,12 @@ import asyncio
import pytest
from _pytest.capture import CaptureFixture
from exo_rs import (
from exo_pyo3_bindings import (
Keypair,
NetworkingHandle,
NoPeersSubscribedToTopicError,
Pidfile,
FromSwarm,
PyFromSwarm,
)
@@ -39,9 +39,9 @@ async def _await_recv(h: NetworkingHandle):
while True:
event = await h.recv()
match event:
case FromSwarm.Connection() as c:
case PyFromSwarm.Connection() as c:
print(f"PYTHON: connection update: {c}")
case FromSwarm.Message() as m:
case PyFromSwarm.Message() as m:
print(f"PYTHON: message: {m}")
+3 -4
View File
@@ -55,7 +55,6 @@
];
OPENSSL_NO_VENDOR = "1";
MATURIN_NO_INSTALL_RUST = "1";
# Required for pyo3 tests to find libpython
LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.python313 ];
@@ -82,11 +81,11 @@
config = {
packages = {
# Python bindings wheel via maturin
exo-rs = craneLib.buildPackage (
exo_pyo3_bindings = craneLib.buildPackage (
commonArgs
// {
inherit cargoArtifacts;
pname = "exo-rs";
pname = "exo_pyo3_bindings";
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
pkgs.maturin
@@ -96,7 +95,7 @@
maturin build \
--release \
--manylinux off \
--manifest-path rust/exo_rs/Cargo.toml \
--manifest-path rust/exo_pyo3_bindings/Cargo.toml \
--features "pyo3/extension-module,pyo3/experimental-async" \
--interpreter ${pkgs.python313}/bin/python \
--out dist
+42 -88
View File
@@ -20,7 +20,7 @@ from fastapi.staticfiles import StaticFiles
from hypercorn.asyncio import serve # pyright: ignore[reportUnknownVariableType]
from hypercorn.config import Config
from hypercorn.typing import ASGIFramework
from hypercorn.utils import LifespanTimeoutError, ShutdownError
from hypercorn.utils import LifespanTimeoutError
from loguru import logger
from exo.api.adapters.chat_completions import (
@@ -50,8 +50,6 @@ from exo.api.keepalive import with_sse_keepalive
from exo.api.types import (
AddCustomModelParams,
AdvancedImageParams,
AwaitInstanceReadyMessage,
AwaitInstanceTimeoutMessage,
BenchChatCompletionRequest,
BenchChatCompletionResponse,
BenchImageGenerationResponse,
@@ -346,7 +344,6 @@ class API:
self.app.post("/place_instance")(self.place_instance)
self.app.get("/instance/placement")(self.get_placement)
self.app.get("/instance/previews")(self.get_placement_previews)
self.app.get("/instance/await", response_model=None)(self.await_instance)
self.app.get("/instance/{instance_id}")(self.get_instance)
self.app.delete("/instance/{instance_id}")(self.delete_instance)
self.app.get("/v1/instance-links")(self.list_instance_links)
@@ -636,48 +633,6 @@ class API:
raise HTTPException(status_code=404, detail="Instance not found")
return self.state.instances[instance_id]
async def await_instance(
self,
model_id: ModelId,
timeout_seconds: float = Query(default=0.0, ge=0.0, le=300.0),
) -> StreamingResponse:
_sleep = 0.1
async def _stream() -> AsyncGenerator[str, None]:
deadline = (
None if timeout_seconds == 0 else anyio.current_time() + timeout_seconds
)
while True:
for instance in self.state.instances.values():
if instance.shard_assignments.model_id == model_id:
payload = AwaitInstanceReadyMessage(instance=instance)
yield f"data: {payload.model_dump_json()}\n\n"
return
if deadline is None:
await anyio.sleep(_sleep)
else:
remaining = deadline - anyio.current_time()
if remaining <= 0:
payload = AwaitInstanceTimeoutMessage(
message=f"No instance found for model {model_id}"
)
yield f"data: {payload.model_dump_json()}\n\n"
return
await anyio.sleep(min(_sleep, remaining))
return StreamingResponse(
with_sse_keepalive(_stream()),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "close",
"X-Accel-Buffering": "no",
},
)
async def delete_instance(self, instance_id: InstanceId) -> DeleteInstanceResponse:
if instance_id not in self.state.instances:
raise HTTPException(status_code=404, detail="Instance not found")
@@ -806,8 +761,6 @@ class API:
if isinstance(chunk, PrefillProgressChunk):
continue
sampler.mark_prefill_done()
if chunk.finish_reason == "error":
raise HTTPException(
status_code=500,
@@ -918,8 +871,10 @@ class API:
) -> ChatCompletionResponse | StreamingResponse:
"""OpenAI Chat Completions API - adapter."""
task_params = await chat_request_to_text_generation(payload)
validated_model = await self._validate_model_has_instance(task_params.model)
task_params = task_params.model_copy(update={"model": validated_model})
resolved_model = await self._resolve_and_validate_text_model(
ModelId(task_params.model)
)
task_params = task_params.model_copy(update={"model": resolved_model})
command = await self._send_text_generation_with_images(task_params)
@@ -951,10 +906,10 @@ class API:
self, payload: BenchChatCompletionRequest
) -> BenchChatCompletionResponse | StreamingResponse:
task_params = await chat_request_to_text_generation(payload)
validated_model = await self._validate_model_has_instance(
resolved_model = await self._resolve_and_validate_text_model(
ModelId(task_params.model)
)
task_params = task_params.model_copy(update={"model": validated_model})
task_params = task_params.model_copy(update={"model": resolved_model})
task_params = task_params.model_copy(
update={
@@ -984,10 +939,8 @@ class API:
return await self._collect_text_generation_with_stats(command.command_id)
async def _validate_model_has_instance(self, model_id: ModelId) -> ModelId:
"""Validate a model has an active instance.
If the model isn't even downloaded, triggers notification to user to download model.
async def _resolve_and_validate_text_model(self, model_id: ModelId) -> ModelId:
"""Validate a text model exists and return the resolved model ID.
Raises HTTPException 404 if no instance is found for the model.
"""
@@ -995,21 +948,30 @@ class API:
instance.shard_assignments.model_id == model_id
for instance in self.state.instances.values()
):
# Check if model is actually downloaded
model_is_downloaded = any(
isinstance(download, DownloadCompleted)
and download.shard_metadata.model_card.model_id == model_id
for node_downloads in self.state.downloads.values()
for download in node_downloads
)
if not model_is_downloaded:
await self._trigger_notify_user_to_download_model(model_id)
await self._trigger_notify_user_to_download_model(model_id)
raise HTTPException(
status_code=404, detail=f"No instance found for model {model_id}"
status_code=404,
detail=f"No instance found for model {model_id}",
)
return model_id
async def _validate_image_model(self, model: ModelId) -> ModelId:
"""Validate model exists and return resolved model ID.
Raises HTTPException 404 if no instance is found for the model.
"""
model_card = await ModelCard.load(model)
resolved_model = model_card.model_id
if not any(
instance.shard_assignments.model_id == resolved_model
for instance in self.state.instances.values()
):
await self._trigger_notify_user_to_download_model(resolved_model)
raise HTTPException(
status_code=404, detail=f"No instance found for model {resolved_model}"
)
return resolved_model
def stream_events(self) -> StreamingResponse:
def _generate_json_array(events: Iterable[Event]) -> Iterable[str]:
yield "["
@@ -1062,9 +1024,7 @@ class API:
"""
payload = payload.model_copy(
update={
"model": await self._validate_model_has_instance(
ModelId(payload.model)
),
"model": await self._validate_image_model(ModelId(payload.model)),
"advanced_params": _ensure_seed(payload.advanced_params),
}
)
@@ -1332,9 +1292,7 @@ class API:
) -> BenchImageGenerationResponse:
payload = payload.model_copy(
update={
"model": await self._validate_model_has_instance(
ModelId(payload.model)
),
"model": await self._validate_image_model(ModelId(payload.model)),
"stream": False,
"partial_images": 0,
"advanced_params": _ensure_seed(payload.advanced_params),
@@ -1370,7 +1328,7 @@ class API:
advanced_params: AdvancedImageParams | None,
) -> ImageEdits:
"""Prepare and send an image edits command with chunked image upload."""
validated_model = await self._validate_model_has_instance(model)
resolved_model = await self._validate_image_model(model)
advanced_params = _ensure_seed(advanced_params)
image_content = await image.read()
@@ -1389,7 +1347,7 @@ class API:
image_data="",
total_input_chunks=total_chunks,
prompt=prompt,
model=validated_model,
model=resolved_model,
n=n,
size=size,
response_format=response_format,
@@ -1410,7 +1368,7 @@ class API:
await self._send(
SendInputChunk(
chunk=InputImageChunk(
model=validated_model,
model=resolved_model,
command_id=command.command_id,
data=chunk_data,
chunk_index=chunk_index,
@@ -1534,10 +1492,10 @@ class API:
) -> ClaudeMessagesResponse | StreamingResponse:
"""Claude Messages API - adapter."""
task_params = await claude_request_to_text_generation(payload)
validated_model = await self._validate_model_has_instance(
resolved_model = await self._resolve_and_validate_text_model(
ModelId(task_params.model)
)
task_params = task_params.model_copy(update={"model": validated_model})
task_params = task_params.model_copy(update={"model": resolved_model})
command = await self._send_text_generation_with_images(task_params)
@@ -1572,8 +1530,8 @@ class API:
) -> ResponsesResponse | StreamingResponse:
"""OpenAI Responses API."""
task_params = await responses_request_to_text_generation(payload)
validated_model = await self._validate_model_has_instance(task_params.model)
task_params = task_params.model_copy(update={"model": validated_model})
resolved_model = await self._resolve_and_validate_text_model(task_params.model)
task_params = task_params.model_copy(update={"model": resolved_model})
command = await self._send_text_generation_with_images(task_params)
@@ -1615,10 +1573,10 @@ class API:
body = await request.body()
payload = OllamaChatRequest.model_validate_json(body)
task_params = ollama_request_to_text_generation(payload)
validated_model = await self._validate_model_has_instance(
resolved_model = await self._resolve_and_validate_text_model(
ModelId(task_params.model)
)
task_params = task_params.model_copy(update={"model": validated_model})
task_params = task_params.model_copy(update={"model": resolved_model})
command = await self._send_text_generation_with_images(task_params)
@@ -1651,10 +1609,10 @@ class API:
body = await request.body()
payload = OllamaGenerateRequest.model_validate_json(body)
task_params = ollama_generate_request_to_text_generation(payload)
validated_model = await self._validate_model_has_instance(
resolved_model = await self._resolve_and_validate_text_model(
ModelId(task_params.model)
)
task_params = task_params.model_copy(update={"model": validated_model})
task_params = task_params.model_copy(update={"model": resolved_model})
command = await self._send_text_generation_with_images(task_params)
@@ -1956,10 +1914,6 @@ class API:
cfg,
shutdown_trigger=ev.wait,
)
if not ev.is_set():
raise ShutdownError(
"Server exited without shutdown trigger - exiting abnormally"
)
except LifespanTimeoutError as e:
logger.warning(
"Graceful server shutdown timed out, some connections forcebly closed"
-2
View File
@@ -1,7 +1,5 @@
from .api import AddCustomModelParams as AddCustomModelParams
from .api import AdvancedImageParams as AdvancedImageParams
from .api import AwaitInstanceReadyMessage as AwaitInstanceReadyMessage
from .api import AwaitInstanceTimeoutMessage as AwaitInstanceTimeoutMessage
from .api import BenchChatCompletionRequest as BenchChatCompletionRequest
from .api import BenchChatCompletionResponse as BenchChatCompletionResponse
from .api import BenchImageGenerationResponse as BenchImageGenerationResponse
-26
View File
@@ -186,12 +186,6 @@ class NodePowerStats(BaseModel, frozen=True):
node_id: NodeId
samples: int
avg_sys_power: float
# Per-phase breakdown. Populated only when the caller marks a phase
# boundary (e.g. prefill -> generation); None otherwise.
prefill_avg_sys_power: float | None = None
generation_avg_sys_power: float | None = None
prefill_energy_joules: float | None = None
generation_energy_joules: float | None = None
class PowerUsage(BaseModel, frozen=True):
@@ -199,16 +193,6 @@ class PowerUsage(BaseModel, frozen=True):
nodes: list[NodePowerStats]
total_avg_sys_power_watts: float
total_energy_joules: float
# Split between the prefill (prompt-processing) phase and the
# generation/decode phase. Populated only when the caller marks a phase
# boundary; None otherwise. The two phase energies should sum to
# approximately `total_energy_joules` (modulo interpolation rounding).
prefill_seconds: float | None = None
generation_seconds: float | None = None
prefill_energy_joules: float | None = None
generation_energy_joules: float | None = None
prefill_avg_sys_power_watts: float | None = None
generation_avg_sys_power_watts: float | None = None
class BenchChatCompletionResponse(ChatCompletionResponse):
@@ -307,16 +291,6 @@ class DeleteInstanceResponse(BaseModel):
instance_id: InstanceId
class AwaitInstanceReadyMessage(BaseModel):
type: Literal["ready"] = "ready"
instance: Instance
class AwaitInstanceTimeoutMessage(BaseModel):
type: Literal["timeout"] = "timeout"
message: str
class CancelCommandResponse(BaseModel):
message: str
command_id: CommandId
-11
View File
@@ -15,10 +15,6 @@ from exo.download.download_utils import (
resolve_existing_model,
)
from exo.download.shard_downloader import ShardDownloader
from exo.routing.event_router import (
EventRouterBrokenResourceError,
EventRouterClosedResourceError,
)
from exo.shared.constants import EXO_DEFAULT_MODELS_DIR, EXO_MODELS_READ_ONLY_DIRS
from exo.shared.models import model_cards
from exo.shared.models.model_cards import ModelId
@@ -143,14 +139,7 @@ class DownloadCoordinator:
async with self._tg as tg:
tg.start_soon(self._command_processor)
tg.start_soon(self._emit_existing_download_progress)
except* (EventRouterBrokenResourceError, EventRouterClosedResourceError):
# Event router has been closed (try-star syntax handles error groups)
pass
finally:
# don't forget to clean up resources
self.download_command_receiver.close()
self.event_sender.close()
self._stopped.set()
async def shutdown(self) -> None:
+19 -66
View File
@@ -8,9 +8,6 @@ from dataclasses import dataclass, field
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 loguru import logger
from pydantic import PositiveInt
@@ -21,12 +18,13 @@ from exo.download.impl_shard_downloader import exo_shard_downloader
from exo.master.main import Master
from exo.routing.event_router import EventRouter
from exo.routing.router import Router, get_node_id_keypair
from exo.shared.constants import EXO_DEFAULT_MODELS_DIR, EXO_LOG, EXO_PID_FILE
from exo.shared.constants import EXO_DEFAULT_MODELS_DIR, EXO_LOG
from exo.shared.election import Election, ElectionResult
from exo.shared.logging import logger_cleanup, logger_setup
from exo.shared.types.common import NodeId, SessionId
from exo.utils import STDIO_FDS
from exo.utils.channels import Receiver, channel
from exo.utils.daemon import detach_stdio_to_devnull
from exo.utils.pidfile import PidfileLockError, acquire_exo_pidfile
from exo.utils.pydantic_ext import FrozenModel
from exo.utils.task_group import TaskGroup
from exo.worker.main import Worker
@@ -192,7 +190,7 @@ class Node:
# - Shut down and re-create the API
if result.is_new_master:
await anyio_checkpoint()
await anyio.sleep(0)
self.event_router.shutdown()
self.event_router = EventRouter(
result.session_id,
@@ -205,10 +203,7 @@ class Node:
result.session_id.master_node_id == self.node_id
and self.master is not None
):
assert not result.is_new_master, (
"cannot be new master if we remain master"
)
logger.info("Node elected Master - maintaining self")
logger.info("Node elected Master")
elif (
result.session_id.master_node_id == self.node_id
and self.master is None
@@ -275,60 +270,14 @@ class Node:
def main():
# Parse args first => --help or bad args don't require PID-locking
# Exit early if no PID file (not compatible with double-for daemonization yet)
try:
pidfile = acquire_exo_pidfile()
except PidfileLockError as exception:
print(exception, file=sys.stderr)
raise SystemExit(1) from exception
args = Args.parse()
# Exit early if cannot acquire PID file
try:
pidfile = Pidfile(EXO_PID_FILE, 0o0600)
except PidfileError as e:
print(e, file=sys.stderr)
raise SystemExit(1) from e
try:
if args.legacy_daemon:
# keep stdio backed by explicit /dev/null streams. multiprocessing spawn expects
# valid stdio FDs; letting DaemonContext close/reopen them can break runner startup.
for stream in (sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__):
if stream is not None:
stream.flush()
stdin = open(os.devnull, "r") # noqa: SIM115
stdout = open(os.devnull, "w") # noqa: SIM115
stderr = open(os.devnull, "w") # noqa: SIM115
with DaemonContext(
detach_process=True,
files_preserve=[pidfile.as_raw_fd()],
stdin=stdin,
stdout=stdout,
stderr=stderr,
):
# cleanup loose file descriptors (as long as they aren't stdio)
for f in (
f for f in (stdin, stdout, stderr) if f.fileno() not in STDIO_FDS
):
f.close()
# 1) if daemonizing => fork then write PID
try:
pidfile.write()
except PidfileError as e:
print(e, file=sys.stderr)
raise SystemExit(1) from e
main_inner(args)
else:
# 2) otherwise => just write PID
try:
pidfile.write()
except PidfileError as e:
print(e, file=sys.stderr)
raise SystemExit(1) from e
main_inner(args)
finally:
pidfile.close()
def main_inner(args: "Args"):
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
target = min(max(soft, 65535), hard)
resource.setrlimit(resource.RLIMIT_NOFILE, (target, hard))
@@ -337,6 +286,9 @@ def main_inner(args: "Args"):
# TODO: Refactor the current verbosity system
logger_setup(EXO_LOG, args.verbosity)
if args.no_stdio:
detach_stdio_to_devnull()
logger.info("Detached stdio to /dev/null")
logger.info(f"{'=' * 40}")
logger.info(f"Starting EXO | pid={os.getpid()}")
@@ -372,6 +324,7 @@ def main_inner(args: "Args"):
finally:
logger.info("EXO Shutdown complete")
logger_cleanup()
del pidfile
class Args(FrozenModel):
@@ -385,7 +338,7 @@ class Args(FrozenModel):
offline: bool = os.getenv("EXO_OFFLINE", "false").lower() == "true"
no_batch: bool = False
fast_synch: bool | None = None # None = auto, True = force on, False = force off
legacy_daemon: bool = False
no_stdio: bool = False
bootstrap_peers: list[str] = []
libp2p_port: int
@@ -446,9 +399,9 @@ class Args(FrozenModel):
help="Disable continuous batching, use sequential generation",
)
parser.add_argument(
"--legacy-daemon",
"--no-stdio",
action="store_true",
help="Run as a legacy SysV-style background daemon using double-fork daemonization",
help="Detach stdin/stdout/stderr to /dev/null after logging is configured",
)
parser.add_argument(
"--bootstrap-peers",
+3 -16
View File
@@ -11,10 +11,6 @@ from exo.master.placement import (
place_instance,
)
from exo.master.placement_utils import find_ip_prioritised
from exo.routing.event_router import (
EventRouterBrokenResourceError,
EventRouterClosedResourceError,
)
from exo.shared.apply import apply
from exo.shared.constants import EXO_EVENT_LOG_DIR, EXO_TRACING_ENABLED
from exo.shared.types.commands import (
@@ -155,9 +151,6 @@ class Master:
tg.start_soon(self._event_processor)
tg.start_soon(self._command_processor)
tg.start_soon(self._plan)
except* (EventRouterBrokenResourceError, EventRouterClosedResourceError):
# Event router has been closed (try-star syntax handles error groups)
pass
finally:
self._event_log.close()
self.global_event_sender.close()
@@ -181,7 +174,6 @@ class Master:
case TestCommand():
pass
case TextGeneration():
# set-difference => prefill-only nodes
prefill_only: set[InstanceId] = set()
for link in self.state.instance_links.values():
prefill_only.update(link.prefill_instances)
@@ -189,13 +181,11 @@ class Master:
prefill_only.difference_update(link.decode_instances)
for instance in self.state.instances.values():
# NON-prefill-only instances matching the model ID
if (
instance.shard_assignments.model_id
== command.task_params.model
and instance.instance_id not in prefill_only
):
# count in-flight tasks of that instance
in_flight = {TaskStatus.Pending, TaskStatus.Running}
task_count = sum(
1
@@ -207,7 +197,6 @@ class Master:
task_count
)
# there are no NON-prefill-only instances matching this model ID
if not instance_task_counts:
raise ValueError(
f"No instance found for model {command.task_params.model}"
@@ -459,9 +448,7 @@ class Master:
self._event_log.read_range(command.since_idx, end),
start=command.since_idx,
):
await self._send_indexed_event(
IndexedEvent(idx=i, event=event)
)
await self._send_event(IndexedEvent(idx=i, event=event))
for event in generated_events:
await self.event_sender.send(event)
except ValueError as e:
@@ -519,10 +506,10 @@ class Master:
self.state = apply(self.state, indexed)
self._event_log.append(event)
await self._send_indexed_event(indexed)
await self._send_event(indexed)
# This function is re-entrant, take care!
async def _send_indexed_event(self, event: IndexedEvent):
async def _send_event(self, event: IndexedEvent):
# Convenience method since this line is ugly
await self.global_event_sender.send(
GlobalForwarderEvent(
+2 -2
View File
@@ -1,4 +1,4 @@
from exo_rs import FromSwarm
from exo_pyo3_bindings import PyFromSwarm
from exo.shared.types.common import NodeId
from exo.utils.pydantic_ext import FrozenModel
@@ -11,5 +11,5 @@ class ConnectionMessage(FrozenModel):
connected: bool
@classmethod
def from_update(cls, update: FromSwarm.Connection) -> "ConnectionMessage":
def from_update(cls, update: PyFromSwarm.Connection) -> "ConnectionMessage":
return cls(node_id=NodeId(update.peer_id), connected=update.connected)
+2 -21
View File
@@ -15,30 +15,11 @@ from exo.shared.types.events import (
IndexedEvent,
LocalForwarderEvent,
)
from exo.utils import channels
from exo.utils.channels import Receiver, Sender, channel
from exo.utils.event_buffer import OrderedBuffer
from exo.utils.task_group import TaskGroup
class EventRouterClosedResourceError(ClosedResourceError):
pass
class EventRouterBrokenResourceError(BrokenResourceError):
pass
# Event Router is created and destroyed before consumers of its channels are,
# hence its nice to have tagged errors for event-router channels being closed
#
# so consumers can catch specifically these errors, rather than the generic ones
_ERROR_CFG = channels.ErrorOverride(
closed_resource_error=EventRouterClosedResourceError,
broken_resource_error=EventRouterBrokenResourceError,
)
@dataclass
class EventRouter:
session_id: SessionId
@@ -83,7 +64,7 @@ class EventRouter:
await self.external_outbound.send(event)
def sender(self) -> Sender[Event]:
send, recv = channel[Event](error_override_config=_ERROR_CFG)
send, recv = channel[Event]()
if self._tg.is_running():
self._tg.start_soon(self._ingest, SystemId(), recv)
else:
@@ -92,7 +73,7 @@ class EventRouter:
def receiver(self) -> Receiver[IndexedEvent]:
assert not self._tg.is_running()
send, recv = channel[IndexedEvent](error_override_config=_ERROR_CFG)
send, recv = channel[IndexedEvent]()
self.internal_outbound.append(send)
return recv
+4 -4
View File
@@ -12,13 +12,13 @@ from anyio import (
move_on_after,
sleep_forever,
)
from exo_rs import (
from exo_pyo3_bindings import (
AllQueuesFullError,
FromSwarm,
Keypair,
MessageTooLargeError,
NetworkingHandle,
NoPeersSubscribedToTopicError,
PyFromSwarm,
)
from filelock import FileLock
from loguru import logger
@@ -191,7 +191,7 @@ class Router:
from_swarm = await self._net.recv()
logger.debug(from_swarm)
match from_swarm:
case FromSwarm.Message(origin, topic, data):
case PyFromSwarm.Message(origin, topic, data):
logger.trace(
f"Received message on {topic} from {origin} with payload {data}"
)
@@ -202,7 +202,7 @@ class Router:
continue
router = self.topic_routers[topic]
await router.publish_bytes(data)
case FromSwarm.Connection():
case PyFromSwarm.Connection():
message = ConnectionMessage.from_update(from_swarm)
logger.trace(
f"Received message on connection_messages with payload {message}"
+1 -1
View File
@@ -46,7 +46,7 @@ class _InterceptHandler(logging.Handler):
def logger_setup(log_file: Path | None, verbosity: int = 0):
"""Set up logging for this process - formatting, file handles, verbosity and output"""
logging.getLogger("exo_rs").setLevel(logging.WARNING)
logging.getLogger("exo_pyo3_bindings").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
+2 -9
View File
@@ -81,10 +81,8 @@ class _CardCache:
card = card.model_copy(update={"is_custom": True})
if self.get(card.model_id) is None:
self.cc[card.model_id] = card
except (ValidationError, TOMLKitError) as e:
logger.opt(exception=e).warning(
f"failed to validate model card at {toml_file}"
)
except (ValidationError, TOMLKitError):
pass
async def refresh(self) -> None:
for path in _BUILTIN_CARD_DIRS:
@@ -199,11 +197,6 @@ class ModelCard(FrozenModel):
def _validate_tasks(cls, v: list[str | ModelTask]) -> list[ModelTask]:
return [item if isinstance(item, ModelTask) else ModelTask(item) for item in v]
@field_validator("backends", mode="before")
@classmethod
def _validate_backends(cls, v: list[str | Backend]) -> list[Backend]:
return [item if isinstance(item, Backend) else Backend(item) for item in v]
async def save(self, path: Path) -> None:
async with await open_file(path, "w") as f:
py = self.model_dump(exclude_none=True, exclude={"is_custom"})
+1
View File
@@ -13,6 +13,7 @@ from exo.shared.models.model_cards import ModelId
from exo.utils.pydantic_ext import TaggedModel
from exo.worker.runner.diagnostics import KnownRunnerDiagnostic
from ...worker.runner.diagnostics import KnownRunnerDiagnostic
from .common import CommandId
-5
View File
@@ -2,11 +2,6 @@ from typing import Any, Type
from .phantom import PhantomData
STDIN_FD = 0
STDOUT_FD = 1
STDERR_FD = 2
STDIO_FDS = (STDIN_FD, STDOUT_FD, STDERR_FD)
def ensure_type[T](obj: Any, expected_type: Type[T]) -> T: # type: ignore
if not isinstance(obj, expected_type):
+5 -4
View File
@@ -25,9 +25,10 @@ from anyio import (
from anyio.abc import TaskStatus
from loguru import logger
from exo.utils import STDERR_FD, STDIO_FDS, STDOUT_FD
from exo.utils.channels import Receiver, Sender, channel
_STDOUT_FD = 1
_STDERR_FD = 2
_READ_CHUNK_SIZE = 64 * 1024
_JOIN_GRACE_SECONDS = 3.0
_TERMINATE_GRACE_SECONDS = 5.0
@@ -255,11 +256,11 @@ def _run_with_captured_stdio(
stderr_fd = stderr.detach()
try:
os.dup2(stdout_fd, STDOUT_FD)
os.dup2(stderr_fd, STDERR_FD)
os.dup2(stdout_fd, _STDOUT_FD)
os.dup2(stderr_fd, _STDERR_FD)
finally:
for fd in (stdout_fd, stderr_fd):
if fd not in STDIO_FDS:
if fd not in (_STDOUT_FD, _STDERR_FD):
_close_fd(fd)
faulthandler.enable(file=sys.stderr, all_threads=True)
+8 -155
View File
@@ -1,16 +1,13 @@
import contextlib
import multiprocessing as mp
from dataclasses import dataclass, field
from functools import wraps
from inspect import iscoroutinefunction
from math import inf
from multiprocessing.synchronize import Event
from queue import Empty, Full
from types import CoroutineType, TracebackType
from typing import Any, Callable, NoReturn, Self, cast, overload, override
from types import TracebackType
from typing import Any, Self
from anyio import (
BrokenResourceError,
CapacityLimiter,
ClosedResourceError,
EndOfStream,
@@ -23,172 +20,35 @@ from anyio.streams.memory import (
from anyio.streams.memory import (
MemoryObjectSendStream as AnyioSender,
)
from anyio.streams.memory import (
MemoryObjectStreamState,
)
from anyio.streams.memory import (
MemoryObjectStreamState as AnyioState,
)
@dataclass(eq=False)
class ErrorOverride:
closed_resource_error: type[ClosedResourceError] = field(
default=ClosedResourceError,
)
broken_resource_error: type[BrokenResourceError] = field(
default=BrokenResourceError,
)
end_of_stream: type[EndOfStream] = field(
default=EndOfStream,
)
would_block: type[WouldBlock] = field(
default=WouldBlock,
)
@overload
def patch[**P, R](
self,
fn: Callable[P, CoroutineType[Any, Any, R]],
/,
) -> Callable[P, CoroutineType[Any, Any, R]]: ...
@overload
def patch[**P, R](
self,
fn: Callable[P, R],
/,
) -> Callable[P, R]: ...
def patch[**P, R](self, fn: Callable[P, Any], /) -> Callable[P, Any]:
"""
Returns a function with all these exceptions replaced by their overrides
"""
if iscoroutinefunction(fn):
async_fn = cast(Callable[P, CoroutineType[Any, Any, R]], fn)
@wraps(async_fn)
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
try:
return await async_fn(*args, **kwargs)
except ClosedResourceError as e:
self._raise_replace(self.closed_resource_error, e)
except BrokenResourceError as e:
self._raise_replace(self.broken_resource_error, e)
except EndOfStream as e:
self._raise_replace(self.end_of_stream, e)
except WouldBlock as e:
self._raise_replace(self.would_block, e)
return async_wrapper
else:
sync_fn = cast(Callable[P, R], fn)
@wraps(sync_fn)
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
try:
return sync_fn(*args, **kwargs)
except ClosedResourceError as e:
self._raise_replace(self.closed_resource_error, e)
except BrokenResourceError as e:
self._raise_replace(self.broken_resource_error, e)
except EndOfStream as e:
self._raise_replace(self.end_of_stream, e)
except WouldBlock as e:
self._raise_replace(self.would_block, e)
return sync_wrapper
@staticmethod
def _raise_replace(replacement: type[BaseException], e: BaseException) -> NoReturn:
if isinstance(e, replacement):
raise
raise replacement() from e
class Sender[T](AnyioSender[T]):
def __init__(
self,
state: MemoryObjectStreamState[T],
error_override_config: ErrorOverride | None,
):
super().__init__(_state=state)
# patch the methods we want to override errors for
#
# NOTE: it is very important that new methods which are added,
# and which can throw, are patched in this block
if (e := error_override_config) is not None:
# new methods of this class
self.clone_receiver = e.patch(self.clone_receiver)
# overridden methods
self.clone = e.patch(self.clone)
# parent methods
self.send_nowait = e.patch(self.send_nowait)
self.send = e.patch(self.send)
self.close = e.patch(self.close)
self.aclose = e.patch(self.aclose)
self.statistics = e.patch(self.statistics)
self.err_config = error_override_config
@override
def clone(self) -> "Sender[T]":
if self._closed:
raise ClosedResourceError
return Sender(self._state, self.err_config)
return Sender(_state=self._state)
def clone_receiver(self) -> "Receiver[T]":
"""Constructs a Receiver using a Senders shared state - similar to calling Receiver.clone() without needing the receiver"""
if self._closed:
raise ClosedResourceError
return Receiver(self._state, self.err_config)
return Receiver(_state=self._state)
class Receiver[T](AnyioReceiver[T]):
def __init__(
self,
state: MemoryObjectStreamState[T],
error_override_config: ErrorOverride | None,
):
super().__init__(_state=state)
# patch the methods we want to override errors for
#
# NOTE: it is very important that new methods which are added,
# and which can throw, are patched in this block
if (e := error_override_config) is not None:
# new methods of this class
self.clone_sender = e.patch(self.clone_sender)
self.collect = e.patch(self.collect)
self.receive_at_least = e.patch(self.receive_at_least)
# overridden methods
self.clone = e.patch(self.clone)
# parent methods
self.receive_nowait = e.patch(self.receive_nowait)
self.receive = e.patch(self.receive)
self.close = e.patch(self.close)
self.aclose = e.patch(self.aclose)
self.statistics = e.patch(self.statistics)
self.err_config = error_override_config
@override
def clone(self) -> "Receiver[T]":
if self._closed:
raise ClosedResourceError
return Receiver(self._state, self.err_config)
return Receiver(_state=self._state)
def clone_sender(self) -> Sender[T]:
"""Constructs a Sender using a Receivers shared state - similar to calling Sender.clone() without needing the sender"""
if self._closed:
raise ClosedResourceError
return Sender(self._state, self.err_config)
return Sender(_state=self._state)
def collect(self) -> list[T]:
"""Collect all currently available items from this receiver"""
@@ -210,7 +70,6 @@ class Receiver[T](AnyioReceiver[T]):
out.extend(self.collect())
return out
@override
def __enter__(self) -> Self:
return self
@@ -426,17 +285,11 @@ class MpReceiver[T]:
class channel[T]: # noqa: N801
"""Create a pair of asynchronous channels for communicating within the same process"""
def __new__(
cls,
max_buffer_size: float = inf,
error_override_config: ErrorOverride | None = None,
) -> tuple[Sender[T], Receiver[T]]:
def __new__(cls, max_buffer_size: float = inf) -> tuple[Sender[T], Receiver[T]]:
if max_buffer_size != inf and not isinstance(max_buffer_size, int):
raise ValueError("max_buffer_size must be either an integer or math.inf")
state = AnyioState[T](max_buffer_size)
return Sender(state, error_override_config), Receiver(
state, error_override_config
)
return Sender(_state=state), Receiver(_state=state)
class mp_channel[T]: # noqa: N801
+28
View File
@@ -0,0 +1,28 @@
import os
import sys
_STDIN_FD = 0
_STDOUT_FD = 1
_STDERR_FD = 2
def detach_stdio_to_devnull() -> None:
"""Redirect process stdio file descriptors to /dev/null."""
for stream in (sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__):
if stream is not None:
stream.flush()
stdin_fd = os.open(os.devnull, os.O_RDONLY)
stdout_fd = os.open(os.devnull, os.O_WRONLY)
stderr_fd = os.open(os.devnull, os.O_WRONLY)
try:
# dup2 closes the target fd first, but leaves the source fd open.
os.dup2(stdin_fd, _STDIN_FD)
os.dup2(stdout_fd, _STDOUT_FD)
os.dup2(stderr_fd, _STDERR_FD)
finally:
for fd in (stdin_fd, stdout_fd, stderr_fd):
if fd not in (_STDIN_FD, _STDOUT_FD, _STDERR_FD):
os.close(fd)
@@ -630,14 +630,6 @@ class InfoGatherer:
f"MacMon failed with return code {e.returncode}: {stderr_msg}"
)
self._tg.start_soon(self._monitor_memory_usage, 1)
except ProcessLookupError:
# usually throws by the process' context manager on exit
# when we ctrl+c, hence usually should be ignored;
# if anything else throws it, we explicitly don't care:
# process is dead anyways ;)
logger.warning(
"Macmon process not found - shutting down macmon monitor"
)
except Exception as e:
logger.opt(exception=e).warning("Error in macmon monitor")
self._tg.start_soon(self._monitor_memory_usage, 1)
+41
View File
@@ -1,3 +1,6 @@
import os
import shutil
import subprocess
from typing import Self
from pydantic import BaseModel
@@ -68,3 +71,41 @@ class MacmonMetrics(TaggedModel):
@classmethod
def from_raw_json(cls, json: str) -> Self:
return cls.from_raw(RawMacmonMetrics.model_validate_json(json))
def read_macmon_metrics_once(
macmon_path: str | None = None,
*,
timeout: float = 5,
) -> MacmonMetrics | None:
"""
Read a single macmon sample, returning None when macmon is unavailable.
"""
resolved_macmon_path = (
macmon_path or os.getenv("EXO_MACMON_PATH") or shutil.which("macmon")
)
if resolved_macmon_path is None:
return None
try:
result = subprocess.run(
[resolved_macmon_path, "pipe", "--samples", "1", "--interval", "100"],
capture_output=True,
check=False,
text=True,
timeout=timeout,
)
except (OSError, subprocess.SubprocessError):
return None
if result.returncode != 0:
return None
lines = result.stdout.strip().splitlines()
if not lines:
return None
try:
return MacmonMetrics.from_raw_json(lines[0])
except ValueError:
return None
+28
View File
@@ -0,0 +1,28 @@
from __future__ import annotations
import os
from typing import Final
from exo_pyo3_bindings import Pidfile, PidfileError
from exo.shared.constants import EXO_PID_FILE
_PIDFILE_MODE: Final = 0o600
class PidfileLockError(RuntimeError):
pass
def acquire_exo_pidfile() -> Pidfile:
path = EXO_PID_FILE
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
pidfile = Pidfile(path, _PIDFILE_MODE)
pidfile.write()
except (OSError, PidfileError) as exception:
raise PidfileLockError(
f"Failed to acquire EXO pidfile at {path}: {exception}"
) from exception
return pidfile
-122
View File
@@ -24,7 +24,6 @@ class PowerSampler:
] = defaultdict(list)
self._start_time: float | None = None
self._stopped = False
self._prefill_done_at: float | None = None
def _take_sample(self, t_rel: float | None = None) -> None:
assert self._start_time is not None
@@ -39,35 +38,14 @@ class PowerSampler:
await anyio.sleep(self._interval)
self._take_sample()
def mark_prefill_done(self) -> None:
"""Anchor the prefill→generation boundary on a fresh sample.
Idempotent. Safe to call before `run()`; boundary then lands at t=0.
"""
if self._prefill_done_at is not None:
return
if self._start_time is None:
self._prefill_done_at = 0.0
return
t_rel = time.perf_counter() - self._start_time
self._take_sample(t_rel=t_rel)
self._prefill_done_at = t_rel
def result(self) -> PowerUsage:
self._stopped = True
assert self._start_time is not None, "result() called before run()"
elapsed = time.perf_counter() - self._start_time
self._take_sample(t_rel=elapsed)
# Clamp the split point to [0, elapsed] in case timing is weird (e.g.
# mark called after result, or sampler ran for < the prefill window).
split = self._prefill_done_at
if split is not None:
split = max(0.0, min(elapsed, split))
node_stats: list[NodePowerStats] = []
total_energy_j = 0.0
total_prefill_energy_j = 0.0
total_generation_energy_j = 0.0
for node_id, ts_profiles in self._samples.items():
n = len(ts_profiles)
if n == 0:
@@ -75,68 +53,20 @@ class PowerSampler:
node_energy_j = trapezoidal_energy(ts_profiles, elapsed)
avg_power_w = node_energy_j / elapsed if elapsed > 0 else 0.0
total_energy_j += node_energy_j
prefill_e: float | None = None
generation_e: float | None = None
prefill_avg: float | None = None
generation_avg: float | None = None
if split is not None:
prefill_e = trapezoidal_energy_range(ts_profiles, 0.0, split)
generation_e = trapezoidal_energy_range(ts_profiles, split, elapsed)
total_prefill_energy_j += prefill_e
total_generation_energy_j += generation_e
prefill_dt = split
generation_dt = elapsed - split
prefill_avg = prefill_e / prefill_dt if prefill_dt > 0 else 0.0
generation_avg = (
generation_e / generation_dt if generation_dt > 0 else 0.0
)
node_stats.append(
NodePowerStats(
node_id=node_id,
samples=n,
avg_sys_power=avg_power_w,
prefill_avg_sys_power=prefill_avg,
generation_avg_sys_power=generation_avg,
prefill_energy_joules=prefill_e,
generation_energy_joules=generation_e,
)
)
total_avg_sys_w = total_energy_j / elapsed if elapsed > 0 else 0.0
prefill_seconds: float | None = None
generation_seconds: float | None = None
prefill_energy_joules: float | None = None
generation_energy_joules: float | None = None
prefill_avg_w: float | None = None
generation_avg_w: float | None = None
if split is not None:
prefill_seconds = split
generation_seconds = elapsed - split
prefill_energy_joules = total_prefill_energy_j
generation_energy_joules = total_generation_energy_j
prefill_avg_w = (
total_prefill_energy_j / prefill_seconds if prefill_seconds > 0 else 0.0
)
generation_avg_w = (
total_generation_energy_j / generation_seconds
if generation_seconds > 0
else 0.0
)
return PowerUsage(
elapsed_seconds=elapsed,
nodes=node_stats,
total_avg_sys_power_watts=total_avg_sys_w,
total_energy_joules=total_energy_j,
prefill_seconds=prefill_seconds,
generation_seconds=generation_seconds,
prefill_energy_joules=prefill_energy_joules,
generation_energy_joules=generation_energy_joules,
prefill_avg_sys_power_watts=prefill_avg_w,
generation_avg_sys_power_watts=generation_avg_w,
)
@@ -159,55 +89,3 @@ def trapezoidal_energy(
continue
energy_j += (p_prev.sys_power + p_cur.sys_power) / 2.0 * dt
return energy_j
def trapezoidal_energy_range(
ts_profiles: list[tuple[float, SystemPerformanceProfile]],
t_start: float,
t_end: float,
) -> float:
"""Integrate sys_power(t) over [t_start, t_end] using the trapezoidal rule.
Linearly interpolates power at the endpoints when they fall between
existing samples, so callers can integrate over arbitrary sub-windows
(e.g. the prefill segment) without losing accuracy. Returns 0 for an
empty or zero-length window. Falls back to constant-power assumption
when only one sample exists.
"""
if t_end <= t_start:
return 0.0
if len(ts_profiles) == 0:
return 0.0
if len(ts_profiles) == 1:
return ts_profiles[0][1].sys_power * (t_end - t_start)
def power_at(t: float) -> float:
if t <= ts_profiles[0][0]:
return ts_profiles[0][1].sys_power
if t >= ts_profiles[-1][0]:
return ts_profiles[-1][1].sys_power
for i in range(1, len(ts_profiles)):
t_cur, p_cur = ts_profiles[i]
if t_cur >= t:
t_prev, p_prev = ts_profiles[i - 1]
span = t_cur - t_prev
if span <= 0:
return p_cur.sys_power
frac = (t - t_prev) / span
return p_prev.sys_power + frac * (p_cur.sys_power - p_prev.sys_power)
return ts_profiles[-1][1].sys_power
p_start = power_at(t_start)
p_end = power_at(t_end)
in_range: list[tuple[float, float]] = [
(t, profile.sys_power) for t, profile in ts_profiles if t_start < t < t_end
]
seq: list[tuple[float, float]] = [(t_start, p_start)] + in_range + [(t_end, p_end)]
energy_j = 0.0
for i in range(1, len(seq)):
dt = seq[i][0] - seq[i - 1][0]
if dt <= 0:
continue
energy_j += (seq[i - 1][1] + seq[i][1]) / 2.0 * dt
return energy_j
-121
View File
@@ -1,121 +0,0 @@
import multiprocessing as mp
import time
import pytest
from anyio import (
BrokenResourceError,
ClosedResourceError,
EndOfStream,
WouldBlock,
fail_after,
)
from loguru import logger
from exo.utils.channels import ErrorOverride, MpReceiver, MpSender, channel, mp_channel
class CustomClosedResourceError(ClosedResourceError):
pass
class CustomBrokenResourceError(BrokenResourceError):
pass
class CustomEndOfStream(EndOfStream):
pass
class CustomWouldBlock(WouldBlock):
pass
ERROR_OVERRIDE = ErrorOverride(
closed_resource_error=CustomClosedResourceError,
broken_resource_error=CustomBrokenResourceError,
end_of_stream=CustomEndOfStream,
would_block=CustomWouldBlock,
)
def foo(recv: MpReceiver[str]):
expected = ["hi", "hi 2", "bye"]
with recv as r:
for item in r:
assert item == expected.pop(0)
def bar(send: MpSender[str]):
logger.warning("hi")
send.send("hi")
time.sleep(0.1)
logger.warning("hi 2")
send.send("hi 2")
time.sleep(0.1)
logger.warning("bye")
send.send("bye")
time.sleep(0.1)
send.close()
@pytest.mark.anyio
async def test_channel_ipc():
with fail_after(0.5):
s, r = mp_channel[str]()
p1 = mp.Process(target=foo, args=(r,))
p2 = mp.Process(target=bar, args=(s,))
p1.start()
p2.start()
p1.join()
p2.join()
def test_channel_error_override_replaces_sync_errors_with_subclasses():
send, recv = channel[int](0, error_override_config=ERROR_OVERRIDE)
with pytest.raises(CustomWouldBlock) as would_block_info:
send.send_nowait(1)
assert type(would_block_info.value.__cause__) is WouldBlock
recv.close()
with pytest.raises(CustomBrokenResourceError) as broken_resource_info:
send.send_nowait(1)
assert type(broken_resource_info.value.__cause__) is BrokenResourceError
send.close()
with pytest.raises(CustomClosedResourceError) as closed_resource_info:
send.send_nowait(1)
assert type(closed_resource_info.value.__cause__) is ClosedResourceError
@pytest.mark.anyio
async def test_channel_error_override_replaces_async_errors_with_subclasses():
send, recv = channel[int](0, error_override_config=ERROR_OVERRIDE)
recv.close()
with pytest.raises(CustomBrokenResourceError) as broken_resource_info:
await send.send(1)
assert type(broken_resource_info.value.__cause__) is BrokenResourceError
send, recv = channel[int](error_override_config=ERROR_OVERRIDE)
send.close()
with pytest.raises(CustomEndOfStream) as end_of_stream_info:
await recv.receive()
assert type(end_of_stream_info.value.__cause__) is EndOfStream
@pytest.mark.anyio
async def test_channel_error_override_is_preserved_by_clones():
send, recv = channel[int](0, error_override_config=ERROR_OVERRIDE)
send_clone = send.clone()
recv.close()
with pytest.raises(CustomBrokenResourceError):
await send_clone.send(1)
send, recv = channel[int](0, error_override_config=ERROR_OVERRIDE)
cloned_send = recv.clone_sender()
recv.close()
with pytest.raises(CustomBrokenResourceError):
await cloned_send.send(1)
+168
View File
@@ -0,0 +1,168 @@
import contextlib
import os
from collections.abc import AsyncIterator
import anyio
import pytest
from anyio import EndOfStream, create_task_group, fail_after
from exo.utils.async_process import AsyncProcess
from exo.utils.channels import MpReceiver, MpSender, Receiver, mp_channel
from exo.utils.daemon import detach_stdio_to_devnull
def _write_before_and_after_detach() -> None:
os.write(1, b"before stdout\n")
os.write(2, b"before stderr\n")
detach_stdio_to_devnull()
os.write(1, b"after stdout\n")
os.write(2, b"after stderr\n")
def _write_grandchild_stdio(label: str) -> None:
os.write(1, f"{label} stdout\n".encode())
os.write(2, f"{label} stderr\n".encode())
async def _spawn_grandchild_and_report(
result_sender: MpSender[tuple[int, bytes, bytes]],
label: str,
) -> None:
result_sender.send(await _collect_spawned_child(label))
result_sender.close()
async def _collect_spawned_child(label: str) -> tuple[int, bytes, bytes]:
process = AsyncProcess(_write_grandchild_stdio, args=(label,))
async with _started_process(process):
return await _collect_process_output(process)
def _detach_stdio_then_spawn_captured_child(
result_sender: MpSender[tuple[int, bytes, bytes]],
) -> None:
detach_stdio_to_devnull()
anyio.run(_spawn_grandchild_and_report, result_sender, "grandchild")
def _detach_stdio_then_spawn_captured_children_sequentially(
result_sender: MpSender[list[tuple[int, bytes, bytes]]],
) -> None:
async def run_children() -> list[tuple[int, bytes, bytes]]:
results: list[tuple[int, bytes, bytes]] = []
for index in range(5):
results.append(await _collect_spawned_child(f"grandchild-{index}"))
return results
detach_stdio_to_devnull()
result_sender.send(anyio.run(run_children))
result_sender.close()
async def _collect_stream(stream: Receiver[bytes], output: bytearray) -> None:
while True:
try:
output.extend(await stream.receive())
except EndOfStream:
return
async def _collect_process_output(
process: AsyncProcess,
) -> tuple[int, bytes, bytes]:
stdout = bytearray()
stderr = bytearray()
exitcodes: list[int] = []
async with create_task_group() as collect_group:
collect_group.start_soon(_collect_stream, process.stdout, stdout)
collect_group.start_soon(_collect_stream, process.stderr, stderr)
exitcodes.append(await process.wait())
if not exitcodes:
raise RuntimeError("process exited without a return code")
return exitcodes[0], bytes(stdout), bytes(stderr)
@contextlib.asynccontextmanager
async def _started_process(process: AsyncProcess) -> AsyncIterator[None]:
async with create_task_group() as task_group:
await task_group.start(process.run)
try:
yield
finally:
await process.stop()
async def _run_process_and_receive[T](
process: AsyncProcess,
recv: MpReceiver[T],
*,
timeout: float,
) -> tuple[int, T]:
async with _started_process(process):
with fail_after(timeout):
result = await recv.receive_async()
exitcode = await process.wait()
return exitcode, result
@pytest.mark.anyio
async def test_detach_stdio_to_devnull_redirects_stdio_away_from_capture() -> None:
process = AsyncProcess(_write_before_and_after_detach)
async with _started_process(process):
exitcode, stdout, stderr = await _collect_process_output(process)
assert exitcode == 0
assert stdout == b"before stdout\n"
assert stderr == b"before stderr\n"
@pytest.mark.anyio
async def test_detached_stdio_process_can_spawn_and_capture_child_stdio() -> None:
send, recv = mp_channel[tuple[int, bytes, bytes]]()
process = AsyncProcess(_detach_stdio_then_spawn_captured_child, args=(send,))
try:
daemonized_parent_exitcode, result = await _run_process_and_receive(
process, recv, timeout=5
)
finally:
recv.close()
child_exitcode, child_stdout, child_stderr = result
assert daemonized_parent_exitcode == 0
assert child_exitcode == 0
assert child_stdout == b"grandchild stdout\n"
assert child_stderr == b"grandchild stderr\n"
@pytest.mark.anyio
async def test_detached_stdio_process_can_spawn_captured_children_sequentially() -> (
None
):
send, recv = mp_channel[list[tuple[int, bytes, bytes]]]()
process = AsyncProcess(
_detach_stdio_then_spawn_captured_children_sequentially,
args=(send,),
)
try:
daemonized_parent_exitcode, results = await _run_process_and_receive(
process, recv, timeout=10
)
finally:
recv.close()
assert daemonized_parent_exitcode == 0
assert results == [
(
0,
f"grandchild-{index} stdout\n".encode(),
f"grandchild-{index} stderr\n".encode(),
)
for index in range(5)
]
+40
View File
@@ -0,0 +1,40 @@
import multiprocessing as mp
import time
import pytest
from anyio import fail_after
from loguru import logger
from exo.utils.channels import MpReceiver, MpSender, mp_channel
def foo(recv: MpReceiver[str]):
expected = ["hi", "hi 2", "bye"]
with recv as r:
for item in r:
assert item == expected.pop(0)
def bar(send: MpSender[str]):
logger.warning("hi")
send.send("hi")
time.sleep(0.1)
logger.warning("hi 2")
send.send("hi 2")
time.sleep(0.1)
logger.warning("bye")
send.send("bye")
time.sleep(0.1)
send.close()
@pytest.mark.anyio
async def test_channel_ipc():
with fail_after(0.5):
s, r = mp_channel[str]()
p1 = mp.Process(target=foo, args=(r,))
p2 = mp.Process(target=bar, args=(s,))
p1.start()
p2.start()
p1.join()
p2.join()
+24 -14
View File
@@ -8,28 +8,36 @@ import textwrap
from pathlib import Path
from typing import Final
from exo_rs import Pidfile
import pytest
import exo.utils.pidfile as pidfile
from exo.utils.pidfile import acquire_exo_pidfile
_CHILD_ACQUIRE_PIDFILE_SCRIPT: Final = textwrap.dedent(
"""
import sys
from pathlib import Path
from unittest.mock import patch
from exo_rs import Pidfile, PidfileError
import exo.utils.pidfile as pidfile
from exo.utils.pidfile import PidfileLockError, acquire_exo_pidfile
path = Path(sys.argv[1])
try:
handle = Pidfile(path, 0o0600)
handle.write()
except (OSError, PidfileError) as exception:
print(f"Failed to acquire EXO pidfile at {path}: {exception}")
raise SystemExit(73) from exception
with patch.object(pidfile, "EXO_PID_FILE", Path(sys.argv[1])):
try:
handle = acquire_exo_pidfile()
except PidfileLockError as exception:
print(str(exception))
raise SystemExit(73) from exception
del handle
del handle
"""
)
def _use_pidfile_path(monkeypatch: pytest.MonkeyPatch, path: Path) -> None:
monkeypatch.setattr(pidfile, "EXO_PID_FILE", path)
def _run_child_acquire_pidfile(path: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, "-c", _CHILD_ACQUIRE_PIDFILE_SCRIPT, str(path)],
@@ -41,11 +49,12 @@ def _run_child_acquire_pidfile(path: Path) -> subprocess.CompletedProcess[str]:
def test_acquire_exo_pidfile_writes_current_pid_and_removes_on_drop(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
path = tmp_path / "exo.pid"
_use_pidfile_path(monkeypatch, path)
handle = Pidfile(path, 0o0600)
handle.write()
handle = acquire_exo_pidfile()
assert path.read_text() == str(os.getpid())
del handle
@@ -56,11 +65,12 @@ def test_acquire_exo_pidfile_writes_current_pid_and_removes_on_drop(
def test_acquire_exo_pidfile_rejects_second_process(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
path = tmp_path / "exo.pid"
_use_pidfile_path(monkeypatch, path)
handle = Pidfile(path, 0o0600)
handle.write()
handle = acquire_exo_pidfile()
try:
blocked_child = _run_child_acquire_pidfile(path)
assert blocked_child.returncode == 73
-136
View File
@@ -141,142 +141,6 @@ def test_trapezoidal_unit_single_sample() -> None:
assert trapezoidal_energy(samples, elapsed=3.0) == 42.0 * 3.0
def test_trapezoidal_range_interpolation() -> None:
"""Sub-window integration should linearly interpolate at the boundaries."""
from exo.utils.power_sampler import trapezoidal_energy_range
# Two samples: t=0 W=10, t=10 W=20 -> power(t) = 10 + t
samples = [
(0.0, _make_profile(10.0)),
(10.0, _make_profile(20.0)),
]
# Integral from t=4 to t=6: power goes 14 -> 16, mean 15, dt=2 -> 30 J
assert abs(trapezoidal_energy_range(samples, 4.0, 6.0) - 30.0) < 1e-9
# Integral over the full window matches the full trapezoidal integral.
full = trapezoidal_energy_range(samples, 0.0, 10.0)
assert abs(full - 150.0) < 1e-9
def test_trapezoidal_range_zero_window() -> None:
"""Zero-length or reversed windows integrate to zero."""
from exo.utils.power_sampler import trapezoidal_energy_range
samples = [(0.0, _make_profile(10.0)), (5.0, _make_profile(20.0))]
assert trapezoidal_energy_range(samples, 3.0, 3.0) == 0.0
assert trapezoidal_energy_range(samples, 5.0, 3.0) == 0.0
def test_trapezoidal_range_splits_sum_to_full() -> None:
"""Energy split at an arbitrary boundary should sum back to the full integral."""
from exo.utils.power_sampler import (
trapezoidal_energy,
trapezoidal_energy_range,
)
samples = [
(0.0, _make_profile(10.0)),
(1.0, _make_profile(20.0)),
(3.0, _make_profile(15.0)),
(5.0, _make_profile(25.0)),
]
full = trapezoidal_energy(samples, elapsed=5.0)
# Split at t=2.5 (between samples) — interpolation should be exact.
left = trapezoidal_energy_range(samples, 0.0, 2.5)
right = trapezoidal_energy_range(samples, 2.5, 5.0)
assert abs((left + right) - full) < 1e-9
async def test_prefill_generation_split() -> None:
"""When mark_prefill_done() is called, the result should split energy."""
state: dict[NodeId, SystemPerformanceProfile] = {
NODE_A: _make_profile(10.0),
}
sampler = PowerSampler(get_node_system=lambda: state, interval=0.02)
async with anyio.create_task_group() as tg:
tg.start_soon(sampler.run)
# "Prefill" phase: power = 10 W
await anyio.sleep(0.1)
# Mark the boundary BEFORE changing state — this matches what
# _collect_text_generation_with_stats does in production: the mark
# fires on the first non-prefill chunk, so the boundary sample is
# the genuine end-of-prefill reading rather than the new phase's.
sampler.mark_prefill_done()
state[NODE_A] = _make_profile(30.0)
# "Generation" phase: power = 30 W
await anyio.sleep(0.1)
tg.cancel_scope.cancel()
result = sampler.result()
assert result.prefill_seconds is not None
assert result.generation_seconds is not None
assert result.prefill_energy_joules is not None
assert result.generation_energy_joules is not None
assert result.prefill_avg_sys_power_watts is not None
assert result.generation_avg_sys_power_watts is not None
# Phase durations should sum to the elapsed seconds.
assert (
abs(
(result.prefill_seconds + result.generation_seconds)
- result.elapsed_seconds
)
< 1e-6
)
# Phase energies should sum to (approximately) the total.
assert (
abs(
(result.prefill_energy_joules + result.generation_energy_joules)
- result.total_energy_joules
)
< 1e-6
)
# With the boundary sample anchored at the genuine end-of-prefill (10 W),
# prefill avg should converge tightly on 10 W and generation on 30 W.
# 15 W cleanly separates the two and would catch any cross-contamination.
assert result.prefill_avg_sys_power_watts < 15.0
assert result.generation_avg_sys_power_watts > 15.0
assert result.nodes[0].prefill_avg_sys_power is not None
assert result.nodes[0].generation_avg_sys_power is not None
async def test_no_split_when_unmarked() -> None:
"""If mark_prefill_done() is never called, phase fields stay None."""
state: dict[NodeId, SystemPerformanceProfile] = {
NODE_A: _make_profile(10.0),
}
sampler = PowerSampler(get_node_system=lambda: state, interval=0.02)
async with anyio.create_task_group() as tg:
tg.start_soon(sampler.run)
await anyio.sleep(0.05)
tg.cancel_scope.cancel()
result = sampler.result()
assert result.prefill_seconds is None
assert result.generation_seconds is None
assert result.prefill_energy_joules is None
assert result.generation_energy_joules is None
assert result.nodes[0].prefill_energy_joules is None
assert result.nodes[0].generation_energy_joules is None
async def test_mark_prefill_done_is_idempotent() -> None:
"""Only the first call to mark_prefill_done() should take effect."""
state: dict[NodeId, SystemPerformanceProfile] = {
NODE_A: _make_profile(10.0),
}
sampler = PowerSampler(get_node_system=lambda: state, interval=0.02)
async with anyio.create_task_group() as tg:
tg.start_soon(sampler.run)
await anyio.sleep(0.05)
sampler.mark_prefill_done()
first_prefill_at = sampler._prefill_done_at # pyright: ignore[reportPrivateUsage]
await anyio.sleep(0.05)
sampler.mark_prefill_done()
assert sampler._prefill_done_at == first_prefill_at # pyright: ignore[reportPrivateUsage]
tg.cancel_scope.cancel()
async def test_result_stops_sampling() -> None:
"""Calling result() should stop the sampler's run loop."""
state: dict[NodeId, SystemPerformanceProfile] = {
+2 -45
View File
@@ -229,47 +229,6 @@ def has_non_kv_caches(cache: KVCacheType) -> bool:
return any(is_non_trimmable_cache_entry(c) for c in cache)
# Max snapshots retained per cache entry. Each CacheSnapshot pins detached GPU
# copies of every non-trimmable (SSM/ArraysCache, RotatingKVCache) layer, so
# retaining one per ~4096-token prefill chunk makes snapshot memory grow linearly
# with context — the dominant residual cost when a single entry is grown to long
# contexts on hybrid models (~56 MB/snapshot on Qwen3.5-122B, so a full 256K
# context = 64 snapshots ≈ 3.6 GB). A sliding window of the most-recent N caps
# this at N×per-snapshot (~0.9 GB here) while preserving the restore points
# in-place grows actually use (they always extend from the tip).
_MAX_RETAINED_SNAPSHOTS = 16
def _bounded_snapshots(snapshots: list[CacheSnapshot]) -> list[CacheSnapshot]:
"""Deduplicate snapshots by token position and bound the retained count.
Returned list is sorted ascending by ``token_count``.
"""
# Deduplicate by position, keeping the most-recently-appended snapshot per
# position. Repeated in-place grows re-snapshot positions the kept old
# snapshots already cover, which would otherwise grow `_snapshots`
# unbounded even at constant context.
# TODO: keying on token_count alone is safe only while a position uniquely
# identifies the prefix within an entry (grows are strict prefix-extensions).
# If edit-and-regenerate, sliding-window/prefix trimming, cross-entry
# snapshot sharing, per-request adapter/LoRA swap, or branchy decoding
# (beam/parallel/speculative) is added, enrich the key to
# (token_count, prefix_hash[, media/adapter id]) — else a stale snapshot
# could be restored for a different prefix (silent wrong output).
by_position: dict[int, CacheSnapshot] = {}
for snapshot in snapshots:
by_position[snapshot.token_count] = snapshot
deduped = [by_position[pos] for pos in sorted(by_position)]
# Sliding window: keep only the most-recent N positions. In-place grows
# always extend from the tip, so the newest snapshots are the ones future
# grows restore from — dropping the oldest is never incorrect: a later hit on
# a prefix older than the window finds no snapshot <= target, so get_kv_cache
# returns a fresh cache (matched_index=None) and the request takes a full cold
# prefill — correct, just slower than a partial-hit reuse for that one request.
return deduped[-_MAX_RETAINED_SNAPSHOTS:]
class KVPrefixCache:
def __init__(self, group: mx.distributed.Group | None):
self.prompts: list[mx.array] = [] # mx array of tokens (ints)
@@ -302,9 +261,7 @@ class KVPrefixCache:
self._evict_if_needed()
self.prompts.append(prompt_tokens)
self.caches.append(deepcopy(cache))
self._snapshots.append(
_bounded_snapshots(ssm_snapshots) if ssm_snapshots else None
)
self._snapshots.append(ssm_snapshots)
self._media_regions.append(media_regions or [])
self.prefill_tps.append(prefill_tps)
self._access_counter += 1
@@ -331,7 +288,7 @@ class KVPrefixCache:
self.prompts[index] = prompt_tokens
self.caches[index] = deepcopy(cache)
self._snapshots[index] = _bounded_snapshots(merged) or None
self._snapshots[index] = merged or None
self._media_regions[index] = media_regions or []
self.prefill_tps[index] = prefill_tps
self._access_counter += 1
+68 -4
View File
@@ -1,3 +1,4 @@
from itertools import pairwise
import json
import os
import re
@@ -5,6 +6,7 @@ import sys
import tempfile
import time
from collections.abc import Generator
from math import isqrt
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast
@@ -786,13 +788,75 @@ def mlx_force_oom(size: int = 200000) -> None:
a = mx.random.uniform(shape=(size, size), dtype=mx.float32)
b = mx.random.uniform(shape=(size, size), dtype=mx.float32)
mx.eval(a, b)
c = mx.matmul(a, b)
d = mx.matmul(a, c)
e = mx.matmul(b, c)
f = mx.sigmoid(d + e)
c = mx.matmul(a, b) # (size,size)
d = mx.matmul(a, c) # (size,size)
e = mx.matmul(b, c) # (size,size)
f = mx.sigmoid(d + e) # (size,size)
mx.eval(f)
def mlx_force_oom2(bytes_alloc: int = 1024**5): # the default is 1 petabyte lol
"""
Force an Out-Of-Memory (OOM) error in MLX by performing large tensor operations.
NOTE: probably only works correctly on Apple unified memory
"""
# TODO: I give up. this either uses swap (inconsistently) and only sometimes OOMs
# or if I tune it to be more aggressive then it kerenel panics entirely;
# there MIGHT be a way to make it not use swap memory but I'm not determined enough
# to figure out how :)
def get_size(memory: int):
mat_elem = -(-memory // 4) # per-matrix elements (4 bytes per elem)
# square root to get size (round up if not integer)
root = isqrt(mat_elem)
return root if root**2 == mat_elem else root + 1
def oom(size: int):
mx.set_default_device(mx.gpu)
mx.clear_cache()
# allocate a lot
z = mx.zeros(shape=(size, size), dtype=mx.float32)
t1 = [mx.random.uniform(shape=(size, size), dtype=mx.float32) for _ in range(2)]
# mat mul cycle
t2: list[mx.array] = []
for m1, m2 in pairwise(t1):
t2.append(mx.matmul(m1, m2))
print("t2-run")
mx.eval(*t2)
print("t2-eval")
# sigmoid sum
f = mx.sigmoid(sum(t1, start=z) + sum(t2, start=z))
print("f-run")
mx.eval(f)
print("f-eval")
# use supplied size, or computer appropriate size otherwise
fail_num = 0
while True:
try:
print(f"size {bytes_alloc / 1024**3} GB")
oom(get_size(bytes_alloc))
break
except RuntimeError as e:
max_bytes = re.compile(
r"\[metal::malloc\] Attempting to allocate (?:\d+) bytes which is greater than the maximum allowed buffer size of (?P<max_bytes>\d+) bytes."
).match(str(e))
if max_bytes is None:
raise RuntimeError(
"Tried to get max buffer, but wrong error format"
) from e
bytes_alloc = round(int(max_bytes.group("max_bytes")) * 0.95**fail_num)
fail_num += 1
mlx_force_oom2()
def set_wired_limit_for_model(model_size: Memory):
"""
A context manager to temporarily change the wired limit.
+1 -7
View File
@@ -8,10 +8,6 @@ from loguru import logger
from exo.api.types import ImageEditsTaskParams
from exo.download.download_utils import is_read_only_model_dir, resolve_existing_model
from exo.routing.event_router import (
EventRouterBrokenResourceError,
EventRouterClosedResourceError,
)
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
@@ -113,9 +109,7 @@ class Worker:
tg.start_soon(self._event_applier)
tg.start_soon(self._poll_connection_updates)
tg.start_soon(self._reconcile_custom_cards)
except* (EventRouterBrokenResourceError, EventRouterClosedResourceError):
# Event router has been closed (try-star syntax handles error groups)
pass
finally:
# Actual shutdown code - waits for all tasks to complete before executing.
logger.info("Stopping Worker")
@@ -11,7 +11,6 @@ from mlx_lm.sample_utils import make_sampler
from exo.shared.types.common import ModelId
from exo.shared.types.text_generation import InputMessage, TextGenerationTaskParams
from exo.worker.engines.mlx.cache import (
CacheSnapshot,
KVPrefixCache,
cache_length,
encode_prompt,
@@ -78,74 +77,6 @@ class TestGetPrefixLength:
assert get_prefix_length(a, b) == 0
class TestSnapshotAccumulation:
"""Locks in the fix for the actual per-grow Metal leak on hybrid (SSM)
models: `update_kv_cache` must not let `_snapshots` grow without bound when
the same entry is grown in place many times."""
def test_repeated_update_does_not_accumulate_snapshots(self):
with patch(
"exo.worker.engines.mlx.cache.get_memory_used_percentage",
return_value=0.0,
):
kv_prefix_cache = KVPrefixCache(None)
initial = [
CacheSnapshot(states=[None], token_count=4096),
CacheSnapshot(states=[None], token_count=8192),
]
kv_prefix_cache.add_kv_cache(
mx.arange(10000), [KVCache()], ssm_snapshots=initial
)
# Each in-place grow re-prefills from restore_pos and produces a
# fresh snapshot at a position the retained old snapshots already
# cover. Pre-fix this appended one snapshot per grow forever.
for _ in range(50):
fresh = [CacheSnapshot(states=[None], token_count=8192)]
kv_prefix_cache.update_kv_cache(
0, mx.arange(10000), [KVCache()], fresh, restore_pos=8192
)
stored = kv_prefix_cache._snapshots[0]
assert stored is not None
# Bounded by the number of distinct snapshot positions (here 2),
# not by the 50 grows.
assert len(stored) == 2
assert sorted(s.token_count for s in stored) == [4096, 8192]
# The kept 8192 snapshot must be the most recently supplied one.
assert stored[1] is fresh[0]
def test_extension_caps_snapshots_to_sliding_window(self):
"""Extending a single entry to a long context (one snapshot per ~4096
tokens) must cap retained snapshots to a sliding window of the most-recent
N, not keep all of them that linear-in-context retention was the
residual OOM cause."""
from exo.worker.engines.mlx.cache import _MAX_RETAINED_SNAPSHOTS
with patch(
"exo.worker.engines.mlx.cache.get_memory_used_percentage",
return_value=0.0,
):
kv_prefix_cache = KVPrefixCache(None)
# 64 distinct positions = a 262144-token context at 4096/chunk.
num_positions = 64
snaps = [
CacheSnapshot(states=[None], token_count=4096 * (i + 1))
for i in range(num_positions)
]
kv_prefix_cache.add_kv_cache(
mx.arange(10), [KVCache()], ssm_snapshots=snaps
)
stored = kv_prefix_cache._snapshots[0]
assert stored is not None
# Capped at the window; the most-recent N positions are retained
# (in-place grows extend from the tip, so these are what get used).
assert len(stored) == _MAX_RETAINED_SNAPSHOTS
assert stored == snaps[-_MAX_RETAINED_SNAPSHOTS:]
assert stored[-1] is snaps[-1] # tip always kept
class TestKVPrefix:
@pytest.fixture
def mock_tokenizer(self):
+3 -27
View File
@@ -7,9 +7,6 @@ set -uo pipefail
HOST="${1:-localhost:52415}"
MODEL_ID="KevTheHermit/security-testing"
ENCODED_MODEL_ID=$(
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$MODEL_ID"
)
CUSTOM_CARDS_DIR="$HOME/.exo/custom_model_cards"
CARD_FILE="$CUSTOM_CARDS_DIR/KevTheHermit--security-testing.toml"
@@ -74,30 +71,9 @@ PLACE_BODY=$(echo "$PLACE_RESPONSE" | sed '$d')
echo " HTTP $PLACE_CODE"
echo " Response: $PLACE_BODY"
if [ "$PLACE_CODE" -ge 400 ]; then
echo " Placement failed; cannot trigger tokenizer loading."
exit 1
fi
# Step 3b: Wait for placement to materialize before inference.
# Step 3b: Send a chat completion to actually trigger tokenizer loading
echo ""
echo "[3b] Waiting for placed instance ..."
if ! AWAIT_RESPONSE=$(curl -fsS --max-time 65 \
"http://$HOST/instance/await?model_id=$ENCODED_MODEL_ID&timeout_seconds=60" |
awk '/^data: / { sub(/^data: /, ""); print; exit }'); then
echo " Timed out waiting for an instance for $MODEL_ID"
exit 1
fi
if ! printf '%s' "$AWAIT_RESPONSE" | grep -q '"type":"ready"'; then
echo " Timed out waiting for an instance for $MODEL_ID"
exit 1
fi
echo " Instance ready"
# Step 3c: Send a chat completion to actually trigger tokenizer loading
echo ""
echo "[3c] Sending chat completion to trigger tokenizer load ..."
echo "[3b] Sending chat completion to trigger tokenizer load ..."
CHAT_RESPONSE=$(curl -s -w "\n%{http_code}" --max-time 30 -X POST "http://$HOST/v1/chat/completions" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL_ID\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}],\"max_tokens\":1}")
@@ -106,7 +82,7 @@ CHAT_BODY=$(echo "$CHAT_RESPONSE" | sed '$d')
echo " HTTP $CHAT_CODE"
echo " Response: $CHAT_BODY"
echo ""
echo "[3d] Checking for RCE proof ..."
echo "[3c] Checking for RCE proof ..."
sleep 5
if [ -f /tmp/exo-rce-proof.txt ]; then
echo " VULNERABLE: Remote code executed!"
Generated
+16 -39
View File
@@ -24,7 +24,7 @@ prerelease-mode = "allow"
members = [
"exo",
"exo-bench",
"exo-rs",
"exo-pyo3-bindings",
"exo-tools",
]
overrides = [{ name = "opencv-python", marker = "python_full_version < '0'" }]
@@ -423,7 +423,7 @@ dependencies = [
{ name = "aiofiles", 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 = "aiohttp", 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 = "anyio", 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 = "exo-rs", 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 = "exo-pyo3-bindings", 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 = "filelock", 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 = "httpx", 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')" },
@@ -434,7 +434,6 @@ dependencies = [
{ name = "openai-harmony", 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 = "psutil", 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 = "pydantic", 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 = "python-daemon", 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 = "python-multipart", 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 = "rustworkx", 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 = "tiktoken", 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')" },
@@ -450,7 +449,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 +460,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 +472,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 +486,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')" },
@@ -523,7 +522,7 @@ requires-dist = [
{ name = "exo", extras = ["mlx"], marker = "extra == 'mlx-cpu'" },
{ name = "exo", extras = ["mlx"], marker = "extra == 'mlx-cuda12'" },
{ name = "exo", extras = ["mlx"], marker = "extra == 'mlx-cuda13'" },
{ name = "exo-rs", editable = "rust/exo_rs" },
{ name = "exo-pyo3-bindings", editable = "rust/exo_pyo3_bindings" },
{ name = "fastapi", specifier = ">=0.116.1" },
{ name = "filelock", specifier = ">=3.18.0" },
{ name = "httpx", specifier = ">=0.28.1" },
@@ -549,7 +548,6 @@ requires-dist = [
{ name = "openai-harmony", specifier = ">=0.0.8" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "pydantic", specifier = ">=2.11.7" },
{ name = "python-daemon", specifier = ">=3.1.2" },
{ name = "python-multipart", specifier = ">=0.0.21" },
{ name = "rustworkx", specifier = ">=0.17.1" },
{ name = "tiktoken", specifier = ">=0.12.0" },
@@ -622,13 +620,13 @@ requires-dist = [
]
[[package]]
name = "exo-rs"
version = "0.2.16"
source = { editable = "rust/exo_rs" }
name = "exo-pyo3-bindings"
version = "0.2.2"
source = { editable = "rust/exo_pyo3_bindings" }
[package.dev-dependencies]
dev = [
{ name = "exo-rs", 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 = "exo-pyo3-bindings", 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 = "pytest", 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 = "pytest-asyncio", 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')" },
]
@@ -637,7 +635,7 @@ dev = [
[package.metadata.requires-dev]
dev = [
{ name = "exo-rs", editable = "rust/exo_rs" },
{ name = "exo-pyo3-bindings", editable = "rust/exo_pyo3_bindings" },
{ name = "pytest", specifier = ">=8.4.0" },
{ name = "pytest-asyncio", specifier = ">=1.0.0" },
]
@@ -1164,15 +1162,6 @@ math = [
{ name = "sympy", 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')" },
]
[[package]]
name = "lockfile"
version = "0.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/17/47/72cb04a58a35ec495f96984dddb48232b551aafb95bde614605b754fe6f7/lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799", size = 20874, upload-time = "2015-11-25T18:29:58.279Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa", size = 13564, upload-time = "2015-11-25T18:29:51.462Z" },
]
[[package]]
name = "loguru"
version = "0.7.3"
@@ -1343,7 +1332,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 +1363,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 +1538,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 +1554,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')" },
@@ -2311,18 +2300,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl", hash = "sha256:d7e5b7198f9b83c795377c09feefa45d56083834e60d04767efd64819fc9da00", size = 6251, upload-time = "2025-10-09T19:15:46.077Z" },
]
[[package]]
name = "python-daemon"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "lockfile", 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')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/37/4f10e37bdabc058a32989da2daf29e57dc59dbc5395497f3d36d5f5e2694/python_daemon-3.1.2.tar.gz", hash = "sha256:f7b04335adc473de877f5117e26d5f1142f4c9f7cd765408f0877757be5afbf4", size = 71576, upload-time = "2024-12-03T08:41:07.843Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/3c/b88167e2d6785c0e781ee5d498b07472aeb9b6765da3b19e7cc9e0813841/python_daemon-3.1.2-py3-none-any.whl", hash = "sha256:b906833cef63502994ad48e2eab213259ed9bb18d54fa8774dcba2ff7864cec6", size = 30872, upload-time = "2024-12-03T08:41:03.322Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"