feat: 推理与评测支持多卡 GPU 选择,训练任务实时 GPU 监控

- 新增 GPU 选择归一化 helper,统一前端各形态的选择(gpu_indices/gpus/gpu_id)
- 评测与推理支持同一节点内多卡选择,校验所选 GPU 空闲后再派发
- 新增 /fine-tune/{id}/gpu-status 接口,训练日志页展示实时 GPU 指标
- 算力节点推理加载支持 CUDA_VISIBLE_DEVICES 多卡可见,nvidia-smi 进程级监控
- GPU 占用跟踪细化为按卡记录,覆盖评测任务、推理模型与对比任务

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wuyongtao
2026-08-19 10:44:56 +08:00
parent c3e96ae61b
commit 9379f93633
13 changed files with 360 additions and 48 deletions

View File

@@ -221,6 +221,35 @@ def create_app() -> FastAPI:
return fallback_gpu_resources()
items: list[dict[str, Any]] = []
processes_by_uuid: dict[str, list[dict[str, Any]]] = {}
try:
process_result = subprocess.run(
[
"nvidia-smi",
"--query-compute-apps=gpu_uuid,pid,process_name,used_memory",
"--format=csv,noheader,nounits",
],
check=True,
capture_output=True,
text=True,
timeout=5,
)
for process_line in process_result.stdout.splitlines():
process_parts = [part.strip() for part in process_line.split(",")]
if len(process_parts) < 4:
continue
process_uuid, pid, process_name, used_memory = process_parts[:4]
processes_by_uuid.setdefault(process_uuid, []).append(
{
"pid": int(_safe_float(pid)),
"name": process_name,
"memory_used_gb": round(_safe_float(used_memory) / 1024, 2),
}
)
except Exception:
# Some driver/runtime combinations do not expose compute-apps;
# utilization and memory metrics remain useful without processes.
pass
for line in result.stdout.splitlines():
parts = [part.strip() for part in line.split(",")]
if len(parts) < 9:
@@ -244,7 +273,7 @@ def create_app() -> FastAPI:
"temperature": int(_safe_float(temp)),
"power_w": round(_safe_float(power), 1),
"power_limit_w": round(_safe_float(power_limit), 1),
"processes": [],
"processes": processes_by_uuid.get(uuid, []),
}
)
return items
@@ -747,6 +776,23 @@ def create_app() -> FastAPI:
infer_backend: str (default: "huggingface")
infer_dtype: str (default: "auto")
"""
requested_gpus = payload.get("gpu_indices")
if requested_gpus is None:
requested_gpus = payload.get("gpus") or []
try:
requested_gpus = sorted({int(item) for item in requested_gpus})
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"invalid GPU selection: {exc}") from exc
if any(item < 0 for item in requested_gpus):
raise HTTPException(status_code=400, detail="GPU index must be non-negative")
if requested_gpus:
known_gpus = {int(item.get("gpu_index", item.get("id", -1))) for item in gpu_resources()}
missing = sorted(set(requested_gpus) - known_gpus)
if missing:
raise HTTPException(status_code=409, detail=f"requested GPU not found: {missing}")
conflict = sorted(set(requested_gpus).intersection(process_manager.locked_gpus()))
if conflict:
raise HTTPException(status_code=409, detail=f"GPU already used by another compute job: {conflict}")
session = get_inference_session()
result = session.load(
model_name_or_path=payload.get("model_name_or_path", ""),
@@ -754,6 +800,7 @@ def create_app() -> FastAPI:
template=payload.get("template", "qwen"),
infer_backend=payload.get("infer_backend", "huggingface"),
infer_dtype=payload.get("infer_dtype", "auto"),
gpu_indices=requested_gpus,
)
return result