feat: 更新后端平台模块、Compute引擎、前端组件及构建产物
- 更新 backend 平台 API、platform_store、compute_gateway sync - 更新 compute agent/engine/adapter 及 API - 更新 Docker 部署配置(app/compute) - 新增 frontend/src/utils/ 工具模块 - 新增 scripts/ops_diagnostics.py 运维诊断脚本 - 新增 docs/2026-07-23-development-summary.md 开发总结 - 重构 frontend/dist 构建产物(新 hash) - 更新前端多个视图组件及 API 模块 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ from fastapi import FastAPI, File, Form, HTTPException, Query, Request, UploadFi
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
from compute.agent.process_manager import ProcessManager
|
||||
from compute.engines.llama_factory.adapter import build_command, parse_log_line
|
||||
from compute.engines.llama_factory.adapter import build_command, parse_log_line, prepare_runtime_files
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
@@ -74,6 +74,48 @@ def create_app() -> FastAPI:
|
||||
return output.splitlines()[0][:120]
|
||||
return ""
|
||||
|
||||
def torch_cuda_status() -> dict[str, Any]:
|
||||
try:
|
||||
import torch # type: ignore[import-not-found]
|
||||
except Exception as exc: # noqa: BLE001 - keep health endpoint resilient
|
||||
return {
|
||||
"available": False,
|
||||
"device_count": 0,
|
||||
"torch_version": "",
|
||||
"torch_cuda_version": "",
|
||||
"error": f"torch import failed: {exc}",
|
||||
}
|
||||
try:
|
||||
available = bool(torch.cuda.is_available())
|
||||
device_count = int(torch.cuda.device_count())
|
||||
devices = []
|
||||
for index in range(device_count):
|
||||
props = torch.cuda.get_device_properties(index)
|
||||
devices.append(
|
||||
{
|
||||
"index": index,
|
||||
"name": props.name,
|
||||
"memory_total_gb": round(props.total_memory / 1024 / 1024 / 1024, 2),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"available": available,
|
||||
"device_count": device_count,
|
||||
"torch_version": str(torch.__version__),
|
||||
"torch_cuda_version": str(torch.version.cuda or ""),
|
||||
"devices": devices,
|
||||
"error": "" if available else "torch cuda is not available",
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001 - expose CUDA initialization failures
|
||||
return {
|
||||
"available": False,
|
||||
"device_count": 0,
|
||||
"torch_version": str(getattr(torch, "__version__", "")),
|
||||
"torch_cuda_version": str(getattr(torch.version, "cuda", "") or ""),
|
||||
"devices": [],
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
def _slice_log_content(
|
||||
content: str,
|
||||
tail_lines: int | None = None,
|
||||
@@ -261,6 +303,35 @@ def create_app() -> FastAPI:
|
||||
)
|
||||
return gpus
|
||||
|
||||
def _validate_training_accelerator(payload: dict[str, Any]) -> tuple[list[str], list[str], dict[str, Any]]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
if str(payload.get("engine") or payload.get("training_engine") or "llama_factory") == "smoke":
|
||||
return errors, warnings, {}
|
||||
requested_gpus = [int(item) for item in payload.get("gpus") or []]
|
||||
if not requested_gpus:
|
||||
warnings.append("no gpu selected; training will run on CPU")
|
||||
return errors, warnings, {}
|
||||
cuda = torch_cuda_status()
|
||||
if not cuda.get("available"):
|
||||
errors.append(f"torch cuda unavailable on compute node: {cuda.get('error') or 'unknown error'}")
|
||||
device_count = int(cuda.get("device_count") or 0)
|
||||
if device_count and max(requested_gpus) >= device_count:
|
||||
errors.append(f"requested gpu index out of torch device range: requested={requested_gpus}, device_count={device_count}")
|
||||
min_memory_gb = _float_env("MIN_TRAINING_GPU_MEMORY_GB", 4.0)
|
||||
gpus = {int(item["gpu_index"]): item for item in gpu_resources() if "gpu_index" in item}
|
||||
for gpu_index in requested_gpus:
|
||||
gpu = gpus.get(gpu_index)
|
||||
if not gpu:
|
||||
errors.append(f"requested gpu not found by nvidia-smi: {gpu_index}")
|
||||
continue
|
||||
memory_total = float(gpu.get("memory_total_gb") or 0)
|
||||
if memory_total and memory_total < min_memory_gb:
|
||||
errors.append(
|
||||
f"gpu {gpu_index} memory too small: {memory_total}GB < required {min_memory_gb}GB"
|
||||
)
|
||||
return errors, warnings, cuda
|
||||
|
||||
def _check_path_item(item: dict[str, Any]) -> dict[str, Any]:
|
||||
path = Path(str(item.get("path") or ""))
|
||||
exists = path.exists()
|
||||
@@ -278,13 +349,32 @@ def create_app() -> FastAPI:
|
||||
"exists": exists,
|
||||
"is_dir": path.is_dir() if exists else False,
|
||||
"is_file": path.is_file() if exists else False,
|
||||
"byte_size": sum(child.stat().st_size for child in path.rglob("*") if child.is_file()) if exists and path.is_dir() else path.stat().st_size if exists and path.is_file() else 0,
|
||||
"ok": ok or not item.get("required", True),
|
||||
}
|
||||
|
||||
def _job_preview(payload: dict[str, Any], check_paths: bool) -> dict[str, Any]:
|
||||
warnings: list[str] = []
|
||||
runtime_files: list[dict[str, str]] = []
|
||||
command_payload = {**payload, "require_dataset_files": check_paths}
|
||||
if check_paths:
|
||||
try:
|
||||
runtime_files = prepare_runtime_files(command_payload)
|
||||
except OSError as exc:
|
||||
return {
|
||||
"valid": False,
|
||||
"errors": [f"prepare runtime files failed: {exc}"],
|
||||
"warnings": warnings,
|
||||
"engine": str(payload.get("engine") or payload.get("training_engine") or "llama_factory"),
|
||||
"command": [],
|
||||
"command_text": "",
|
||||
"work_dir": os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"),
|
||||
"env": {},
|
||||
"runtime_files": [],
|
||||
"path_checks": [],
|
||||
}
|
||||
try:
|
||||
command = build_command(payload, os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"))
|
||||
command = build_command(command_payload, os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"))
|
||||
except ValueError as exc:
|
||||
return {
|
||||
"valid": False,
|
||||
@@ -295,23 +385,36 @@ def create_app() -> FastAPI:
|
||||
"command_text": "",
|
||||
"work_dir": os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"),
|
||||
"env": {},
|
||||
"runtime_files": runtime_files,
|
||||
"path_checks": [],
|
||||
}
|
||||
|
||||
errors: list[str] = []
|
||||
engine = str(payload.get("engine") or payload.get("training_engine") or "llama_factory")
|
||||
path_checks: list[dict[str, Any]] = []
|
||||
accelerator: dict[str, Any] = {}
|
||||
if check_paths and engine != "smoke":
|
||||
path_checks = [
|
||||
_check_path_item(
|
||||
{
|
||||
"name": "model_name_or_path",
|
||||
"path": payload.get("model_name_or_path") or payload.get("base_model") or "",
|
||||
"path": payload.get("model_name_or_path") or payload.get("base_model") or payload.get("base_model_path") or "",
|
||||
"type": "any",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
]
|
||||
if engine in {"merge", "export", "llama_factory_export"} and payload.get("adapter_name_or_path"):
|
||||
path_checks.append(
|
||||
_check_path_item(
|
||||
{
|
||||
"name": "adapter_name_or_path",
|
||||
"path": payload.get("adapter_name_or_path"),
|
||||
"type": "any",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
if payload.get("dataset_dir"):
|
||||
path_checks.append(
|
||||
_check_path_item(
|
||||
@@ -341,6 +444,10 @@ def create_app() -> FastAPI:
|
||||
errors.append(f"training command not found: {command.command[0]}")
|
||||
if not Path(command.work_dir).exists():
|
||||
errors.append(f"llama_factory_home not found: {command.work_dir}")
|
||||
if engine not in {"merge", "export", "llama_factory_export"}:
|
||||
accelerator_errors, accelerator_warnings, accelerator = _validate_training_accelerator(payload)
|
||||
errors.extend(accelerator_errors)
|
||||
warnings.extend(accelerator_warnings)
|
||||
elif engine == "smoke":
|
||||
warnings.append("smoke engine skips model and dataset path checks")
|
||||
|
||||
@@ -353,6 +460,8 @@ def create_app() -> FastAPI:
|
||||
"command_text": " ".join(command.command),
|
||||
"work_dir": command.work_dir,
|
||||
"env": command.env,
|
||||
"runtime_files": runtime_files,
|
||||
"accelerator": accelerator,
|
||||
"path_checks": path_checks,
|
||||
}
|
||||
|
||||
@@ -373,6 +482,8 @@ def create_app() -> FastAPI:
|
||||
dataset_root = Path(os.getenv("YG_FT_DATASET_ROOT", str(data_root / "datasets")))
|
||||
output_root = Path(os.getenv("YG_FT_OUTPUT_ROOT", str(data_root / "outputs")))
|
||||
llama_factory_home = Path(os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"))
|
||||
gpu_items = gpu_resources()
|
||||
torch_cuda = torch_cuda_status()
|
||||
return {
|
||||
"status": "ok",
|
||||
"api_version": "v1",
|
||||
@@ -391,8 +502,10 @@ def create_app() -> FastAPI:
|
||||
"llama_factory_version": os.getenv("LLAMA_FACTORY_VERSION", ""),
|
||||
"execution_mode": execution_mode(),
|
||||
"gpu_count": _int_env("COMPUTE_GPU_COUNT", 0),
|
||||
"nvidia_gpu_count": len(gpu_items),
|
||||
"torch_cuda": torch_cuda,
|
||||
"gpu_discovery_endpoint": f"{route_prefix}/compute/resources/gpus",
|
||||
"capabilities": ["gpu_discovery", "llama_factory", "file_gateway", "job_polling"],
|
||||
"capabilities": ["gpu_discovery", "torch_cuda_diagnostics", "llama_factory", "file_gateway", "job_polling"],
|
||||
}
|
||||
|
||||
@app.get(f"{route_prefix}/v1/compute/jobs")
|
||||
@@ -466,6 +579,11 @@ def create_app() -> FastAPI:
|
||||
|
||||
@app.post(f"{route_prefix}/compute/jobs")
|
||||
async def create_job(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
payload = {**payload, "require_dataset_files": True}
|
||||
try:
|
||||
prepare_runtime_files(payload)
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=400, detail=f"prepare runtime files failed: {exc}")
|
||||
try:
|
||||
command = build_command(payload, os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"))
|
||||
except ValueError as exc:
|
||||
|
||||
Reference in New Issue
Block a user