-
-
Notifications
You must be signed in to change notification settings - Fork 35
feat(scheduler): GPU arbiter — VRAM-accounted admission + queue + eviction #1683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a32ea2b
9714de0
1277abe
74b1185
d51641a
6a8f8f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,7 +57,7 @@ async def get_response(self, path, scope): | |
| from tinyagentos.backend_adapters import check_backend_health | ||
| from tinyagentos.benchmark import BenchmarkStore | ||
| from tinyagentos.installation_state import InstallationState | ||
| from tinyagentos.scheduler import BackendCatalog, HistoryStore, ScoreCache, TaskScheduler | ||
| from tinyagentos.scheduler import BackendCatalog, GpuArbiter, HistoryStore, ScoreCache, TaskScheduler | ||
| from tinyagentos.scheduler.discovery import build_scheduler as build_resource_scheduler | ||
| from tinyagentos.torrent_settings import TorrentSettingsStore | ||
| from tinyagentos.relationships import RelationshipManager | ||
|
|
@@ -1124,6 +1124,7 @@ async def _reload_llm_proxy_on_catalog_change() -> None: | |
| # Build the resource scheduler from hardware profile + live catalog. | ||
| # Phase 1: local resources only (NPU + CPU), capability-based routing | ||
| # with fallback and priority. Cluster-aware dispatch is Phase 3. | ||
| resource_scheduler = None | ||
| try: | ||
| resource_scheduler = build_resource_scheduler( | ||
| hardware_profile, | ||
|
|
@@ -1140,6 +1141,23 @@ async def _reload_llm_proxy_on_catalog_change() -> None: | |
| except Exception: | ||
| logger.exception("resource scheduler failed to build — routes will use static config") | ||
| app.state.resource_scheduler = None | ||
|
|
||
| # Build the GPU arbiter — wraps the resource scheduler with VRAM-accounted | ||
| # admission control, queuing, and eviction for GPU-bound workloads. | ||
| try: | ||
| gpu_arbiter = GpuArbiter( | ||
| scheduler=resource_scheduler if resource_scheduler is not None else None, | ||
| cluster_manager=cluster_manager, | ||
| max_queue_size=100, | ||
| eviction_enabled=True, | ||
| ) | ||
| await gpu_arbiter.start() | ||
| app.state.gpu_arbiter = gpu_arbiter | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Set
🤖 Prompt for AI Agents |
||
| cluster_manager._gpu_arbiter = gpu_arbiter # wire for eviction paths | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Reply with |
||
| logger.info("GPU arbiter ready (queue size=100, eviction=enabled)") | ||
| except Exception: | ||
| logger.exception("GPU arbiter failed to start — GPU tasks will use vanilla scheduler") | ||
| app.state.gpu_arbiter = None | ||
|
Comment on lines
+1144
to
+1160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add a stop in the shutdown path: 🔒 Proposed shutdown cleanup_gpu_arbiter = getattr(app.state, "gpu_arbiter", None)
if _gpu_arbiter is not None:
try:
await _gpu_arbiter.stop()
except Exception:
logger.exception("gpu arbiter stop failed")🤖 Prompt for AI Agents |
||
| # Detect and set container runtime | ||
| from tinyagentos.containers.backend import configure_container_runtime | ||
| configure_container_runtime(config) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
|
|
||
| from tinyagentos.scheduler.backend_catalog import BackendCatalog | ||
| from tinyagentos.scheduler.history_store import HistoryStore | ||
| from tinyagentos.scheduler.gpu_arbiter import _probe_nvidia_vram | ||
| from tinyagentos.scheduler.resource import Resource, Tier | ||
| from tinyagentos.scheduler.scheduler import Scheduler | ||
| from tinyagentos.scheduler.score_cache import ScoreCache | ||
|
|
@@ -51,6 +52,20 @@ | |
| "vision", | ||
| } | ||
|
|
||
| # Every capability a GPU can run given the right backend. GPU is the | ||
| # fastest tier; the potential set mirrors CPU/NPU because any inference | ||
| # task that can run on CPU can also run (faster) on GPU. Live capabilities | ||
| # are still filtered to what backends actually have loaded right now. | ||
| GPU_POTENTIAL_CAPABILITIES: set[str] = { | ||
| "llm-chat", | ||
| "embedding", | ||
| "reranking", | ||
| "image-generation", | ||
| "speech-to-text", | ||
| "text-to-speech", | ||
| "vision", | ||
| } | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
|
|
@@ -172,6 +187,70 @@ def _npu_backend_for(capability: str) -> Optional[str]: | |
| ) | ||
| ) | ||
|
|
||
| # GPU (CUDA/ROCm/Vulkan/Metal), only if a healthy GPU-capable backend exists. | ||
| # GPU backends: vllm, llama-cpp (when built with CUDA), ollama (GPU mode), | ||
| # exo, mlx (Apple Silicon GPU). Also sd-cpp/sd-gpu for image-generation. | ||
| gpu_backend_types = {"vllm", "ollama", "exo", "mlx"} | ||
| gpu_backends = [ | ||
| b for b in catalog.backends() | ||
| if b.status == "ok" and b.type in gpu_backend_types | ||
| ] | ||
| gpu_info = getattr(hardware_profile, "gpu", None) | ||
| gpu_type = getattr(gpu_info, "type", None) if gpu_info else None | ||
| has_gpu_hardware = gpu_type not in (None, "", "none") | ||
|
|
||
| if gpu_backends or has_gpu_hardware: | ||
| gpu_count = 1 # Default single-GPU; multi-GPU is Phase 2 | ||
| gpu_signature = ResourceSignature( | ||
| platform="cuda" if gpu_type in ("cuda", "nvidia") else ( | ||
| "rocm" if gpu_type == "rocm" else ( | ||
| "metal" if gpu_type == "apple" else "gpu" | ||
| ) | ||
| ), | ||
| runtime="cuda" if gpu_type in ("cuda", "nvidia") else ( | ||
| "rocm" if gpu_type == "rocm" else "native" | ||
| ), | ||
| runtime_version="", | ||
| ) | ||
|
|
||
| def _gpu_vram_probe() -> int: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Reply with |
||
| free, _total = _probe_nvidia_vram() | ||
| return free if free > 0 else 999_999 # optimistic | ||
|
Comment on lines
+216
to
+218
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path('tinyagentos/scheduler/discovery.py')
text = path.read_text()
lines = text.splitlines()
for start, end in [(1, 260)]:
print(f'লines {start}-{min(end, len(lines))}')
for i in range(start-1, min(end, len(lines))):
print(f"{i+1:4d}: {lines[i]}")
PYRepository: jaylfc/taOS Length of output: 11510 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect symbols around the GPU probe helpers and any resource registration logic.
python3 - <<'PY'
from pathlib import Path
path = Path('tinyagentos/scheduler/discovery.py')
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
if 'def _gpu_vram_probe' in line or '_probe_nvidia_vram' in line or 'Resource' in line or 'vram' in line:
start = max(1, i-25)
end = min(len(lines), i+40)
print(f"\n--- context around line {i} ---")
for j in range(start, end+1):
print(f"{j:4d}: {lines[j-1]}")
PYRepository: jaylfc/taOS Length of output: 48442 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Search for the NVIDIA VRAM probe implementation and any documentation/comments
# describing what 0 or unknown means.
rg -n "_probe_nvidia_vram|gpu_vram_probe|vram" tinyagentos -SRepository: jaylfc/taOS Length of output: 24249 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Look for tests or docs around GPU resource discovery semantics.
rg -n "999_999|unknown|probe.*gpu|free.*vram|total.*vram|nvidia" . -SRepository: jaylfc/taOS Length of output: 50368 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Read only the relevant file sections with line numbers.
python3 - <<'PY'
from pathlib import Path
path = Path('tinyagentos/scheduler/discovery.py')
lines = path.read_text().splitlines()
for start, end in [(1, 260)]:
for i in range(start-1, min(end, len(lines))):
print(f"{i+1:4d}: {lines[i]}")
PYRepository: jaylfc/taOS Length of output: 11498 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path('tinyagentos/scheduler/discovery.py')
lines = path.read_text().splitlines()
targets = ['def _probe_nvidia_vram', 'def _gpu_vram_probe', 'Resource(', 'vram']
for idx, line in enumerate(lines, 1):
if any(t in line for t in targets):
start = max(1, idx-20)
end = min(len(lines), idx+35)
print(f"\n### around line {idx}")
for j in range(start, end+1):
print(f"{j:4d}: {lines[j-1]}")
PYRepository: jaylfc/taOS Length of output: 16576 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the NVIDIA VRAM probe and how Resource.memory_probe is used.
python3 - <<'PY'
from pathlib import Path
for file_path, ranges in [
('tinyagentos/scheduler/gpu_arbiter.py', [(1, 90), (140, 180)]),
('tinyagentos/scheduler/resource.py', [(1, 260)]),
]:
path = Path(file_path)
lines = path.read_text().splitlines()
print(f"\n### {file_path} ({len(lines)} lines)")
for start, end in ranges:
print(f"\n--- lines {start}-{min(end, len(lines))} ---")
for i in range(start-1, min(end, len(lines))):
print(f"{i+1:4d}: {lines[i]}")
PYRepository: jaylfc/taOS Length of output: 14635 Separate probe failure from real 0-free VRAM. 🤖 Prompt for AI Agents |
||
|
|
||
| def _gpu_capabilities() -> set[str]: | ||
| caps: set[str] = set() | ||
| for b in catalog.backends(): | ||
| if b.status == "ok" and b.type in gpu_backend_types: | ||
| caps |= b.capabilities | ||
| return caps | ||
|
|
||
| def _gpu_backend_for(capability: str): | ||
| for b in catalog.backends_with_capability(capability): | ||
| if b.type in gpu_backend_types: | ||
| return b.url | ||
| return None | ||
|
Comment on lines
+190
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
The comment (Lines 190-192) and the PR objective both call out Confirm whether the image-gen backend types should be included here. 🤖 Prompt for AI Agents |
||
|
|
||
| for gpu_idx in range(gpu_count): | ||
| gpu_name = f"gpu-cuda-{gpu_idx}" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: The GPU resource is registered as Reply with |
||
| scheduler.register( | ||
| Resource( | ||
| name=gpu_name, | ||
| signature=gpu_signature, | ||
| concurrency=1, | ||
| tier=Tier.GPU, | ||
| potential_capabilities=GPU_POTENTIAL_CAPABILITIES, | ||
| get_capabilities=_gpu_capabilities, | ||
| backend_lookup=_gpu_backend_for, | ||
| score_lookup=_make_score_lookup(gpu_name), | ||
| memory_probe=_gpu_vram_probe, | ||
| ) | ||
| ) | ||
| logger.info( | ||
| "discovery: registered GPU resource %s (%s, concurrency=1)", | ||
| gpu_name, | ||
| gpu_signature.platform, | ||
| ) | ||
|
|
||
| # CPU inference, always register. Backend-driven: only advertises the | ||
| # capabilities that some CPU backend currently serves (sd-cpp, llama-cpp, etc.) | ||
| cpu_signature = ResourceSignature( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING:
GpuArbiteris constructed withscheduler=resource_scheduler if resource_scheduler is not None else Noneandcluster_manager=cluster_manager, but novram_probeis passed. With the default_default_vram_probereturning(0, 0),_check_admissionwill always treat local VRAM as unavailable and fall through to the cluster-leases branch (or blindly admit if there is no cluster manager). That means on a single-node install with a real GPU, the arbiter cannot enforce VRAM admission at all — defeating the stated purpose of preventing Xid 62 crashes. Wire_probe_nvidia_vram(or the Resource'smemory_probefor the GPU resource) into the arbiter when GPU hardware is present.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.