Add model family readiness smoke
This commit is contained in:
10
README.md
10
README.md
@@ -54,7 +54,15 @@ classified as supporting artifacts rather than direct web actions.
|
|||||||
The same panel can run `POST /api/acceptance/smoke`, a lightweight live smoke
|
The same panel can run `POST /api/acceptance/smoke`, a lightweight live smoke
|
||||||
that creates an upload dataset, uploads a label, downloads it through the
|
that creates an upload dataset, uploads a label, downloads it through the
|
||||||
artifact API, runs a mock job, checks SSE log streaming, and executes one
|
artifact API, runs a mock job, checks SSE log streaming, and executes one
|
||||||
legacy image/label overlay job on tiny generated PNGs.
|
legacy image/label overlay job on tiny generated PNGs. It also runs model
|
||||||
|
family readiness checks: a SegModel/SMP forward pass, a YOLO segmentation
|
||||||
|
prediction on a tiny image, MMSeg config parsing, and local MMSeg pretrained
|
||||||
|
weight discovery.
|
||||||
|
|
||||||
|
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. The acceptance smoke reports MMSeg full model construction as a
|
||||||
|
warning until a full `mmcv` build with `mmcv._ext` is installed.
|
||||||
|
|
||||||
## Weight Sync
|
## Weight Sync
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import urllib.error
|
import urllib.error
|
||||||
@@ -11,6 +13,22 @@ from typing import Any
|
|||||||
from .config import settings
|
from .config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _run_snippet(code: str, cwd: Path | None = None, timeout: int = 60) -> dict[str, Any]:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-c", code],
|
||||||
|
cwd=str(cwd or settings.project_root),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"passed": result.returncode == 0,
|
||||||
|
"returncode": result.returncode,
|
||||||
|
"stdout": result.stdout[-4000:],
|
||||||
|
"stderr": result.stderr[-4000:],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _request_json(method: str, url: str, payload: dict[str, Any] | None = None, timeout: int = 10) -> dict[str, Any]:
|
def _request_json(method: str, url: str, payload: dict[str, Any] | None = None, timeout: int = 10) -> dict[str, Any]:
|
||||||
data = None
|
data = None
|
||||||
headers = {"Accept": "application/json"}
|
headers = {"Accept": "application/json"}
|
||||||
@@ -122,6 +140,68 @@ def _write_acceptance_images(root: Path) -> tuple[Path, Path, Path]:
|
|||||||
return image_path, label_path, result_dir
|
return image_path, label_path, result_dir
|
||||||
|
|
||||||
|
|
||||||
|
def run_model_family_readiness() -> dict[str, Any]:
|
||||||
|
"""Exercise the model-family runtime stack without launching full training."""
|
||||||
|
source = settings.source_root
|
||||||
|
yolo_weight = source / "Seg_All_In_One_YoloModel" / "yolo11n-seg.pt"
|
||||||
|
mmseg_config = source / "Seg_All_In_One_MMSeg" / "configs" / "fcn" / "fcn_r18-d8_4xb2-80k_cityscapes-512x1024.py"
|
||||||
|
mmseg_pretrained = source / "Seg_All_In_One_MMSeg" / "My_Local_Model" / "mmcls" / "resnet18.pth"
|
||||||
|
|
||||||
|
checks = [
|
||||||
|
{
|
||||||
|
"name": "segmodel_smp_forward",
|
||||||
|
"required": True,
|
||||||
|
"detail": _run_snippet(
|
||||||
|
"import torch, segmentation_models_pytorch as smp; "
|
||||||
|
"m=smp.Unet(encoder_name='resnet18', encoder_weights=None, classes=2).eval(); "
|
||||||
|
"torch.set_grad_enabled(False); y=m(torch.randn(1,3,64,64)); "
|
||||||
|
"print(tuple(y.shape))"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yolo_seg_predict_cpu",
|
||||||
|
"required": True,
|
||||||
|
"detail": _run_snippet(
|
||||||
|
"from ultralytics import YOLO; import numpy as np; "
|
||||||
|
f"model=YOLO({str(yolo_weight)!r}); "
|
||||||
|
"r=model.predict(np.zeros((64,64,3), dtype=np.uint8), imgsz=64, verbose=False, save=False, device='cpu'); "
|
||||||
|
"print(len(r), r[0].orig_shape)"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mmseg_config_parse",
|
||||||
|
"required": True,
|
||||||
|
"detail": _run_snippet(
|
||||||
|
"from mmengine.config import Config; "
|
||||||
|
f"cfg=Config.fromfile({str(mmseg_config)!r}); "
|
||||||
|
"print(cfg.model.type, cfg.train_dataloader.batch_size)"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mmseg_local_pretrained_weight",
|
||||||
|
"required": True,
|
||||||
|
"detail": {"passed": mmseg_pretrained.exists(), "path": str(mmseg_pretrained), "size": mmseg_pretrained.stat().st_size if mmseg_pretrained.exists() else 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mmseg_full_model_build",
|
||||||
|
"required": False,
|
||||||
|
"detail": _run_snippet(
|
||||||
|
"from mmengine.config import Config; from mmseg.registry import MODELS; "
|
||||||
|
f"cfg=Config.fromfile({str(mmseg_config)!r}); "
|
||||||
|
"model=MODELS.build(cfg.model); print(type(model).__name__)",
|
||||||
|
timeout=90,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for check in checks:
|
||||||
|
check["passed"] = bool(check["detail"].get("passed"))
|
||||||
|
return {
|
||||||
|
"passed": all(item["passed"] for item in checks if item["required"]),
|
||||||
|
"warnings": [item for item in checks if not item["required"] and not item["passed"]],
|
||||||
|
"checks": checks,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def latest_acceptance_report() -> dict[str, Any]:
|
def latest_acceptance_report() -> dict[str, Any]:
|
||||||
path = settings.project_root / "var" / "acceptance" / "latest.json"
|
path = settings.project_root / "var" / "acceptance" / "latest.json"
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
@@ -188,12 +268,16 @@ def run_live_acceptance(base_url: str = "http://127.0.0.1:8010") -> dict[str, An
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readiness = run_model_family_readiness()
|
||||||
|
checks.append({"name": "model_family_readiness", "passed": readiness["passed"], "detail": readiness})
|
||||||
|
|
||||||
report = {
|
report = {
|
||||||
"available": True,
|
"available": True,
|
||||||
"run_id": run_id,
|
"run_id": run_id,
|
||||||
"base_url": base_url,
|
"base_url": base_url,
|
||||||
"passed": all(item["passed"] for item in checks),
|
"passed": all(item["passed"] for item in checks),
|
||||||
"checks": checks,
|
"checks": checks,
|
||||||
|
"model_family_readiness": readiness,
|
||||||
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
}
|
}
|
||||||
latest = acceptance_root / "latest.json"
|
latest = acceptance_root / "latest.json"
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ type AcceptancePayload = {
|
|||||||
run_id?: string;
|
run_id?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
checks?: Array<{ name: string; passed: boolean }>;
|
checks?: Array<{ name: string; passed: boolean }>;
|
||||||
|
model_family_readiness?: {
|
||||||
|
passed: boolean;
|
||||||
|
warnings: Array<{ name: string; passed: boolean }>;
|
||||||
|
checks: Array<{ name: string; passed: boolean; required: boolean }>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type GpuPayload = {
|
type GpuPayload = {
|
||||||
@@ -539,12 +544,17 @@ function App() {
|
|||||||
<span>轻量验收</span>
|
<span>轻量验收</span>
|
||||||
<strong>{acceptance?.available === false ? "New" : acceptance?.passed ? "OK" : "Check"}</strong>
|
<strong>{acceptance?.available === false ? "New" : acceptance?.passed ? "OK" : "Check"}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>模型族</span>
|
||||||
|
<strong>{acceptance?.model_family_readiness?.passed ? "OK" : "Check"}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="coverageStatus">
|
<div className="coverageStatus">
|
||||||
{(coverage?.unmapped_user_scripts.length ?? 0) === 0 ? (
|
{(coverage?.unmapped_user_scripts.length ?? 0) === 0 ? (
|
||||||
<>
|
<>
|
||||||
<span>当前用户侧脚本已全部映射到网页任务。</span>
|
<span>当前用户侧脚本已全部映射到网页任务。</span>
|
||||||
<span>最近验收:{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""}</span>
|
<span>最近验收:{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""}</span>
|
||||||
|
<span>模型族 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}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
coverage?.unmapped_user_scripts.slice(0, 8).map((item) => <code key={item}>{item}</code>)
|
coverage?.unmapped_user_scripts.slice(0, 8).map((item) => <code key={item}>{item}</code>)
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ textarea {
|
|||||||
|
|
||||||
.coverageGrid {
|
.coverageGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user