Revert "fix(settings): 补齐CUDA运行组件下载与安装入口"

This reverts commit c912409343.
This commit is contained in:
2026-09-05 02:02:26 +08:00
parent c912409343
commit f697364aaf
11 changed files with 3 additions and 284 deletions
-12
View File
@@ -7,18 +7,6 @@ from app.local_models.runtime import RuntimeConfig, configuration, configure, in
router = APIRouter(prefix="/api/local-models", tags=["Local models"])
@router.get("/runtime-components/cuda")
async def cuda_status():
from app.local_models import components
return await components.status()
@router.post("/runtime-components/cuda", status_code=202)
async def install_cuda():
from app.local_models import components
return await components.install()
@router.get("")
async def list_models():
items, diagnostics = await asyncio.gather(asyncio.to_thread(manager.describe), asyncio.to_thread(model_diagnostics.recent))
-111
View File
@@ -1,111 +0,0 @@
"""User-triggered installation of the fixed optional CUDA runtime on Windows."""
import asyncio
import json
import os
import shutil
import subprocess
from app.config import BACKEND_DIR
from app.errors import ApiError
from app.local_models.process import ThreadedProcess
ROOT = BACKEND_DIR / '.venv-models-cuda'
state = {'status': 'unchecked', 'stage': '', 'cuda_available': None}
task = None
def ready():
return (ROOT / 'ready.json').is_file() and (ROOT / 'Scripts/python.exe').is_file()
async def status():
global task
if state['status'] == 'unchecked':
state.update(status='checking', stage='检查已有 CUDA 组件')
task = asyncio.create_task(run(False))
return {**state, 'supported': os.name == 'nt', 'custom_interpreter': bool(os.getenv('APP_MODEL_PYTHON'))}
async def install():
global task
from app.local_models.runtime import runtime
if os.name != 'nt':
raise ApiError(422, 'PLATFORM_UNSUPPORTED', '此安装入口目前支持 Windows。')
if task is not None and not task.done():
return await status()
if runtime.active or runtime.waiters:
raise ApiError(409, 'MODEL_IN_USE', '请等待本地模型任务结束后再安装组件。')
if state['status'] == 'installed':
return await status()
if not shutil.which('uv'):
raise ApiError(422, 'UV_NOT_INSTALLED', '后端未找到 uv,请先安装 uv 并重启后端。')
state.update(status='installing', stage='准备独立 CUDA 环境', error=None)
task = asyncio.create_task(run(True))
return await status()
async def execute(args, timeout):
process = ThreadedProcess(args, env={**os.environ, 'PYTHONIOENCODING': 'utf-8'},
limit=8192, creationflags=0x08000000 if os.name == 'nt' else 0)
process.stdin.close()
lines = []
try:
async with asyncio.timeout(timeout):
while line := await process.stdout.readline():
value = line.decode('utf-8', errors='replace').strip()
stages = {'COMPONENT:torch': '下载并安装 PyTorch CUDA(约 3 GB',
'COMPONENT:dependencies': '安装模型依赖', 'COMPONENT:verify': '验证运行组件'}
if value in stages:
state['stage'] = stages[value]
lines = (lines + [value])[-4:]
await process.wait()
if process.returncode:
raise RuntimeError('component command failed')
return lines
finally:
if process.returncode is None:
if os.name == 'nt':
await asyncio.to_thread(subprocess.run, ['taskkill', '/PID', str(process.process.pid), '/T', '/F'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
creationflags=0x08000000)
else:
process.kill()
await process.wait()
await process.close()
async def run(download):
marker = ROOT / 'ready.json'
try:
if download:
marker.unlink(missing_ok=True)
await execute(['powershell.exe', '-NoProfile', '-NonInteractive', '-File',
str(BACKEND_DIR / 'scripts/install-model-runtime.ps1'), '-Device', 'cuda',
'-RuntimeDirectory', str(ROOT), '-QuietProgress'], 7200)
python = ROOT / 'Scripts/python.exe'
if not python.is_file():
state.update(status='not_installed', stage='尚未安装')
return
result = await execute([str(python), '-c',
'import json, torch, torchaudio, sentence_transformers, qwen_asr; '
'assert torch.version.cuda; '
'print(json.dumps({"torch":torch.__version__,"cuda_available":torch.cuda.is_available()}))'], 180)
info = json.loads(result[-1])
marker.write_text(json.dumps(info), encoding='utf-8')
state.update(status='installed', stage='组件已安装', error=None, **info)
except asyncio.CancelledError:
marker.unlink(missing_ok=True)
state.update(status='interrupted', stage='安装检查已中断,可重试')
raise
except Exception:
marker.unlink(missing_ok=True)
state.update(status='failed', stage='组件安装或验证失败',
error='请检查网络、磁盘空间和 uv;可以重试。CPU 环境不受影响。')
async def shutdown():
if task is not None and not task.done():
task.cancel()
await asyncio.gather(task, return_exceptions=True)
if state['status'] in {'checking', 'interrupted'}:
state['status'] = 'unchecked'
-3
View File
@@ -70,9 +70,6 @@ def configure(request):
def interpreter():
from app.local_models import components
if not os.getenv("APP_MODEL_PYTHON") and configuration().device == "cuda" and components.ready():
return components.ROOT / "Scripts/python.exe"
return Path(os.getenv("APP_MODEL_PYTHON", str(BACKEND_DIR / ".venv-models" / ("Scripts/python.exe" if os.name == "nt" else "bin/python"))))
-2
View File
@@ -26,8 +26,6 @@ async def lifespan(_: FastAPI):
yield
finally:
await transcription_service.shutdown()
from app.local_models import components
await components.shutdown()
from app.local_models import manager
for _, key in list(manager._downloads):
await manager.cancel_download(key)
+3 -8
View File
@@ -1,10 +1,8 @@
param(
[ValidateSet('cpu', 'cuda')][string]$Device = 'cpu',
[string]$RuntimeDirectory = '',
[switch]$QuietProgress
[string]$RuntimeDirectory = ''
)
$ErrorActionPreference = 'Stop'
$uvOptions = if ($QuietProgress) { @('--quiet') } else { @() }
$backendRoot = Split-Path $PSScriptRoot -Parent
$runtimeRoot = if ($RuntimeDirectory) { [IO.Path]::GetFullPath($RuntimeDirectory) } else { Join-Path $backendRoot '.venv-models' }
$runtimePython = Join-Path $runtimeRoot 'Scripts/python.exe'
@@ -16,12 +14,9 @@ if (!(Test-Path -LiteralPath $runtimePython)) {
$torchIndex = if ($Device -eq 'cuda') { 'https://download.pytorch.org/whl/cu128' } else { 'https://download.pytorch.org/whl/cpu' }
$wheelVariant = if ($Device -eq 'cuda') { 'cu128' } else { 'cpu' }
# Pin the local version too: ==2.9.1 alone also accepts an already-installed CPU wheel.
Write-Output 'COMPONENT:torch'
& uv @uvOptions pip install --python $runtimePython --index-url $torchIndex "torch==2.9.1+$wheelVariant" "torchaudio==2.9.1+$wheelVariant"
& uv pip install --python $runtimePython --index-url $torchIndex "torch==2.9.1+$wheelVariant" "torchaudio==2.9.1+$wheelVariant"
if ($LASTEXITCODE -ne 0) { throw 'PyTorch 安装失败' }
Write-Output 'COMPONENT:dependencies'
& uv @uvOptions pip install --python $runtimePython -r (Join-Path $PSScriptRoot 'model-requirements.lock') -c (Join-Path $PSScriptRoot 'model-requirements.txt')
& uv pip install --python $runtimePython -r (Join-Path $PSScriptRoot 'model-requirements.lock') -c (Join-Path $PSScriptRoot 'model-requirements.txt')
if ($LASTEXITCODE -ne 0) { throw '模型依赖安装失败' }
Write-Output 'COMPONENT:verify'
& $runtimePython -c 'import torch; print({"torch":torch.__version__,"cuda_available":torch.cuda.is_available()})'
if ($LASTEXITCODE -ne 0) { throw '模型运行环境检查失败' }
-85
View File
@@ -1,85 +0,0 @@
import asyncio
import json
import os
import pytest
from app.errors import ApiError
from app.local_models import components, runtime
@pytest.fixture(autouse=True)
def isolate(monkeypatch, tmp_path):
monkeypatch.setattr(components, 'ROOT', tmp_path / 'cuda')
monkeypatch.setattr(components, 'state', {'status': 'unchecked', 'stage': '', 'cuda_available': None})
monkeypatch.setattr(components, 'task', None)
def test_status_checks_without_installing_and_detects_existing_cuda(monkeypatch):
python = components.ROOT / 'Scripts/python.exe'
python.parent.mkdir(parents=True)
python.touch()
calls = []
async def execute(args, timeout):
calls.append(args)
return [json.dumps({'torch': '2.9.1+cu128', 'cuda_available': True})]
monkeypatch.setattr(components, 'execute', execute)
async def scenario():
assert (await components.status())['status'] == 'checking'
await components.task
assert (await components.status())['status'] == 'installed'
assert len(calls) == 1 and calls[0][0] == str(python)
assert components.ready()
asyncio.run(scenario())
@pytest.mark.skipif(os.name != 'nt', reason='Windows installer')
def test_install_deduplicates_and_failure_can_retry(monkeypatch):
monkeypatch.setattr(components.shutil, 'which', lambda name: 'uv.exe')
async def scenario():
entered, release = asyncio.Event(), asyncio.Event()
calls = []
async def execute(args, timeout):
calls.append(args)
entered.set()
await release.wait()
raise RuntimeError('private exception')
monkeypatch.setattr(components, 'execute', execute)
await components.install()
await entered.wait()
first = components.task
await components.install()
assert first is components.task
release.set()
await first
assert components.state['status'] == 'failed'
assert 'private exception' not in str(components.state)
await components.install()
await components.task
assert len(calls) == 2 and '-RuntimeDirectory' in calls[0]
assert not components.ready()
asyncio.run(scenario())
@pytest.mark.skipif(os.name != 'nt', reason='Windows installer')
def test_install_refuses_active_inference(monkeypatch):
monkeypatch.setattr(runtime.runtime, 'active', {1: 'bekko'})
async def scenario():
with pytest.raises(ApiError) as exc:
await components.install()
assert exc.value.code == 'MODEL_IN_USE'
asyncio.run(scenario())
def test_interpreter_keeps_cpu_default_and_respects_explicit_override(monkeypatch):
monkeypatch.delenv('APP_MODEL_PYTHON', raising=False)
python = components.ROOT / 'Scripts/python.exe'
python.parent.mkdir(parents=True)
python.touch()
(components.ROOT / 'ready.json').write_text('{}')
monkeypatch.setattr(runtime, 'configuration', lambda: runtime.RuntimeConfig(device='cpu'))
assert runtime.interpreter() != python
monkeypatch.setattr(runtime, 'configuration', lambda: runtime.RuntimeConfig(device='cuda'))
assert runtime.interpreter() == python
monkeypatch.setenv('APP_MODEL_PYTHON', 'explicit-python.exe')
assert str(runtime.interpreter()) == 'explicit-python.exe'