mirror of
https://github.com/exo-explore/exo.git
synced 2026-09-08 19:41:32 -04:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f9d000e5 | ||
|
|
551e94ed38 | ||
|
|
b0834c8a2e | ||
|
|
d545a4ea25 | ||
|
|
0e7721ba0e | ||
|
|
abb4402036 | ||
|
|
e8b9c808a6 | ||
|
|
cf00cd83ed | ||
|
|
41ff366b1d | ||
|
|
303600616a | ||
|
|
51d9912fcf | ||
|
|
d5da78b39b | ||
|
|
88027475a5 | ||
|
|
423dc407f8 | ||
|
|
f62b3b09d0 | ||
|
|
94424d92c3 | ||
|
|
e029a1ea65 | ||
|
|
49e5350b17 | ||
|
|
feaf05f82d | ||
|
|
280031d431 | ||
|
|
58551f2c32 | ||
|
|
cecc37085a | ||
|
|
a73be3c2bb | ||
|
|
acec94316d | ||
|
|
5c7068387d | ||
|
|
d62b333405 | ||
|
|
eeedc6c17a | ||
|
|
2d11b6520f | ||
|
|
42b26af63b | ||
|
|
1649708ccb | ||
|
|
fbe962b08e | ||
|
|
38258fb5f3 | ||
|
|
86398a2bd1 | ||
|
|
bab245759d | ||
|
|
2f506167e0 | ||
|
|
88d4c37bfa | ||
|
|
9ec09ec59d | ||
|
|
cd22516186 | ||
|
|
ddc6e95e68 | ||
|
|
feef63b98c | ||
|
|
3611c33b61 | ||
|
|
547b58bf48 | ||
|
|
36b1b82b48 | ||
|
|
b1d09b026d | ||
|
|
faafc6f4ad | ||
|
|
2969b7d4c6 | ||
|
|
6c13b4550f | ||
|
|
3fc56b2da5 | ||
|
|
9c4cc084db | ||
|
|
49d7555ea6 | ||
|
|
32155bdf00 | ||
|
|
163ef39bbb | ||
|
|
9636e86855 | ||
|
|
4ea2532dd6 | ||
|
|
1d17f4de18 | ||
|
|
1c7f514ca3 | ||
|
|
c3aa1f47fc | ||
|
|
9686c02251 | ||
|
|
2f9ba7d1de | ||
|
|
ab172322c5 | ||
|
|
278bea11ad | ||
|
|
dbd5ca00eb | ||
|
|
197cbf4f9d | ||
|
|
d58ab113d1 | ||
|
|
9c7908f247 | ||
|
|
ad2c1e7c66 |
No files matched your search
@@ -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
|
||||
@@ -38,8 +38,6 @@ bench/**/*.json
|
||||
# tmp
|
||||
tmp/models
|
||||
/build/exo
|
||||
/.agents
|
||||
/.claude/skills
|
||||
/.claude
|
||||
/.codex
|
||||
skills-lock.json
|
||||
Generated
+712
-810
File diff suppressed because it is too large.
Load diff
+1
-2
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
File renamed without changes.
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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] = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user