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:
@@ -32,16 +32,26 @@ def _task_for_compute_job(job_id: str) -> dict[str, Any] | None:
|
||||
return next((task for task in get_platform_store().tasks() if task.get("compute_job_id") == job_id), None)
|
||||
|
||||
|
||||
def _node_for_compute_job_record(job_id: str) -> dict[str, Any] | None:
|
||||
store = get_platform_store()
|
||||
try:
|
||||
record = store.compute_job(job_id)
|
||||
except KeyError:
|
||||
return None
|
||||
return next((node for node in store.compute_nodes() if node["id"] == record.get("node_id")), None)
|
||||
|
||||
|
||||
async def _submit_fine_tune_task(store: Any, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
task_id = str(payload.get("task_id") or payload.get("id") or "")
|
||||
if task_id and get_settings().compute_mode != "simulator":
|
||||
try:
|
||||
preflight = await _fine_tune_preflight(store, task_id, payload, validate=True)
|
||||
preflight = await _fine_tune_preflight(store, task_id, payload, validate=True, sync_resources=True)
|
||||
except Exception as exc: # noqa: BLE001 - task has not entered running state yet
|
||||
raise RuntimeError(f"preflight failed: {exc}") from exc
|
||||
if not preflight["valid"]:
|
||||
errors = "; ".join(preflight.get("errors") or ["preflight failed"])
|
||||
raise RuntimeError(f"preflight failed: {errors}")
|
||||
payload = {**payload, "compute_node_id": preflight["node"]["id"]}
|
||||
task = store.start_task(payload)
|
||||
if get_settings().compute_mode == "simulator":
|
||||
return task
|
||||
@@ -55,8 +65,20 @@ async def _fine_tune_preflight(
|
||||
task_id: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
validate: bool = True,
|
||||
sync_resources: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
node, job_payload = store.prepare_compute_job_payload(task_id, payload or {})
|
||||
sync_results: list[dict[str, Any]] = []
|
||||
sync_errors: list[str] = []
|
||||
if sync_resources and get_settings().compute_mode != "simulator":
|
||||
try:
|
||||
sync_results = await _sync_training_dataset_to_compute_node(
|
||||
store,
|
||||
node,
|
||||
str(job_payload.get("train_dataset_id") or ""),
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - return as preflight error for page visibility
|
||||
sync_errors.append(str(exc))
|
||||
if get_settings().compute_mode == "simulator":
|
||||
preview = {
|
||||
"valid": True,
|
||||
@@ -73,6 +95,7 @@ async def _fine_tune_preflight(
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
preview = await (client.validate_job(job_payload) if validate else client.preview_job(job_payload))
|
||||
errors = list(preview.get("errors") or [])
|
||||
errors.extend(sync_errors)
|
||||
warnings = list(preview.get("warnings") or [])
|
||||
if not node.get("enabled"):
|
||||
errors.append(f"compute node disabled: {node.get('code')}")
|
||||
@@ -92,6 +115,7 @@ async def _fine_tune_preflight(
|
||||
},
|
||||
"job_payload": job_payload,
|
||||
"preview": preview,
|
||||
"sync_results": sync_results,
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +221,21 @@ async def delete_trained_model(model_id: str, type: str = Query(default="merged"
|
||||
return ok({"deleted": model_id, "type": type})
|
||||
|
||||
|
||||
@router.get("/model-manage/trained-models/{model_id}/artifacts")
|
||||
async def trained_model_artifacts(model_id: str) -> dict[str, Any]:
|
||||
return ok(get_platform_store().model_artifacts(model_id))
|
||||
|
||||
|
||||
@router.get("/model-manage/trained-models/{model_id}/lineage")
|
||||
async def trained_model_lineage(model_id: str) -> dict[str, Any]:
|
||||
return ok(get_platform_store().model_lineage(model_id))
|
||||
|
||||
|
||||
@router.get("/model-manage/export-jobs")
|
||||
async def model_export_jobs(trained_model_id: str | None = Query(default=None)) -> dict[str, Any]:
|
||||
return ok(get_platform_store().model_export_jobs(trained_model_id))
|
||||
|
||||
|
||||
@router.get("/model-manage/name/{name}")
|
||||
async def model_by_name(name: str) -> dict[str, Any]:
|
||||
try:
|
||||
@@ -254,7 +293,51 @@ async def delete_model(model_id: str) -> dict[str, Any]:
|
||||
|
||||
@router.post("/model-manage/merge")
|
||||
async def merge_model(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return ok({"job_id": f"merge_{uuid.uuid4().hex[:12]}", "status": "queued", **payload})
|
||||
store = get_platform_store()
|
||||
trained_model_id = str(payload.get("trained_model_id") or payload.get("model_id") or payload.get("model_name") or "")
|
||||
trained_model = next(
|
||||
(
|
||||
item
|
||||
for item in store.trained_models()
|
||||
if trained_model_id and (item["id"] == trained_model_id or item["name"] == trained_model_id)
|
||||
),
|
||||
None,
|
||||
)
|
||||
base_model_path = payload.get("base_model_path") or (trained_model and trained_model.get("base_model_path"))
|
||||
adapter_path = payload.get("adapter_path") or payload.get("adapter_name_or_path") or (trained_model and trained_model.get("merged_path"))
|
||||
if not base_model_path:
|
||||
raise fail(400, "base_model_path is required")
|
||||
if not adapter_path:
|
||||
raise fail(400, "adapter_path is required")
|
||||
node = store.schedule_node({**payload, "gpus": payload.get("gpus") or []})
|
||||
health = node.get("health_detail") or {}
|
||||
output_root = str(health.get("output_root") or f"{node['data_root'].rstrip('/')}/outputs")
|
||||
output_name = str(payload.get("output_model_name") or payload.get("merged_model_name") or f"{trained_model_id or 'model'}-merged")
|
||||
output_dir = str(payload.get("output_dir") or f"{output_root.rstrip('/')}/{output_name}")
|
||||
job_payload = {
|
||||
**payload,
|
||||
"id": str(payload.get("job_id") or f"merge_{uuid.uuid4().hex[:12]}"),
|
||||
"name": output_name,
|
||||
"engine": "merge",
|
||||
"base_model": base_model_path,
|
||||
"model_name_or_path": base_model_path,
|
||||
"adapter_name_or_path": adapter_path,
|
||||
"output_dir": output_dir,
|
||||
"template": payload.get("template", "qwen"),
|
||||
"train_method": payload.get("train_method", "lora"),
|
||||
"gpus": payload.get("gpus") or [],
|
||||
"trained_model_id": trained_model["id"] if trained_model else trained_model_id,
|
||||
"model_name": trained_model["name"] if trained_model else payload.get("model_name"),
|
||||
}
|
||||
if get_settings().compute_mode == "simulator":
|
||||
job = {"id": job_payload["id"], "status": "queued", "progress": 10, "command": [], "output_dir": output_dir}
|
||||
else:
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
preview = await client.validate_job(job_payload)
|
||||
if not preview.get("valid", False):
|
||||
raise fail(409, "; ".join(preview.get("errors") or ["merge preflight failed"]))
|
||||
job = await client.create_job(job_payload)
|
||||
return ok(store.record_model_merge_job(node, job_payload, job, trained_model["id"] if trained_model else trained_model_id))
|
||||
|
||||
|
||||
@router.get("/dataset-manage/preview/{file_id}")
|
||||
@@ -364,6 +447,47 @@ async def _sync_dataset_file_to_compute_nodes(
|
||||
return results
|
||||
|
||||
|
||||
async def _sync_training_dataset_to_compute_node(
|
||||
store: Any,
|
||||
node: dict[str, Any],
|
||||
dataset_id: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
if not dataset_id:
|
||||
raise RuntimeError("train_dataset_id is required")
|
||||
files = store.training_dataset_files(dataset_id)
|
||||
if not files:
|
||||
raise RuntimeError(f"dataset has no uploaded file: {dataset_id}")
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
results: list[dict[str, Any]] = []
|
||||
for item in files:
|
||||
target_name = Path(str(item.get("name") or f"{item['id']}.jsonl")).name
|
||||
result = await client.upload_file(
|
||||
target_name,
|
||||
str(item.get("content") or "").encode("utf-8"),
|
||||
f"datasets/{dataset_id}/{target_name}",
|
||||
resource_type="dataset",
|
||||
resource_id=dataset_id,
|
||||
)
|
||||
store.upsert_resource_replica(
|
||||
node["id"],
|
||||
"dataset",
|
||||
dataset_id,
|
||||
str(result.get("local_path") or ""),
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"node_id": node["id"],
|
||||
"node_code": node.get("code"),
|
||||
"file_id": item.get("id"),
|
||||
"name": target_name,
|
||||
"local_path": result.get("local_path"),
|
||||
"byte_size": result.get("byte_size"),
|
||||
"checksum_sha256": result.get("checksum_sha256"),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
@router.post("/dataset-manage/upload/{dataset_id}")
|
||||
async def upload_dataset_files(
|
||||
dataset_id: str,
|
||||
@@ -543,6 +667,10 @@ async def fine_tune_logs(
|
||||
if node:
|
||||
try:
|
||||
logs = await ComputeNodeClient(node["api_base_url"]).job_logs(task["compute_job_id"], tail_lines, offset, limit)
|
||||
try:
|
||||
store.record_training_log_metrics(task_id, str(logs.get("content") or ""))
|
||||
except Exception:
|
||||
pass
|
||||
if task.get("status") in {"queued", "running", "failed", "stopped", "completed"}:
|
||||
try:
|
||||
job = await ComputeNodeClient(node["api_base_url"]).get_job(task["compute_job_id"])
|
||||
@@ -613,18 +741,36 @@ async def delete_fine_tune(task_id: str) -> dict[str, Any]:
|
||||
|
||||
@router.get("/fine-tune/{task_id}/overview")
|
||||
async def fine_tune_overview(task_id: str) -> dict[str, Any]:
|
||||
task = get_platform_store().task(task_id)
|
||||
return ok({"task": task, "progress": get_platform_store().progress(task_id)})
|
||||
store = get_platform_store()
|
||||
task = store.task(task_id)
|
||||
return ok(
|
||||
{
|
||||
"task": task,
|
||||
"progress": store.progress(task_id),
|
||||
"metrics": store.task_metrics(task_id),
|
||||
"checkpoints": store.task_checkpoints(task_id),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/fine-tune/{task_id}/checkpoints")
|
||||
async def fine_tune_checkpoints(task_id: str) -> dict[str, Any]:
|
||||
task = get_platform_store().task(task_id)
|
||||
checkpoints = []
|
||||
for step in [50, 100, 150]:
|
||||
if task.get("progress", 0) >= min(100, step // 2):
|
||||
checkpoints.append({"step": step, "path": f"/data/yg-ft/outputs/{task['name']}/checkpoint-{step}"})
|
||||
return ok(checkpoints)
|
||||
store = get_platform_store()
|
||||
try:
|
||||
store.task(task_id)
|
||||
except KeyError:
|
||||
raise fail(404, "fine tune task not found")
|
||||
return ok(store.task_checkpoints(task_id))
|
||||
|
||||
|
||||
@router.get("/fine-tune/{task_id}/metrics")
|
||||
async def fine_tune_metrics(task_id: str) -> dict[str, Any]:
|
||||
store = get_platform_store()
|
||||
try:
|
||||
store.task(task_id)
|
||||
except KeyError:
|
||||
raise fail(404, "fine tune task not found")
|
||||
return ok(store.task_metrics(task_id))
|
||||
|
||||
|
||||
@router.get("/model-eval")
|
||||
@@ -909,6 +1055,142 @@ async def compute_node_replicas(node_id: str) -> dict[str, Any]:
|
||||
return ok(get_platform_store().replicas(node_id))
|
||||
|
||||
|
||||
@router.get("/compute/nodes/{node_id}/replicas/drift")
|
||||
async def compute_node_replica_drift(node_id: str) -> dict[str, Any]:
|
||||
store = get_platform_store()
|
||||
node = next((item for item in store.compute_nodes() if item["id"] == node_id), None)
|
||||
if not node:
|
||||
raise fail(404, "compute node not found")
|
||||
replicas = store.replicas(node_id)
|
||||
if not replicas:
|
||||
return ok({"node_id": node_id, "items": [], "drifted": 0})
|
||||
paths = [
|
||||
{
|
||||
"name": replica["id"],
|
||||
"path": replica["local_path"],
|
||||
"type": "any",
|
||||
"required": True,
|
||||
}
|
||||
for replica in replicas
|
||||
]
|
||||
try:
|
||||
result = await ComputeNodeClient(node["api_base_url"]).check_paths(paths)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise fail(502, f"replica drift check failed: {exc}")
|
||||
check_map = {str(item.get("name")): item for item in result.get("items") or []}
|
||||
items = []
|
||||
for replica in replicas:
|
||||
check = check_map.get(replica["id"], {})
|
||||
updated = store.update_resource_replica_check(
|
||||
replica["id"],
|
||||
bool(check.get("ok")),
|
||||
int(check.get("byte_size") or replica.get("byte_size") or 0),
|
||||
"" if check.get("ok") else f"path not available: {replica['local_path']}",
|
||||
)
|
||||
items.append({**updated, "check": check})
|
||||
return ok({"node_id": node_id, "items": items, "drifted": len([item for item in items if item.get("sync_status") == "drifted"])})
|
||||
|
||||
|
||||
@router.post("/compute/nodes/{node_id}/replicas/repair")
|
||||
async def compute_node_replica_repair(node_id: str, payload: dict[str, Any] | None = Body(default=None)) -> dict[str, Any]:
|
||||
store = get_platform_store()
|
||||
node = next((item for item in store.compute_nodes() if item["id"] == node_id), None)
|
||||
if not node:
|
||||
raise fail(404, "compute node not found")
|
||||
payload = payload or {}
|
||||
replica_ids = payload.get("replica_ids") or [
|
||||
item["id"] for item in store.replicas(node_id) if item.get("sync_status") in {"drifted", "failed", "repair_pending"}
|
||||
]
|
||||
updated = store.mark_resource_replica_repair_pending([str(item) for item in replica_ids])
|
||||
sync_id = store.create_sync_job(
|
||||
node_id,
|
||||
{
|
||||
"resources": [
|
||||
{
|
||||
"resource_type": item.get("resource_type"),
|
||||
"resource_id": item.get("resource_id"),
|
||||
"replica_id": item.get("id"),
|
||||
"target_path": item.get("local_path"),
|
||||
}
|
||||
for item in updated
|
||||
]
|
||||
},
|
||||
)
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
repaired = []
|
||||
failures = []
|
||||
for replica in updated:
|
||||
replica_id = str(replica["id"])
|
||||
resource_type = str(replica.get("resource_type") or "")
|
||||
resource_id = str(replica.get("resource_id") or "")
|
||||
try:
|
||||
if resource_type == "dataset":
|
||||
files = store.training_dataset_files(resource_id)
|
||||
if not files:
|
||||
raise RuntimeError(f"dataset has no uploaded file: {resource_id}")
|
||||
total_size = 0
|
||||
checksum = ""
|
||||
local_path = str(replica.get("local_path") or "")
|
||||
for item in files:
|
||||
filename = Path(str(item.get("name") or f"{item['id']}.jsonl")).name
|
||||
result = await client.upload_file(
|
||||
filename,
|
||||
str(item.get("content") or "").encode("utf-8"),
|
||||
f"datasets/{resource_id}/{filename}",
|
||||
resource_type="dataset",
|
||||
resource_id=resource_id,
|
||||
)
|
||||
total_size += int(result.get("byte_size") or 0)
|
||||
checksum = str(result.get("checksum_sha256") or checksum)
|
||||
local_path = str(result.get("local_path") or local_path)
|
||||
repaired.append(store.update_resource_replica_sync_result(replica_id, True, local_path, total_size, checksum))
|
||||
continue
|
||||
|
||||
source_path = ""
|
||||
target_relative_path = ""
|
||||
if resource_type == "model":
|
||||
model = store.model(resource_id)
|
||||
source_path = str(model.get("path") or "")
|
||||
target_relative_path = f"models/{Path(source_path).name}" if source_path else ""
|
||||
elif resource_type in {"trained_model", "model_artifact"}:
|
||||
if resource_type == "trained_model":
|
||||
artifacts = store.model_artifacts(resource_id)
|
||||
artifact = next((item for item in artifacts if item.get("path")), None)
|
||||
else:
|
||||
artifact = store.model_artifact(resource_id)
|
||||
source_path = str((artifact or {}).get("path") or replica.get("local_path") or "")
|
||||
target_relative_path = f"outputs/{Path(source_path).name}" if source_path else ""
|
||||
else:
|
||||
source_path = str(payload.get("source_path") or replica.get("source_path") or replica.get("local_path") or "")
|
||||
target_relative_path = str(payload.get("target_relative_path") or "")
|
||||
|
||||
if not source_path:
|
||||
raise RuntimeError(f"authoritative source path not found for {resource_type}:{resource_id}")
|
||||
result = await client.import_local_file(
|
||||
{
|
||||
"source_path": source_path,
|
||||
"target_relative_path": target_relative_path,
|
||||
"resource_type": resource_type,
|
||||
"resource_id": resource_id,
|
||||
}
|
||||
)
|
||||
repaired.append(
|
||||
store.update_resource_replica_sync_result(
|
||||
replica_id,
|
||||
True,
|
||||
str(result.get("local_path") or replica.get("local_path") or ""),
|
||||
int(result.get("byte_size") or 0),
|
||||
str(result.get("checksum_sha256") or ""),
|
||||
)
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - collect all replica repair failures
|
||||
error = str(exc)
|
||||
failures.append({"replica_id": replica_id, "resource_type": resource_type, "resource_id": resource_id, "error": error})
|
||||
repaired.append(store.update_resource_replica_sync_result(replica_id, False, None, int(replica.get("byte_size") or 0), "", error))
|
||||
store.update_sync_job(sync_id, "failed" if failures else "completed", 100 if not failures else 99, completed=True)
|
||||
return ok({"sync": store.sync_job(sync_id), "replicas": repaired, "failed": failures})
|
||||
|
||||
|
||||
@router.get("/compute/nodes/{node_id}/engines")
|
||||
async def compute_node_engines(node_id: str) -> dict[str, Any]:
|
||||
node = next((item for item in get_platform_store().compute_nodes() if item["id"] == node_id), None)
|
||||
@@ -955,7 +1237,11 @@ async def compute_queue() -> dict[str, Any]:
|
||||
async def compute_job_detail(job_id: str) -> dict[str, Any]:
|
||||
task = _task_for_compute_job(job_id)
|
||||
if not task:
|
||||
raise fail(404, "compute job not found")
|
||||
node = _node_for_compute_job_record(job_id)
|
||||
if not node:
|
||||
raise fail(404, "compute job not found")
|
||||
job = await ComputeNodeClient(node["api_base_url"]).get_job(job_id)
|
||||
return ok(get_platform_store().sync_model_merge_job(job_id, job))
|
||||
node = _node_for_task(task)
|
||||
if not node:
|
||||
raise fail(404, "compute node not found")
|
||||
@@ -966,7 +1252,11 @@ async def compute_job_detail(job_id: str) -> dict[str, Any]:
|
||||
async def compute_job_stop(job_id: str) -> dict[str, Any]:
|
||||
task = _task_for_compute_job(job_id)
|
||||
if not task:
|
||||
raise fail(404, "compute job not found")
|
||||
node = _node_for_compute_job_record(job_id)
|
||||
if not node:
|
||||
raise fail(404, "compute job not found")
|
||||
job = await ComputeNodeClient(node["api_base_url"]).stop_job(job_id)
|
||||
return ok(get_platform_store().sync_model_merge_job(job_id, job))
|
||||
node = _node_for_task(task)
|
||||
if not node:
|
||||
raise fail(404, "compute node not found")
|
||||
@@ -984,7 +1274,10 @@ async def compute_job_logs(
|
||||
) -> dict[str, Any]:
|
||||
task = _task_for_compute_job(job_id)
|
||||
if not task:
|
||||
raise fail(404, "compute job not found")
|
||||
node = _node_for_compute_job_record(job_id)
|
||||
if not node:
|
||||
raise fail(404, "compute job not found")
|
||||
return ok(await ComputeNodeClient(node["api_base_url"]).job_logs(job_id, tail_lines, offset, limit))
|
||||
node = _node_for_task(task)
|
||||
if not node:
|
||||
raise fail(404, "compute node not found")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,45 @@ CREATE TABLE IF NOT EXISTS trained_models (
|
||||
merged_path TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_lineage (
|
||||
id TEXT PRIMARY KEY,
|
||||
child_resource_type TEXT NOT NULL,
|
||||
child_resource_id TEXT NOT NULL,
|
||||
parent_resource_type TEXT NOT NULL,
|
||||
parent_resource_id TEXT NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
compute_job_id TEXT,
|
||||
payload TEXT NOT NULL,
|
||||
create_time TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_artifacts (
|
||||
id TEXT PRIMARY KEY,
|
||||
model_id TEXT NOT NULL,
|
||||
model_kind TEXT NOT NULL,
|
||||
artifact_type TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
checksum_sha256 TEXT,
|
||||
metadata TEXT NOT NULL,
|
||||
compute_job_id TEXT,
|
||||
create_time TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_export_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
trained_model_id TEXT,
|
||||
compute_job_id TEXT NOT NULL,
|
||||
node_id TEXT,
|
||||
export_type TEXT NOT NULL,
|
||||
quantization_bit INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL,
|
||||
output_dir TEXT,
|
||||
payload TEXT NOT NULL,
|
||||
create_time TEXT NOT NULL,
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS datasets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
@@ -111,6 +150,62 @@ CREATE TABLE IF NOT EXISTS fine_tune_tasks (
|
||||
compute_job_id TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fine_tune_metrics (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES fine_tune_tasks(id) ON DELETE CASCADE,
|
||||
step INTEGER NOT NULL,
|
||||
epoch DOUBLE PRECISION,
|
||||
loss DOUBLE PRECISION,
|
||||
grad_norm DOUBLE PRECISION,
|
||||
learning_rate DOUBLE PRECISION,
|
||||
raw TEXT NOT NULL,
|
||||
create_time TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fine_tune_checkpoints (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES fine_tune_tasks(id) ON DELETE CASCADE,
|
||||
step INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
create_time TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS compute_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT REFERENCES fine_tune_tasks(id) ON DELETE SET NULL,
|
||||
node_id TEXT REFERENCES compute_nodes(id) ON DELETE SET NULL,
|
||||
engine TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
command TEXT NOT NULL,
|
||||
output_dir TEXT,
|
||||
log_file TEXT,
|
||||
payload TEXT NOT NULL,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gpu_allocations (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT REFERENCES fine_tune_tasks(id) ON DELETE CASCADE,
|
||||
compute_job_id TEXT,
|
||||
node_id TEXT REFERENCES compute_nodes(id) ON DELETE CASCADE,
|
||||
gpu_index INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
create_time TEXT NOT NULL,
|
||||
released_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scheduler_locks (
|
||||
lock_key TEXT PRIMARY KEY,
|
||||
owner TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resource_replicas (
|
||||
id TEXT PRIMARY KEY,
|
||||
node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
|
||||
@@ -119,6 +214,10 @@ CREATE TABLE IF NOT EXISTS resource_replicas (
|
||||
local_path TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
sync_status TEXT NOT NULL,
|
||||
checksum_sha256 TEXT,
|
||||
byte_size BIGINT NOT NULL DEFAULT 0,
|
||||
last_checked_at TEXT,
|
||||
last_error TEXT,
|
||||
create_time TEXT NOT NULL
|
||||
);
|
||||
|
||||
@@ -160,6 +259,20 @@ CREATE TABLE IF NOT EXISTS compare_tasks (
|
||||
CREATE INDEX IF NOT EXISTS idx_fine_tune_status ON fine_tune_tasks(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_fine_tune_compute_job ON fine_tune_tasks(compute_job_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_fine_tune_compute_node_status ON fine_tune_tasks(compute_node_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_lineage_child ON model_lineage(child_resource_type, child_resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_lineage_parent ON model_lineage(parent_resource_type, parent_resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_artifacts_model ON model_artifacts(model_kind, model_id, artifact_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_export_jobs_model ON model_export_jobs(trained_model_id, create_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_export_jobs_compute ON model_export_jobs(compute_job_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_fine_tune_metrics_task_step ON fine_tune_metrics(task_id, step);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_fine_tune_metrics_task_step_epoch ON fine_tune_metrics(task_id, step, epoch);
|
||||
CREATE INDEX IF NOT EXISTS idx_fine_tune_checkpoints_task_step ON fine_tune_checkpoints(task_id, step);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_fine_tune_checkpoints_task_path ON fine_tune_checkpoints(task_id, path);
|
||||
CREATE INDEX IF NOT EXISTS idx_compute_jobs_task ON compute_jobs(task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_compute_jobs_node_status ON compute_jobs(node_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpu_allocations_node_status ON gpu_allocations(node_id, status);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_gpu_allocations_active ON gpu_allocations(node_id, gpu_index) WHERE status IN ('allocated','running');
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduler_locks_expires ON scheduler_locks(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_dataset_files_dataset ON dataset_files(dataset_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpus_node ON gpus(node_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_gpus_node_index ON gpus(node_id, gpu_index);
|
||||
|
||||
@@ -20,8 +20,25 @@ async def poll_compute_jobs_once() -> dict[str, Any]:
|
||||
failed.append({"task_id": task["id"], "error": "compute node not found"})
|
||||
continue
|
||||
try:
|
||||
job = await ComputeNodeClient(node["api_base_url"]).get_job(task["compute_job_id"])
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
job = await client.get_job(task["compute_job_id"])
|
||||
try:
|
||||
logs = await client.job_logs(task["compute_job_id"], tail_lines=5000)
|
||||
store.record_training_log_metrics(task["id"], str(logs.get("content") or ""))
|
||||
except Exception:
|
||||
pass
|
||||
synced.append(store.apply_compute_job(task["id"], job))
|
||||
except Exception as exc: # noqa: BLE001 - keep polling other jobs
|
||||
failed.append({"task_id": task["id"], "error": str(exc)})
|
||||
return {"synced": len(synced), "failed": failed, "items": synced}
|
||||
standalone_synced: list[dict[str, Any]] = []
|
||||
for record in store.active_standalone_compute_jobs():
|
||||
node = next((item for item in store.compute_nodes() if item["id"] == record.get("node_id")), None)
|
||||
if not node:
|
||||
failed.append({"job_id": record["id"], "error": "compute node not found"})
|
||||
continue
|
||||
try:
|
||||
job = await ComputeNodeClient(node["api_base_url"]).get_job(record["id"])
|
||||
standalone_synced.append(store.sync_model_merge_job(record["id"], job))
|
||||
except Exception as exc: # noqa: BLE001 - keep polling other jobs
|
||||
failed.append({"job_id": record["id"], "error": str(exc)})
|
||||
return {"synced": len(synced) + len(standalone_synced), "failed": failed, "items": synced, "standalone": standalone_synced}
|
||||
|
||||
Reference in New Issue
Block a user