From cf920e97c371e530229f2eb2aa13794f9f85531f Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Tue, 30 Jun 2026 13:42:30 +0800 Subject: [PATCH] Add deep training acceptance checks --- .env.example | 1 + README.md | 9 +- backend/app/acceptance.py | 110 +++++++++++++++++++++++++ backend/app/agents/evaluation_agent.py | 2 + backend/app/agents/validation_agent.py | 5 +- backend/app/main.py | 12 ++- frontend/src/main.tsx | 45 ++++++++-- frontend/src/styles.css | 6 +- 8 files changed, 179 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 584102d..5a239f6 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,5 @@ SEG_MMSEG_CONDA_ENV=seg_mmcv SEG_BACKEND_CONDA_ENV=seg_smp SEG_WEIGHT_MODE=copy SEG_ENABLE_SHELL_TASKS=1 +SEG_VALIDATE_DEEP=1 VITE_API_BASE=http://localhost:8010 diff --git a/README.md b/README.md index c4f7ef4..5430932 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ weight discovery. MMSeg full-model readiness is validated in `SEG_MMSEG_CONDA_ENV` by importing `mmcv._ext` and building a local MMSeg `EncoderDecoder` from the existing config tree. +For stronger runtime proof, `POST /api/acceptance/deep` runs minimal training +loops for the three model families: one SegModel optimizer step, one YOLO +segmentation epoch on a synthetic 64x64 dataset, and one MMSeg optimizer step +through the full `mmcv._ext` runtime. The latest report is available from +`GET /api/acceptance/deep/latest` and is surfaced in the coverage panel. + Current `seg_smp` uses `mmcv-lite` because no `torch 2.6/cu124` full `mmcv` wheel is available on this machine and `nvcc` is not installed for source builds. A dedicated `seg_mmcv` environment is used for MMSeg tasks and has @@ -138,4 +144,5 @@ The validation agent checks catalog coverage, the `seg_smp` task env, the `seg_mmcv` MMSeg env, GPU visibility, no-weight Git safety, backend tests, frontend build, and live backend/frontend endpoints when the services are running. With live validation enabled it also runs the lightweight acceptance -smoke above. +smoke above. By default it also runs the deep training acceptance; set +`SEG_VALIDATE_DEEP=0` when a quick non-training validation pass is needed. diff --git a/backend/app/acceptance.py b/backend/app/acceptance.py index 44c0a57..579308e 100644 --- a/backend/app/acceptance.py +++ b/backend/app/acceptance.py @@ -62,6 +62,66 @@ MMSEG_FULL_BUILD_SNIPPET = ( ) +SEGMODEL_TRAIN_STEP_SNIPPET = ( + "import torch, segmentation_models_pytorch as smp; " + "torch.manual_seed(7); " + "model=smp.Unet(encoder_name='resnet18', encoder_weights=None, classes=2).train(); " + "inputs=torch.randn(2,3,64,64); " + "targets=torch.randint(0,2,(2,64,64)); " + "optimizer=torch.optim.SGD(model.parameters(), lr=1e-3); " + "outputs=model(inputs); " + "loss=torch.nn.functional.cross_entropy(outputs, targets); " + "loss.backward(); optimizer.step(); " + "print('loss', round(float(loss.detach()), 6), 'shape', tuple(outputs.shape))" +) + + +def _yolo_tiny_train_snippet(root: Path, weight: Path) -> str: + return ( + "import shutil, cv2, numpy as np; " + "from pathlib import Path; " + "from ultralytics import YOLO; " + f"root=Path({str(root)!r}); weight={str(weight)!r}; " + "shutil.rmtree(root, ignore_errors=True); " + "[ (root / item).mkdir(parents=True, exist_ok=True) for item in ['images/train','images/val','labels/train','labels/val','runs'] ]; " + "image=np.zeros((64,64,3), dtype=np.uint8); " + "cv2.rectangle(image, (16,16), (48,48), (255,255,255), -1); " + "label='0 0.25 0.25 0.75 0.25 0.75 0.75 0.25 0.75\\n'; " + "\nfor split in ['train','val']:\n" + " cv2.imwrite(str(root / 'images' / split / 'sample.jpg'), image)\n" + " (root / 'labels' / split / 'sample.txt').write_text(label, encoding='utf-8')\n" + "(root / 'data.yaml').write_text('path: '+str(root)+'\\ntrain: images/train\\nval: images/val\\nnc: 1\\nnames:\\n 0: object\\n', encoding='utf-8'); " + "model=YOLO(weight); " + "model.train(data=str(root/'data.yaml'), epochs=1, imgsz=64, batch=1, workers=0, device='cpu', project=str(root/'runs'), name='tiny', exist_ok=True, verbose=False, plots=False, val=False); " + "results=root/'runs'/'tiny'/'results.csv'; best=root/'runs'/'tiny'/'weights'/'best.pt'; " + "assert results.exists() and results.stat().st_size > 0; " + "assert best.exists() and best.stat().st_size > 0; " + "print('results', results, results.stat().st_size, 'best', best.stat().st_size)" + ) + + +def _mmseg_train_step_snippet(config_path: Path) -> str: + return ( + "import torch; " + "from mmengine.config import Config; " + "from mmengine.structures import PixelData; " + "from mmseg.registry import MODELS; " + "from mmseg.structures import SegDataSample; " + "from mmseg.utils import register_all_modules; " + "register_all_modules(init_default_scope=True); " + f"cfg=Config.fromfile({str(config_path)!r}); " + "cfg.model.backbone.init_cfg=None; cfg.model.pretrained=None; " + "model=MODELS.build(cfg.model).train(); " + "sample=SegDataSample(); " + "sample.gt_sem_seg=PixelData(data=torch.randint(0,19,(1,64,64), dtype=torch.long)); " + "losses=model(torch.randn(1,3,64,64), [sample], mode='loss'); " + "loss=sum(value if torch.is_tensor(value) else sum(value) for value in losses.values()); " + "optimizer=torch.optim.SGD(model.parameters(), lr=1e-4); " + "loss.backward(); optimizer.step(); " + "print('loss', round(float(loss.detach()), 6), sorted(losses.keys()))" + ) + + def _request_json(method: str, url: str, payload: dict[str, Any] | None = None, timeout: int = 10) -> dict[str, Any]: data = None headers = {"Accept": "application/json"} @@ -252,6 +312,56 @@ def latest_acceptance_report() -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) +def latest_deep_acceptance_report() -> dict[str, Any]: + path = settings.project_root / "var" / "acceptance" / "deep_latest.json" + if not path.exists(): + return {"available": False, "path": str(path)} + return json.loads(path.read_text(encoding="utf-8")) + + +def run_deep_acceptance() -> dict[str, Any]: + """Run minimal training loops for each model family without full datasets.""" + acceptance_root = settings.project_root / "var" / "acceptance" + run_id = uuid.uuid4().hex[:8] + fixture_root = acceptance_root / f"deep_{run_id}" + fixture_root.mkdir(parents=True, exist_ok=True) + + yolo_weight = settings.source_root / "Seg_All_In_One_YoloModel" / "yolo11n-seg.pt" + mmseg_config = settings.source_root / "Seg_All_In_One_MMSeg" / "configs" / "fcn" / "fcn_r18-d8_4xb2-80k_cityscapes-512x1024.py" + + checks = [ + { + "name": "segmodel_tiny_train_step", + "passed": False, + "detail": _run_snippet(SEGMODEL_TRAIN_STEP_SNIPPET, timeout=90), + }, + { + "name": "yolo_tiny_segment_train_epoch", + "passed": False, + "detail": _run_snippet(_yolo_tiny_train_snippet(fixture_root / "yolo_tiny", yolo_weight), timeout=180), + }, + { + "name": "mmseg_tiny_train_step", + "passed": False, + "detail": _run_conda_snippet(settings.mmseg_conda_env, _mmseg_train_step_snippet(mmseg_config), timeout=120), + }, + ] + for check in checks: + check["passed"] = bool(check["detail"].get("passed")) + + report = { + "available": True, + "run_id": run_id, + "fixture_root": str(fixture_root), + "passed": all(item["passed"] for item in checks), + "checks": checks, + "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + latest = acceptance_root / "deep_latest.json" + latest.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") + return report + + def run_live_acceptance(base_url: str = "http://127.0.0.1:8010") -> dict[str, Any]: """Run a lightweight end-to-end smoke against the live API and job runner.""" acceptance_root = settings.project_root / "var" / "acceptance" diff --git a/backend/app/agents/evaluation_agent.py b/backend/app/agents/evaluation_agent.py index 639639b..a60aaa6 100644 --- a/backend/app/agents/evaluation_agent.py +++ b/backend/app/agents/evaluation_agent.py @@ -42,6 +42,8 @@ def evaluate_project() -> dict: "loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" in frontend_text, "dataset_api": "/api/datasets" in backend_text and "api_upload_dataset_files" in backend_text, "curve_api": "/api/results/curves" in backend_text, + "deep_acceptance_api": "/api/acceptance/deep" in backend_text, + "deep_acceptance_ui": "runDeepAcceptance" in frontend_text and "深度训练" in frontend_text, "coverage_api": "/api/coverage" in backend_text and coverage["task_build_passed"], "visual_tools": "visual.yolo11_heatmap_v2" in catalog["task_types"] and "visual.fps" in catalog["task_types"], "yolo_dataset_tools": "dataset.yolo_txt_sort" in catalog["task_types"] and "dataset.yolo_resize" in catalog["task_types"], diff --git a/backend/app/agents/validation_agent.py b/backend/app/agents/validation_agent.py index 9ef5e0e..ef849ed 100644 --- a/backend/app/agents/validation_agent.py +++ b/backend/app/agents/validation_agent.py @@ -8,7 +8,7 @@ import urllib.error import urllib.request from pathlib import Path -from ..acceptance import run_live_acceptance +from ..acceptance import run_deep_acceptance, run_live_acceptance from ..catalog import get_catalog from ..config import settings from ..coverage import get_coverage_report @@ -108,6 +108,9 @@ def validate_project(run_build: bool = False) -> dict: if os.getenv("SEG_VALIDATE_ACCEPTANCE", "1") == "1": acceptance = run_live_acceptance(backend_url) checks.append({"name": "live_acceptance_smoke", "passed": acceptance["passed"], "detail": acceptance}) + if os.getenv("SEG_VALIDATE_DEEP", "1") == "1": + deep_acceptance = run_deep_acceptance() + checks.append({"name": "deep_training_acceptance", "passed": deep_acceptance["passed"], "detail": deep_acceptance}) if run_build: tests = _run(["conda", "run", "-n", settings.backend_conda_env, "python", "-m", "pytest", "-q"], cwd=settings.project_root, timeout=120) diff --git a/backend/app/main.py b/backend/app/main.py index 847291a..7198599 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, StreamingResponse from . import db -from .acceptance import latest_acceptance_report, run_live_acceptance +from .acceptance import latest_acceptance_report, latest_deep_acceptance_report, run_deep_acceptance, run_live_acceptance from .catalog import get_catalog from .config import settings from .coverage import get_coverage_report @@ -81,6 +81,16 @@ def api_acceptance_smoke(base_url: str = "http://127.0.0.1:8010") -> dict: return run_live_acceptance(base_url) +@app.get("/api/acceptance/deep/latest") +def api_deep_acceptance_latest() -> dict: + return latest_deep_acceptance_report() + + +@app.post("/api/acceptance/deep") +def api_deep_acceptance() -> dict: + return run_deep_acceptance() + + @app.get("/api/datasets") def api_datasets() -> list[dict]: return list_uploaded_datasets() diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 953baba..09e8cf3 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -109,6 +109,14 @@ type AcceptancePayload = { }; }; +type DeepAcceptancePayload = { + available?: boolean; + passed?: boolean; + run_id?: string; + created_at?: string; + checks?: Array<{ name: string; passed: boolean }>; +}; + type GpuPayload = { available: boolean; gpus: Array<{ @@ -191,11 +199,12 @@ function useData() { const [datasets, setDatasets] = useState([]); const [coverage, setCoverage] = useState(null); const [acceptance, setAcceptance] = useState(null); + const [deepAcceptance, setDeepAcceptance] = useState(null); const [error, setError] = useState(""); async function refresh() { try { - const [catalogNext, gpusNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext] = await Promise.all([ + const [catalogNext, gpusNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext] = await Promise.all([ api("/api/catalog"), api("/api/system/gpus"), api("/api/jobs"), @@ -203,7 +212,8 @@ function useData() { api("/api/results/curves"), api("/api/datasets"), api("/api/coverage"), - api("/api/acceptance/latest") + api("/api/acceptance/latest"), + api("/api/acceptance/deep/latest") ]); setCatalog(catalogNext); setGpus(gpusNext); @@ -213,6 +223,7 @@ function useData() { setDatasets(datasetsNext); setCoverage(coverageNext); setAcceptance(acceptanceNext); + setDeepAcceptance(deepAcceptanceNext); setError(""); } catch (err) { setError(String(err)); @@ -225,7 +236,7 @@ function useData() { return () => window.clearInterval(timer); }, []); - return { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, error, refresh }; + return { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, deepAcceptance, error, refresh }; } function StatusPill({ status }: { status: string }) { @@ -233,7 +244,7 @@ function StatusPill({ status }: { status: string }) { } function App() { - const { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, error, refresh } = useData(); + const { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, deepAcceptance, error, refresh } = useData(); const [taskType, setTaskType] = useState("mock.echo"); const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); const [selectedJob, setSelectedJob] = useState(null); @@ -333,6 +344,16 @@ function App() { } } + async function runDeepAcceptance() { + setBusy(true); + try { + await api("/api/acceptance/deep", { method: "POST" }); + await refresh(); + } finally { + setBusy(false); + } + } + async function createDataset() { setBusy(true); try { @@ -591,9 +612,14 @@ function App() {

Coverage

Seg 功能覆盖

- +
+ + +
@@ -616,12 +642,17 @@ function App() { 模型族 {acceptance?.model_family_readiness?.passed ? "OK" : "Check"}
+
+ 深度训练 + {deepAcceptance?.available === false ? "New" : deepAcceptance?.passed ? "OK" : "Check"} +
{(coverage?.unmapped_user_scripts.length ?? 0) === 0 ? ( <> 当前用户侧脚本已全部映射到网页任务。 最近验收:{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""} + 深度验收:{deepAcceptance?.created_at ?? "尚未运行"} {deepAcceptance?.run_id ? `#${deepAcceptance.run_id}` : ""},通过 {deepAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{deepAcceptance?.checks?.length ?? 0} 模型族 readiness:{acceptance?.model_family_readiness?.checks?.filter((item) => item.passed).length ?? 0}/{acceptance?.model_family_readiness?.checks?.length ?? 0},warnings {acceptance?.model_family_readiness?.warnings?.length ?? 0} ) : ( diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 67a879c..d2fcf02 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -358,6 +358,10 @@ textarea { gap: 10px; } +.buttonRow.compactButtons { + gap: 8px; +} + .opGrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -399,7 +403,7 @@ textarea { .coverageGrid { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }