diff --git a/dashboard/src/routes/downloads/+page.svelte b/dashboard/src/routes/downloads/+page.svelte
index ca3dc129..8086c139 100644
--- a/dashboard/src/routes/downloads/+page.svelte
+++ b/dashboard/src/routes/downloads/+page.svelte
@@ -225,6 +225,11 @@
let configPolicy = $state<"manual" | "auto-evict">("manual");
let configSaving = $state(false);
let configApplyAll = $state(false);
+ let configDiskTotalGb = $derived(
+ storageConfigNode
+ ? Math.round((storageConfigNode.diskTotal ?? 0) / 1024 ** 3)
+ : 0,
+ );
function openStorageConfig(col: NodeColumn) {
storageConfigNode = col;
@@ -1074,31 +1079,52 @@
{
+ if (!configNoLimit && configMaxGb == null) {
+ configMaxGb = configDiskTotalGb || 50;
+ }
+ }}
class="accent-exo-yellow w-4 h-4"
/>
Unlimited storage
-
-
-
+
+
+
+
+
+ {configMaxGb ?? 0} GB
+
+
+
+ 1 GB
+ {Math.max(configDiskTotalGb, configMaxGb ?? 1)} GB
+
@@ -1167,4 +1193,30 @@
table {
min-width: max-content;
}
+
+ .slider {
+ background: rgba(255, 255, 255, 0.1);
+ }
+ .slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #f5c518;
+ cursor: pointer;
+ }
+ .slider::-moz-range-thumb {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ border: none;
+ background: #f5c518;
+ cursor: pointer;
+ }
+ .slider:disabled::-webkit-slider-thumb {
+ cursor: not-allowed;
+ }
+ .slider:disabled::-moz-range-thumb {
+ cursor: not-allowed;
+ }
diff --git a/src/exo/master/placement.py b/src/exo/master/placement.py
index c8a7f797..aaad8c9b 100644
--- a/src/exo/master/placement.py
+++ b/src/exo/master/placement.py
@@ -32,10 +32,11 @@ from exo.shared.types.memory import Memory
from exo.shared.types.profiling import MemoryUsage, NodeNetworkInfo
from exo.shared.types.tasks import Task, TaskId, TaskStatus
from exo.shared.types.worker.downloads import (
- ModelModelDownloadFailed,
+ ModelDownloadFailed,
ModelDownloading,
ModelNotDownloading,
ModelReady,
+ ModelRejected,
ModelStatus,
)
from exo.shared.types.worker.instances import (
@@ -87,6 +88,8 @@ def _get_node_download_fraction(
return progress.downloaded.in_bytes / total if total > 0 else 0.0
case ModelDownloadFailed():
return 0.0
+ case ModelRejected():
+ return 0.0
return 0.0
diff --git a/src/exo/master/tests/test_placement.py b/src/exo/master/tests/test_placement.py
index 3a8a36c0..bf01cbcb 100644
--- a/src/exo/master/tests/test_placement.py
+++ b/src/exo/master/tests/test_placement.py
@@ -26,10 +26,10 @@ from exo.shared.types.tasks import TaskId, TaskStatus, TextGeneration
from exo.shared.types.text_generation import InputMessage, TextGenerationTaskParams
from exo.shared.types.topology import Connection, SocketConnection
from exo.shared.types.worker.downloads import (
- DownloadCompleted,
- DownloadFailed,
- DownloadOngoing,
DownloadProgressData,
+ ModelDownloadFailed,
+ ModelDownloading,
+ ModelReady,
)
from exo.shared.types.worker.instances import (
Instance,
@@ -624,7 +624,7 @@ def test_placement_prefers_cycle_with_downloaded_model(
# node_b has the model fully downloaded, node_a does not
download_status = {
node_b: [
- DownloadCompleted(
+ ModelReady(
node_id=node_b,
shard_metadata=shard_meta,
total=model_card.storage_size,
@@ -671,7 +671,7 @@ def test_placement_prefers_cycle_with_higher_download_progress(
# node_a: 30% downloaded, node_b: 80% downloaded
download_status = {
node_a: [
- DownloadOngoing(
+ ModelDownloading(
node_id=node_a,
shard_metadata=shard_meta,
download_progress=DownloadProgressData(
@@ -687,7 +687,7 @@ def test_placement_prefers_cycle_with_higher_download_progress(
),
],
node_b: [
- DownloadOngoing(
+ ModelDownloading(
node_id=node_b,
shard_metadata=shard_meta,
download_progress=DownloadProgressData(
@@ -744,7 +744,7 @@ def test_placement_does_not_prefer_cycle_with_failed_download(
# node_b has a failed download — should not be preferred
download_status = {
node_b: [
- DownloadFailed(
+ ModelDownloadFailed(
node_id=node_b,
shard_metadata=shard_meta,
error_message="connection reset",