diff --git a/README.md b/README.md index cdb7c5f063..2e84ac43a5 100644 --- a/README.md +++ b/README.md @@ -538,6 +538,13 @@ The `exo-bench` tool measures model prefill and token generation speed across di - Nodes should be running with `uv run exo` before benchmarking - The tool uses the `/bench/chat/completions` endpoint +For Ring Attention on Metal, start every node with MLX's low-latency CPU/GPU +synchronization enabled: + +```bash +MLX_METAL_FAST_SYNCH=1 uv run exo +``` + **Basic usage:** ```bash @@ -554,7 +561,7 @@ uv run bench/exo_bench.py \ - `--tg`: Generation lengths (comma-separated integers) - `--max-nodes`: Limit placements to N nodes (default: 4) - `--instance-meta`: Filter by `ring`, `jaccl`, or `both` (default: both) -- `--sharding`: Filter by `pipeline`, `tensor`, or `both` (default: both) +- `--sharding`: Filter by `pipeline`, `tensor`, `ring`, or `both` (default: both; `both` retains the pipeline/tensor comparison) - `--repeat`: Number of repetitions per configuration (default: 1) - `--warmup`: Warmup runs per placement (default: 0) - `--json-out`: Output file for results (default: bench/results.json) @@ -572,6 +579,29 @@ uv run bench/exo_bench.py \ --json-out my-results.json ``` +To compare Ring Attention against replicated pipeline prefill on the same +Ring-compatible model and node count, run both placements with identical prompt +lengths, generation lengths, repetitions, and warmups: + +```bash +uv run bench/exo_bench.py \ + --model Llama-3.2-1B-Instruct-4bit \ + --pp 4096,16384,65536 --tg 128,128,128 \ + --min-nodes 2 --max-nodes 2 --instance-meta ring \ + --sharding ring --warmup 1 --repeat 3 \ + --json-out bench/ring-attention.json + +uv run bench/exo_bench.py \ + --model Llama-3.2-1B-Instruct-4bit \ + --pp 4096,16384,65536 --tg 128,128,128 \ + --min-nodes 2 --max-nodes 2 --instance-meta ring \ + --sharding pipeline --warmup 1 --repeat 3 \ + --json-out bench/replicated-pipeline.json +``` + +Use `prompt_tps` as the primary Ring prefill metric. Keep prefix caching disabled +(the default), and record node hardware and network topology alongside results. + The tool outputs performance metrics including prompt tokens per second (prompt_tps), generation tokens per second (generation_tps), and peak memory usage for each configuration. --- diff --git a/dashboard/src/lib/components/ModelCard.svelte b/dashboard/src/lib/components/ModelCard.svelte index 842871587b..d5b10a93c8 100644 --- a/dashboard/src/lib/components/ModelCard.svelte +++ b/dashboard/src/lib/components/ModelCard.svelte @@ -22,7 +22,7 @@ }>; } | null; nodes?: Record; - sharding?: "Pipeline" | "Tensor"; + sharding?: "Pipeline" | "Tensor" | "Ring"; runtime?: "MlxRing" | "MlxJaccl"; onLaunch?: () => void; tags?: string[]; diff --git a/dashboard/src/lib/stores/app.svelte.ts b/dashboard/src/lib/stores/app.svelte.ts index a717c3afbe..cb45cd72a1 100644 --- a/dashboard/src/lib/stores/app.svelte.ts +++ b/dashboard/src/lib/stores/app.svelte.ts @@ -173,7 +173,7 @@ export interface ModelDownloadStatus { // Placement preview from the API export interface PlacementPreview { model_id: string; - sharding: "Pipeline" | "Tensor"; + sharding: "Pipeline" | "Tensor" | "Ring"; instance_meta: "MlxRing" | "MlxJaccl"; instance: unknown | null; memory_delta_by_node: Record | null; diff --git a/dashboard/src/routes/+page.svelte b/dashboard/src/routes/+page.svelte index 5db620795e..f5f577ae2f 100644 --- a/dashboard/src/routes/+page.svelte +++ b/dashboard/src/routes/+page.svelte @@ -783,6 +783,7 @@ quantization?: string; base_model?: string; capabilities?: string[]; + supports_ring?: boolean; }> >([]); type ModelMemoryFitStatus = @@ -885,14 +886,14 @@ sendMessage(content, files, thinkingEnabled()); } - let selectedSharding = $state<"Pipeline" | "Tensor">("Pipeline"); + let selectedSharding = $state<"Pipeline" | "Tensor" | "Ring">("Pipeline"); type InstanceMeta = "MlxRing" | "MlxJaccl"; // Launch defaults persistence const LAUNCH_DEFAULTS_KEY = "exo-launch-defaults-v2"; interface LaunchDefaults { modelId: string | null; - sharding: "Pipeline" | "Tensor"; + sharding: "Pipeline" | "Tensor" | "Ring"; instanceType: InstanceMeta; minNodes: number; } @@ -932,7 +933,9 @@ // Apply sharding and instance type unconditionally selectedSharding = defaults.sharding; selectedInstanceType = - defaults.instanceType === "MlxRing" ? "MlxRing" : "MlxJaccl"; + defaults.sharding === "Ring" || defaults.instanceType === "MlxRing" + ? "MlxRing" + : "MlxJaccl"; // Apply minNodes if valid (between 1 and maxNodes) if ( @@ -954,6 +957,20 @@ } let selectedInstanceType = $state("MlxRing"); + + const selectedModelSupportsRing = $derived( + models.find( + (model) => + model.id === selectedModelId || + model.hugging_face_id === selectedModelId, + )?.supports_ring === true, + ); + + $effect(() => { + if (selectedSharding === "Ring" && !selectedModelSupportsRing) { + selectedSharding = "Pipeline"; + } + }); let selectedMinNodes = $state(1); let minNodesInitialized = $state(false); let launchingModelId = $state(null); @@ -2043,7 +2060,7 @@ return inst.shardAssignments?.modelId || "Unknown Model"; } - // Get instance details: type (MLX Ring/IBV), sharding (Pipeline/Tensor), and node names + // Get instance details: type (MLX Ring/IBV), sharding strategy, and node names function getInstanceInfo(instanceWrapped: unknown): { instanceType: string; sharding: string; @@ -2082,6 +2099,7 @@ const [shardTag] = getTagged(firstShardWrapped); if (shardTag === "PipelineShardMetadata") sharding = "Pipeline"; else if (shardTag === "TensorShardMetadata") sharding = "Tensor"; + else if (shardTag === "RingShardMetadata") sharding = "Ring"; else if (shardTag === "PrefillDecodeShardMetadata") sharding = "Prefill/Decode"; } @@ -2193,7 +2211,7 @@ function getOrderedRunnerNodes( instance: Record, - shardType: "Pipeline" | "Tensor", + shardType: "Pipeline" | "Tensor" | "Ring", ) { const runnerToShard = ( @@ -5772,6 +5790,37 @@ Tensor + @@ -5807,6 +5856,9 @@