From f697364aafc8743f8fbfd099248a445639c57fba Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sat, 5 Sep 2026 02:02:26 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"fix(settings):=20=E8=A1=A5=E9=BD=90CU?= =?UTF-8?q?DA=E8=BF=90=E8=A1=8C=E7=BB=84=E4=BB=B6=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E4=B8=8E=E5=AE=89=E8=A3=85=E5=85=A5=E5=8F=A3"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit c912409343ed7a6bd66823e994d25487ed42cf9a. --- backend/app/local_model_routes.py | 12 -- backend/app/local_models/components.py | 111 ------------------ backend/app/local_models/runtime.py | 3 - backend/app/main.py | 2 - backend/scripts/install-model-runtime.ps1 | 11 +- backend/tests/test_runtime_components.py | 85 -------------- docs/contracts/第二阶段接口契约-开发版.md | 2 - .../多模态管线与模型运行开发说明.md | 2 - .../阶段F-Embedding与知识库问题与解决方案.md | 8 -- .../settings/LocalModelSettings.spec.ts | 26 ---- .../features/settings/LocalModelSettings.vue | 25 ---- 11 files changed, 3 insertions(+), 284 deletions(-) delete mode 100644 backend/app/local_models/components.py delete mode 100644 backend/tests/test_runtime_components.py delete mode 100644 frontend/src/features/settings/LocalModelSettings.spec.ts diff --git a/backend/app/local_model_routes.py b/backend/app/local_model_routes.py index fd79d8c..d3cb20c 100644 --- a/backend/app/local_model_routes.py +++ b/backend/app/local_model_routes.py @@ -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)) diff --git a/backend/app/local_models/components.py b/backend/app/local_models/components.py deleted file mode 100644 index c908620..0000000 --- a/backend/app/local_models/components.py +++ /dev/null @@ -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' diff --git a/backend/app/local_models/runtime.py b/backend/app/local_models/runtime.py index e362df0..ae73314 100644 --- a/backend/app/local_models/runtime.py +++ b/backend/app/local_models/runtime.py @@ -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")))) diff --git a/backend/app/main.py b/backend/app/main.py index d307982..f7b97c6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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) diff --git a/backend/scripts/install-model-runtime.ps1 b/backend/scripts/install-model-runtime.ps1 index bfab0ac..fe4b890 100644 --- a/backend/scripts/install-model-runtime.ps1 +++ b/backend/scripts/install-model-runtime.ps1 @@ -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 '模型运行环境检查失败' } diff --git a/backend/tests/test_runtime_components.py b/backend/tests/test_runtime_components.py deleted file mode 100644 index 6f367e6..0000000 --- a/backend/tests/test_runtime_components.py +++ /dev/null @@ -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' diff --git a/docs/contracts/第二阶段接口契约-开发版.md b/docs/contracts/第二阶段接口契约-开发版.md index 5f93ced..0a716af 100644 --- a/docs/contracts/第二阶段接口契约-开发版.md +++ b/docs/contracts/第二阶段接口契约-开发版.md @@ -1576,8 +1576,6 @@ frontend/src/ ### 阶段 F 收尾接口补充(2026-09-05) -CUDA 组件:`GET /api/local-models/runtime-components/cuda` 返回 status、stage、supported、custom_interpreter、cuda_available、可选 torch/error。status 为 checking/not_installed/installing/installed/failed/interrupted;读取只检查现有环境,不下载安装。`POST` 同路径明确触发后台安装,返回 202;重复请求复用当前安装任务。正在推理/排队返回 409 MODEL_IN_USE,缺少 uv 返回 422 UV_NOT_INSTALLED,不支持的平台返回 422 PLATFORM_UNSUPPORTED。阶段进度不冒充字节百分比。关闭后端时回收安装进程树,重启后重新验证环境。 - | 接口/字段 | 行为 | | --- | --- | | `POST /api/media/attachments` | 可选 `Idempotency-Key` Header,16–100 位字母、数字、下划线或连字符。同键同扩展名同内容返回同 attachment_id;同键同扩展名内容不一致返回 409 `IDEMPOTENCY_CONFLICT`。上传仍受 25 MiB 限制。 | diff --git a/docs/development/多模态管线与模型运行开发说明.md b/docs/development/多模态管线与模型运行开发说明.md index d6175e5..1ae5d67 100644 --- a/docs/development/多模态管线与模型运行开发说明.md +++ b/docs/development/多模态管线与模型运行开发说明.md @@ -95,8 +95,6 @@ POST /api/providers/request-preview 不联网,隐藏正文/文件且不包含 ### 阶段 F 收尾行为(2026-09-05) -CUDA 页面安装入口:**设置 → 模型提供商 → 本地模型 → CUDA 运行组件(可选)**。未安装时显示“下载并安装 CUDA 组件”;安装中展示真实阶段和不定进度条,失败可重试。后端仅运行项目内固定安装脚本,写入独立 `.venv-models-cuda`,检查依赖及 CUDA wheel 后才标记就绪。已有环境会先检查;成功后选择 CUDA 并保存运行设置即可使用,CPU 环境保留。显式 `APP_MODEL_PYTHON` 继续优先,页面提示覆盖关系。当前页面安装支持 Windows,需后端能找到 uv;不会自动安装显卡驱动。 - - 模型卡片读取权重目录的实际文件大小,包含未完成下载的文件;下载进度与磁盘占用分别显示。 - 本地任务串行执行;等待队列中交互检索优先级为 0,媒体任务为 10,后台笔记索引为 20。同级 FIFO,不抢占已运行任务。 - 默认 CPU。选择 CUDA 后,设备不可用直接使用 CPU;CUDA 初始化失败或显存不足时先释放原子进程,再用冻结的同一任务配置重试 CPU 一次。其他错误不触发设备重试;用户取消不会启动后续尝试。重试会清除上一尝试的部分转写片段。 diff --git a/docs/retrospectives/阶段F-Embedding与知识库问题与解决方案.md b/docs/retrospectives/阶段F-Embedding与知识库问题与解决方案.md index f749073..71c847f 100644 --- a/docs/retrospectives/阶段F-Embedding与知识库问题与解决方案.md +++ b/docs/retrospectives/阶段F-Embedding与知识库问题与解决方案.md @@ -138,14 +138,6 @@ embedding_local_only: true ## 9. 工程经验 -### F-16:CUDA 选装只有脚本,前端缺少安装入口 - -问题:上一轮完成了独立 CUDA 环境安装和 GPU 实测,但页面只有设备下拉框及脚本说明。用户无法从前端下载组件,工程收尾遗漏了可操作入口。 - -实际方案:增加独立组件卡片和 GET/POST 状态、安装接口;展示环境检查、下载 PyTorch、安装依赖、验证等真实阶段,失败允许重试。固定脚本、目录和参数,默认 CPU 环境不变;安装成功后 CUDA 模式自动选择已验证环境,显式 Python 覆盖仍优先。推理期间拒绝安装,重复点击不产生多个任务,后端关闭时回收安装子进程树。 - -验证:21 项后端相关测试及新增前端组件测试通过,类型检查和构建通过;真实页面已显示本机 `2.9.1+cu128` 组件就绪,默认 CPU 未改变。本轮复用已安装组件验证识别,未重复下载 3 GB 安装包;下载入口、重复请求和失败重试由隔离测试覆盖。 - ### F-13:CUDA 失败重试与诊断无法追溯 问题:设备不可用时能够使用 CPU,但 CUDA 初始化失败、显存不足会直接使任务失败;诊断只留在进程内存中,重启后无法解释当时的失败和回退。 diff --git a/frontend/src/features/settings/LocalModelSettings.spec.ts b/frontend/src/features/settings/LocalModelSettings.spec.ts deleted file mode 100644 index 8b80f57..0000000 --- a/frontend/src/features/settings/LocalModelSettings.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @vitest-environment happy-dom -import { mount, flushPromises } from '@vue/test-utils' -import { expect, it, vi } from 'vitest' -import { apiClient } from '@/services/apiClient' -import LocalModelSettings from './LocalModelSettings.vue' - -vi.mock('@/services/apiClient', () => ({apiClient:{get:vi.fn(),post:vi.fn()}})) -it('shows an optional CUDA installer and live installation stage', async () => { - vi.mocked(apiClient.get).mockImplementation(async (url) => url.includes('runtime-components') - ? {status:'not_installed', stage:'尚未安装', supported:true, cuda_available:null,custom_interpreter:false} - : {items:[],config:null,runtime_installed:true,last_inference:null}) - vi.mocked(apiClient.post).mockResolvedValue({status:'installing',stage:'下载并安装 PyTorch CUDA(约 3 GB)',supported:true}) - const wrapper = mount(LocalModelSettings) - try { - await flushPromises() - const button = wrapper.findAll('button').find(b => b.text() === '下载并安装 CUDA 组件')! - expect(button.exists()).toBe(true) - expect(apiClient.post).not.toHaveBeenCalled() - await button.trigger('click') - await flushPromises() - expect(apiClient.post).toHaveBeenCalledWith('/api/local-models/runtime-components/cuda') - expect(wrapper.text()).toContain('下载并安装 PyTorch CUDA') - expect(wrapper.get('progress').attributes('value')).toBeUndefined() - expect(button.attributes('disabled')).toBeDefined() - } finally {wrapper.unmount()} -}) diff --git a/frontend/src/features/settings/LocalModelSettings.vue b/frontend/src/features/settings/LocalModelSettings.vue index e75f7e2..b6966c8 100644 --- a/frontend/src/features/settings/LocalModelSettings.vue +++ b/frontend/src/features/settings/LocalModelSettings.vue @@ -6,16 +6,6 @@ interface Model {key: string; name: string; capability: string; revision: string const items = ref([]) const config = ref(null) const installed = ref(false) -interface CudaComponent {status:string;stage:string;cuda_available:boolean|null;supported:boolean;custom_interpreter:boolean;error?:string;torch?:string} -const cuda = ref(null) -const cudaError = ref('') -async function loadCuda() { - try { cuda.value = await apiClient.get('/api/local-models/runtime-components/cuda'); cudaError.value = '' } - catch(e) { cudaError.value = (e as Error).message } -} -async function installCuda() { - await act(async () => { cuda.value = await apiClient.post('/api/local-models/runtime-components/cuda') }) -} const lastInference = ref<{actual_device:string;requested_device:string;inference_seconds?:number;elapsed_seconds?:number;status?:string;error_code?:string}|null>(null) const error = ref('') const dirty = ref(false) @@ -25,7 +15,6 @@ let stopped = false const size = (bytes: number | null) => bytes === null ? '未知' : `${(bytes / 1024 / 1024).toFixed(1)} MiB` const labels: Record = {not_installed:'未下载',downloading:'下载中',installed:'已下载并校验',failed:'下载失败',interrupted:'已中断,可续传'} async function load() { - await loadCuda() try { const data = await apiClient.get<{items:Model[];config:Config;runtime_installed:boolean;last_inference:typeof lastInference.value}>('/api/local-models') items.value = data.items; installed.value = data.runtime_installed @@ -56,20 +45,6 @@ onUnmounted(() => { stopped = true; clearTimeout(timer) })

最近实际运行:{{ lastInference.actual_device || '未开始推理' }} · 请求设备 {{ lastInference.requested_device }} · 推理 {{ (lastInference.inference_seconds ?? lastInference.elapsed_seconds ?? 0).toFixed(2) }} 秒 · {{ lastInference.status }} {{ lastInference.error_code || '' }}

尚未安装模型运行环境。在项目根目录执行 ./backend/scripts/install-model-runtime.ps1;CUDA 选装追加 -Device cuda

-
-

CUDA 运行组件(可选)

-

默认使用 CPU。需要 NVIDIA GPU 加速时下载此组件,约 3 GB,安装时还需要额外磁盘空间;不包含显卡驱动和模型权重。

- - -