Five workstreams that finish the remote-GPU line, plus the test hole that let a broken signature reach a commit. **Downloads go through the normal path** (Phase 5). Rather than a second remote-only route, the existing Models install flow became target-aware, so a model landing on a worker uses the same code, the same progress events and the same UI as a local one. Progress rows key on (target, repo_id) — the aggregator keyed on bare repo_id, so the same model downloading here and on a worker at once collapsed into one row that told the user nothing true about either. **Audiobooks render chapter by chapter on the worker** (Phase 8), with per-chapter local fallback and ONE aggregated notice. The failure that shape exists to prevent: a remote GPU that sleeps at chapter 40 of 200 must not turn a working book into 160 rows of PROGRESS_LEASE_EXPIRED. Dictation is deliberately NOT ported — it runs ASR per utterance inside a live WebSocket loop, and paying queue admission plus a round trip there would spend the one thing that route is for. **Dubbing stays local, and says so** (Phase 7). The coarse worker operation is not finished, so the picker still reports dubbing as local rather than showing a green remote chip over work this machine is doing. What could not wait is the in-loop OOM retry: it sniffed the error string and flushed the *local* CUDA cache, which under remote execution is the wrong machine's GPU entirely. That is fixed now, before the path that would have exercised it exists. **Two instances can no longer share the control plane.** A second VoiceStudio silently bound the same worker port and coexisted, so remote workers landed on whichever process won the race — a session that registers with one instance and appears dead to the other. This produced hours of misdiagnosis during hardware testing and would hit any user with the app open twice. The second instance now keeps running locally and explains the conflict instead of quietly competing. **And the hole that allowed all this to be missable.** gpu_gateway called Scheduler.submit(pinned_worker_id=...) one commit before that parameter existed. Every remote generation raised TypeError; 5236 tests passed anyway, because nothing exercised the gateway against the real scheduler. tests/test_gpu_gateway_scheduler_contract.py now runs that path for real and binds every gateway→dependency call signature. Verified by renaming the parameter away and watching both tests fail with the original error. Gallery previews also fall back to a local render when a downloaded clip cannot be decoded, rather than yielding silence. Backend 5274 passed, frontend 1812 passed. Not yet verified on hardware: Phases 4, 5, 6, 7, 8. Only the TTS path and its artifact transport have been proven on a real GPU.
125 lines
3.9 KiB
Python
125 lines
3.9 KiB
Python
"""Integration coverage for the GPU gateway's scheduler contract."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remote_run_uses_real_scheduler_contract(tmp_path):
|
|
# Import inside the test: tests/backend/conftest.py deliberately reloads
|
|
# services.* and worker dependencies between tests.
|
|
from services import gpu_gateway
|
|
from worker.lifecycle import TaskState
|
|
from worker.pool import WorkerPool
|
|
from worker.routing import Decision
|
|
from worker.scheduler import Scheduler
|
|
|
|
result_path = tmp_path / "result.bin"
|
|
result_path.write_bytes(b"remote result")
|
|
scheduler = Scheduler(WorkerPool(), persist=False)
|
|
submitted = []
|
|
|
|
# This listener stands in for the worker transport. Submission, lookup,
|
|
# waiting, and the Task object all remain the real scheduler implementation.
|
|
def complete_from_transport(event, task):
|
|
if event == "queued":
|
|
submitted.append(task)
|
|
task.state = TaskState.COMPLETED
|
|
task.result_ref = str(result_path)
|
|
|
|
scheduler.on_change(complete_from_transport)
|
|
|
|
class Plane:
|
|
running = True
|
|
pool = None
|
|
|
|
def __init__(self):
|
|
self.scheduler = scheduler
|
|
|
|
local_called = False
|
|
|
|
def local():
|
|
nonlocal local_called
|
|
local_called = True
|
|
return b"local result"
|
|
|
|
decision = Decision(
|
|
remote=True,
|
|
worker_id="worker-1",
|
|
label="Test worker",
|
|
reason="chosen",
|
|
)
|
|
result = await gpu_gateway.run(
|
|
"tts",
|
|
local=gpu_gateway.LocalCall(local),
|
|
remote=gpu_gateway.RemoteCall(
|
|
engine="test-engine",
|
|
model_id="test:model",
|
|
deadline_seconds=1,
|
|
decode=lambda remote_result: remote_result.read(),
|
|
),
|
|
decision=decision,
|
|
control_plane=Plane(),
|
|
)
|
|
|
|
assert result == b"remote result"
|
|
assert local_called is False
|
|
assert submitted[0].pinned_worker_id == decision.worker_id
|
|
|
|
|
|
def test_gateway_dependency_call_signatures_are_compatible():
|
|
"""Keep every gateway call into its orchestration dependencies bindable."""
|
|
from services.model_manager import check_gpu_admission, run_on_gpu_pool_guarded
|
|
from worker import routing
|
|
from worker.pool import WorkerPool
|
|
from worker.scheduler import Scheduler
|
|
from worker.service import ControlPlane
|
|
from worker.transport.server import WorkerServicer
|
|
|
|
target = object()
|
|
calls = [
|
|
(routing.decide, (target,), {"op": "tts"}),
|
|
(
|
|
Scheduler.submit,
|
|
(target,),
|
|
{
|
|
"operation": "tts",
|
|
"engine": "test-engine",
|
|
"model_id": "test:model",
|
|
"params": {},
|
|
"idempotency_key": "request-1",
|
|
"deadline_seconds": 1,
|
|
"pinned_worker_id": "worker-1",
|
|
},
|
|
),
|
|
(Scheduler.wait, (target, "task-1"), {"timeout": 1}),
|
|
(Scheduler.get, (target, "task-1"), {}),
|
|
(Scheduler.cancel, (target, "task-1"), {"reason": "cancelled"}),
|
|
(ControlPlane.cancel, (target, "task-1"), {"reason": "cancelled"}),
|
|
(WorkerPool.get, (target, "worker-1"), {}),
|
|
(WorkerServicer.prewarm, (target, "worker-1"), {"engine": "test-engine"}),
|
|
(
|
|
WorkerServicer.prewarm,
|
|
(target, "worker-1"),
|
|
{"engine": "", "model_id": "repo/model", "download_if_missing": True},
|
|
),
|
|
(check_gpu_admission, (), {"what": "GPU job", "executor": target}),
|
|
(
|
|
run_on_gpu_pool_guarded,
|
|
(lambda: None,),
|
|
{
|
|
"what": "GPU job",
|
|
"timeout": 1,
|
|
"queue_timeout": 1,
|
|
"min_vram_gb": 1,
|
|
"executor": target,
|
|
},
|
|
),
|
|
]
|
|
|
|
for callee, args, kwargs in calls:
|
|
inspect.signature(callee).bind(*args, **kwargs)
|