main #7

Merged
caoxiaozhu merged 18 commits from main into dev 2026-07-21 14:25:38 +08:00
24 changed files with 571 additions and 630 deletions
Showing only changes of commit a72b8f1e4b - Show all commits

View File

@@ -2,7 +2,7 @@
YG_FT 是一个面向企业治理场景的模型微调平台,覆盖用户中心、多租户、项目隔离、数据集管理、模型管理、训练任务、评测、推理、审批流、审计留存、算力调度和训练引擎适配。
当前前端已有基础页面,后端与算力平台已按多人协作开发方式建立工程骨架,并开始实现第一版可演示闭环能力。第一版实现不是临时代码,工程模块、配置项、接口标签和数据库对象均按后续正式开发可延续的方式命名
当前前端已有基础页面,后端与算力平台已按多人协作开发方式建立工程骨架,并开始实现正式系统主链路能力。当前代码和 SQL 均作为后续生产演进基线维护,不再以一次性演示或静态 Mock 为开发准则
## 总体架构
@@ -38,12 +38,12 @@ YG_FT/
| 训练引擎 | 当前固定接入 LLaMA-Factory预留其他训练平台适配标准 | `compute/engines/` |
| 数据层 | PostgreSQL、Redis、本地文件存储、日志归档 | `docs/postgres-schema.sql` |
## 第一版能力
## 当前开发基线
- 使用 FastAPI 提供统一 API 响应结构 `{ code, message, data }`
- 本地运行阶段使用 SQLite 初始化样例用户、模型、数据集、算力节点、GPU微调任务。
- 本地运行阶段统一使用 PostgreSQL后端启动时会在 PG 中初始化当前运行表和系统内置账号;模型、数据集、算力节点、GPU微调任务等业务数据必须通过页面、接口或正式导入流程产生
- 支持登录、模型管理、数据集管理、微调任务创建/启动/停止/进度轮询。
- 支持训练日志、loss 指标、checkpoint 和训练产物的模拟生成
- 支持训练日志、loss 指标、checkpoint 和训练产物接口;真实训练执行器接入前,联调状态机必须通过显式环境变量开启
- 支持多算力节点、GPU、任务队列、资源副本和资源同步状态接口。
- 前端新增 `/compute` 算力节点页面展示节点地址、权重、标签、启用状态、GPU、队列和资源副本。
- `compute/engines/llama_factory/adapter.py` 提供 LLaMA-Factory 参数校验、命令生成和日志解析基础能力。
@@ -69,12 +69,14 @@ GET /modelTF/fine-tune
GET /modelTF/compute/nodes
```
本地运行时默认数据库路径
本地运行时默认 PostgreSQL 连接
```text
LOCAL_DB_PATH=./runtime/platform.db
DATABASE_URL=postgresql+psycopg://yg_ft:change_me@localhost:15432/yg_ft
```
本地启动前需要确保 PostgreSQL 已监听 `localhost:15432`,并已创建 `yg_ft` 数据库和 `yg_ft` 用户。后端启动后会自动创建当前运行表并写入内置管理员账号,运行数据统一写入 PostgreSQL。
## 前端启动
```bash
@@ -92,7 +94,7 @@ cd compute
uvicorn api.main:app --reload --port 19100
```
第一版默认 `COMPUTE_MODE=simulator`后续真实 GPU 接入时,在算力服务器上部署 Compute API、Agent、File Gateway 和 LLaMA-Factory应用平台通过 `compute_nodes.api_base_url``compute_nodes.file_gateway_url` 主动轮询。
默认 `COMPUTE_MODE=real`。真实 GPU 接入时,在每台算力服务器上部署 Compute API、Agent、File Gateway 和 LLaMA-Factory应用平台通过 `compute_nodes.api_base_url``compute_nodes.file_gateway_url` 主动轮询。仅在隔离联调环境可显式设置 `COMPUTE_MODE=simulator``COMPUTE_EXECUTION_MODE=simulator`
## 日志
@@ -112,10 +114,11 @@ logs/error-YYYY-MM-DD.log
## 主要文档
- `docs/platform-architecture-requirements.md`:平台需求、功能模块、页面补全建议。
- `docs/menu-functional-requirements.md`:当前菜单、二级路由、规划菜单、功能需求、接口和数据库映射。
- `docs/backend-api-design.md`FastAPI 接口分组、参数定义、权限说明。
- `docs/postgres-schema.sql`PostgreSQL 数据库脚本,包含权限、用户中心、多租户、审批、审计等模型。
- `docs/system-development-plan.md`多人协作开发计划按前端、后端、DB、部署拆分。
- `docs/first-version-development-plan.md`第一版可演示闭环开发计划覆盖前端、后端、DB、Compute Simulator、GPU 和 LLaMA-Factory 适配。
- `docs/first-version-development-plan.md`当前系统主链路开发计划覆盖前端、后端、DB、Compute API、GPU 和 LLaMA-Factory 适配。
- `docs/backend-logging.md`:后端日志模块使用说明。
- `docs/deployment-plan.md`:后期部署方案,覆盖单机算力服务器部署与应用/算力分离部署。
- `docker/README.md`Docker 部署入口,包含应用服务器和算力服务器两套 Compose 使用方式。

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from typing import Any
import uuid
from fastapi import APIRouter, Body, File, HTTPException, Query, UploadFile
from fastapi.responses import PlainTextResponse
@@ -147,7 +148,7 @@ 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": "merge-sample-job", "status": "queued", **payload})
return ok({"job_id": f"merge_{uuid.uuid4().hex[:12]}", "status": "queued", **payload})
@router.get("/dataset-manage/preview/{file_id}")
@@ -289,8 +290,11 @@ async def fine_tune_list() -> dict[str, Any]:
@router.post("/fine-tune")
async def create_fine_tune(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
task = get_platform_store().create_task(payload)
return ok({"id": task["id"]})
except ValueError as exc:
raise fail(400, str(exc))
@router.post("/fine-tune/start")
@@ -361,7 +365,10 @@ async def compute_nodes() -> dict[str, Any]:
@router.post("/compute/nodes")
async def create_compute_node(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return ok({"id": "node_sample_new", **payload, "status": "created"})
try:
return ok(get_platform_store().create_compute_node(payload))
except KeyError as exc:
raise fail(400, f"missing field: {exc}")
@router.put("/compute/nodes/{node_id}")

View File

@@ -16,8 +16,8 @@ class Settings:
app_env: str = os.getenv("APP_ENV", "local")
route_prefix: str = os.getenv("MODELTF_ROUTE_PREFIX", "/modelTF")
app_mode: str = os.getenv("APP_MODE", "local")
local_db_path: str = os.getenv("LOCAL_DB_PATH", "./runtime/platform.db")
compute_mode: str = os.getenv("COMPUTE_MODE", "simulator")
database_url: str = os.getenv("DATABASE_URL", "postgresql+psycopg://yg_ft:change_me@localhost:15432/yg_ft")
compute_mode: str = os.getenv("COMPUTE_MODE", "real")
compute_status_sync_mode: str = os.getenv("COMPUTE_STATUS_SYNC_MODE", "polling")
compute_poll_interval_seconds: int = _int_env("COMPUTE_POLL_INTERVAL_SECONDS", 3)
log_level: str = os.getenv("LOG_LEVEL", "INFO")

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
from __future__ import annotations
import json
import hashlib
import hmac
import math
import sqlite3
import secrets
import time
import uuid
from contextlib import contextmanager
@@ -10,6 +12,8 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterator
import psycopg
from app.core.config import get_settings
@@ -53,175 +57,133 @@ def new_id(prefix: str) -> str:
return f"{prefix}_{uuid.uuid4().hex[:12]}"
class PlatformStore:
"""Small SQLite-backed store for the first runnable platform version.
PASSWORD_HASH_ITERATIONS = 390_000
The production model is PostgreSQL. This store mirrors the API-facing subset
needed by the first system iteration so developers can run the app without
provisioning enterprise infrastructure first.
def hash_password(password: str) -> str:
salt = secrets.token_hex(16)
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), PASSWORD_HASH_ITERATIONS)
return f"pbkdf2_sha256${PASSWORD_HASH_ITERATIONS}${salt}${digest.hex()}"
def verify_password(password: str, stored: str) -> tuple[bool, bool]:
if not stored.startswith("pbkdf2_sha256$"):
return hmac.compare_digest(password, stored), True
try:
_, iterations, salt, expected = stored.split("$", 3)
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), int(iterations)).hex()
return hmac.compare_digest(digest, expected), False
except ValueError:
return False, False
def _psycopg_url(database_url: str) -> str:
return database_url.replace("postgresql+psycopg://", "postgresql://")
def _pg_sql(sql: str) -> str:
return sql.replace("?", "%s")
class PgRow(dict):
def __init__(self, columns: list[str], values: tuple[Any, ...]) -> None:
super().__init__(zip(columns, values))
self._values = values
def __getitem__(self, key: str | int) -> Any:
if isinstance(key, int):
return self._values[key]
return super().__getitem__(key)
class PgCursor:
def __init__(self, cursor: psycopg.Cursor[Any]) -> None:
self.cursor = cursor
def execute(self, sql: str, params: tuple[Any, ...] | list[Any] | None = None) -> "PgCursor":
self.cursor.execute(_pg_sql(sql), params)
return self
def fetchone(self) -> PgRow | None:
row = self.cursor.fetchone()
if row is None:
return None
return PgRow(self._columns(), tuple(row))
def fetchall(self) -> list[PgRow]:
columns = self._columns()
return [PgRow(columns, tuple(row)) for row in self.cursor.fetchall()]
def _columns(self) -> list[str]:
return [col.name for col in self.cursor.description or []]
class PgConnection:
def __init__(self, conn: psycopg.Connection[Any]) -> None:
self.conn = conn
def execute(self, sql: str, params: tuple[Any, ...] | list[Any] | None = None) -> PgCursor:
cursor = PgCursor(self.conn.cursor())
return cursor.execute(sql, params)
def executemany(self, sql: str, params_seq: list[tuple[Any, ...]] | list[list[Any]]) -> None:
with self.conn.cursor() as cursor:
cursor.executemany(_pg_sql(sql), params_seq)
def executescript(self, sql: str) -> None:
with self.conn.cursor() as cursor:
for statement in sql.split(";"):
statement = statement.strip()
if statement:
cursor.execute(statement)
def commit(self) -> None:
self.conn.commit()
def rollback(self) -> None:
self.conn.rollback()
def close(self) -> None:
self.conn.close()
class PlatformStore:
"""PostgreSQL-backed store for the first runnable platform version.
This store mirrors the API-facing subset needed by the first system
iteration while using the same PostgreSQL dependency as later production
development.
"""
def __init__(self, db_path: str | None = None) -> None:
def __init__(self, database_url: str | None = None) -> None:
settings = get_settings()
raw_path = db_path or settings.local_db_path
self.db_path = Path(raw_path)
if not self.db_path.is_absolute():
self.db_path = Path.cwd() / self.db_path
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self.database_url = _psycopg_url(database_url or settings.database_url)
self.ensure_schema()
self.ensure_seed_data()
@contextmanager
def connect(self) -> Iterator[sqlite3.Connection]:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
def connect(self) -> Iterator["PgConnection"]:
raw_conn = psycopg.connect(self.database_url)
conn = PgConnection(raw_conn)
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def ensure_schema(self) -> None:
schema_path = Path(__file__).with_name("sql") / "001_platform_runtime.sql"
with self.connect() as conn:
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
display_name TEXT NOT NULL,
role TEXT NOT NULL,
status TEXT NOT NULL,
permissions TEXT NOT NULL,
create_time TEXT NOT NULL,
last_login TEXT,
protected INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS models (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
purpose TEXT NOT NULL,
model_source TEXT NOT NULL,
description TEXT,
path TEXT,
api_url TEXT,
api_key TEXT,
online_model_name TEXT,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS trained_models (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
train_methods TEXT NOT NULL,
base_model_path TEXT,
create_time TEXT NOT NULL,
merged INTEGER NOT NULL DEFAULT 0,
merging INTEGER NOT NULL DEFAULT 0,
merged_path TEXT
);
CREATE TABLE IF NOT EXISTS datasets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
storage_type TEXT NOT NULL,
source TEXT NOT NULL,
task_id TEXT,
size TEXT,
count INTEGER NOT NULL DEFAULT 0,
description TEXT,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS dataset_files (
id TEXT PRIMARY KEY,
dataset_id TEXT NOT NULL,
name TEXT NOT NULL,
size TEXT,
content TEXT NOT NULL,
active_version_id TEXT NOT NULL,
versions TEXT NOT NULL,
create_time TEXT NOT NULL,
FOREIGN KEY(dataset_id) REFERENCES datasets(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS compute_nodes (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
api_base_url TEXT NOT NULL,
file_gateway_url TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
scheduler_status TEXT NOT NULL,
scheduler_weight INTEGER NOT NULL DEFAULT 100,
tags TEXT NOT NULL,
gpu_count INTEGER NOT NULL DEFAULT 0,
current_running_jobs INTEGER NOT NULL DEFAULT 0,
max_parallel_jobs INTEGER NOT NULL DEFAULT 2,
data_root TEXT NOT NULL,
model_root TEXT NOT NULL,
log_root TEXT NOT NULL,
last_health_check_at TEXT,
health_detail TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS gpus (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL,
gpu_index INTEGER NOT NULL,
uuid TEXT NOT NULL,
name TEXT NOT NULL,
memory_total_gb REAL NOT NULL,
power_limit_w REAL NOT NULL,
base_temperature INTEGER NOT NULL,
FOREIGN KEY(node_id) REFERENCES compute_nodes(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS fine_tune_tasks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
payload TEXT NOT NULL,
status TEXT NOT NULL,
progress INTEGER NOT NULL DEFAULT 0,
process_id INTEGER,
create_time TEXT NOT NULL,
start_time TEXT,
completed_at TEXT,
compute_node_id TEXT,
gpus TEXT NOT NULL,
sync_job_id TEXT
);
CREATE TABLE IF NOT EXISTS resource_replicas (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
local_path TEXT NOT NULL,
status TEXT NOT NULL,
sync_status TEXT NOT NULL,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS resource_sync_jobs (
id TEXT PRIMARY KEY,
target_node_id TEXT NOT NULL,
resources TEXT NOT NULL,
status TEXT NOT NULL,
progress INTEGER NOT NULL DEFAULT 0,
create_time TEXT NOT NULL,
completed_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_fine_tune_status ON fine_tune_tasks(status);
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 INDEX IF NOT EXISTS idx_replicas_resource ON resource_replicas(resource_type, resource_id);
"""
)
conn.executescript(schema_path.read_text(encoding="utf-8"))
columns = conn.execute(
"SELECT column_name FROM information_schema.columns WHERE table_name='users'"
).fetchall()
column_names = {row["column_name"] for row in columns}
if "password" in column_names and "password_hash" not in column_names:
conn.execute("ALTER TABLE users RENAME COLUMN password TO password_hash")
def ensure_seed_data(self) -> None:
with self.connect() as conn:
@@ -245,334 +207,10 @@ class PlatformStore:
conn.executemany(
"""
INSERT INTO users
(id, username, password, display_name, role, status, permissions, create_time, protected)
(id, username, password_hash, display_name, role, status, permissions, create_time, protected)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
[(u[0], u[1], u[2], u[3], u[4], u[5], json_dumps(u[6]), now, u[7]) for u in users],
)
models = [
(
"m_qwen25_7b",
"Qwen2.5-7B-Instruct",
"LLM",
"training",
"local",
"Sample base model for SFT and LoRA training.",
"/models/Qwen2.5-7B-Instruct",
),
(
"m_llama31_8b",
"Llama-3.1-8B-Instruct",
"LLM",
"training",
"local",
"Reserved base model path for LLaMA-Factory dry-run.",
"/models/Llama-3.1-8B-Instruct",
),
]
conn.executemany(
"""
INSERT INTO models
(id, name, type, purpose, model_source, description, path, create_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
[(*m, now) for m in models],
)
self._insert_dataset(
conn,
"ds_finance_sft",
"finance-sft-sample",
"train",
"Financial QA SFT samples for sample training.",
"finance-sft-sample.jsonl",
"\n".join(
[
'{"instruction":"Summarize revenue growth.","input":"Revenue grew from 10M to 13M.","output":"Revenue increased 30% year over year."}',
'{"instruction":"Classify risk.","input":"Customer has overdue payment for 90 days.","output":"High credit risk."}',
'{"instruction":"Draft an analyst note.","input":"Gross margin improved by 4 points.","output":"Margin expansion indicates stronger operating leverage."}',
]
),
now,
)
self._insert_dataset(
conn,
"ds_customer_service",
"customer-service-sample",
"train",
"Customer-service instruction tuning samples.",
"customer-service-sample.jsonl",
"\n".join(
[
'{"instruction":"Respond politely.","input":"My package is late.","output":"I am sorry for the delay. I can help check the latest shipment status."}',
'{"instruction":"Escalate request.","input":"I need a refund for a defective item.","output":"I will create a refund case and share the next steps."}',
]
),
now,
)
node_rows = [
(
"node_01",
"gpu-node-01",
"GPU Node 01",
"http://gpu-node-01:19100",
"http://gpu-node-01:19101",
1,
"online",
100,
["A800", "80GB", "llama_factory"],
4,
0,
2,
"/data/yg-ft",
"/models",
"/data/yg-ft/training-logs",
now,
{"mode": "simulator", "heartbeat": "ok"},
),
(
"node_02",
"gpu-node-02",
"GPU Node 02",
"http://gpu-node-02:19100",
"http://gpu-node-02:19101",
1,
"online",
60,
["4090", "24GB", "llama_factory"],
4,
0,
1,
"/data/yg-ft",
"/models",
"/data/yg-ft/training-logs",
now,
{"mode": "simulator", "heartbeat": "ok"},
),
]
conn.executemany(
"""
INSERT INTO compute_nodes
(id, code, name, api_base_url, file_gateway_url, enabled, scheduler_status,
scheduler_weight, tags, gpu_count, current_running_jobs, max_parallel_jobs,
data_root, model_root, log_root, last_health_check_at, health_detail)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
[
(
r[0],
r[1],
r[2],
r[3],
r[4],
r[5],
r[6],
r[7],
json_dumps(r[8]),
r[9],
r[10],
r[11],
r[12],
r[13],
r[14],
r[15],
json_dumps(r[16]),
)
for r in node_rows
],
)
for node_id, model_name, mem in [
("node_01", "NVIDIA A800-SXM4-80GB", 80),
("node_02", "NVIDIA GeForce RTX 4090", 24),
]:
for idx in range(4):
conn.execute(
"""
INSERT INTO gpus
(id, node_id, gpu_index, uuid, name, memory_total_gb, power_limit_w, base_temperature)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
f"{node_id}_gpu_{idx}",
node_id,
idx,
f"GPU-{node_id.upper()}-{idx}",
model_name,
mem,
300 if mem >= 80 else 450,
35 + idx,
),
)
self._insert_task(
conn,
task_id="ft_pending_sample",
name="pending-sft-sample",
status="pending",
progress=0,
create_time=now,
start_time=None,
completed_at=None,
node_id=None,
gpus=[],
process_id=None,
)
started = datetime.now(timezone.utc).timestamp() - 22
self._insert_task(
conn,
task_id="ft_running_sample",
name="running-sft-sample",
status="running",
progress=45,
create_time=now,
start_time=datetime.fromtimestamp(started, timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
completed_at=None,
node_id="node_01",
gpus=[0, 1],
process_id=42001,
)
self._insert_task(
conn,
task_id="ft_completed_sample",
name="completed-sft-sample",
status="completed",
progress=100,
create_time=now,
start_time=now,
completed_at=now,
node_id="node_01",
gpus=[2],
process_id=42002,
)
conn.execute(
"""
INSERT INTO trained_models
(id, name, train_methods, base_model_path, create_time, merged, merging, merged_path)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"tm_completed_sample",
"completed-sft-sample-lora",
json_dumps([{"name": "lora"}]),
"/models/Qwen2.5-7B-Instruct",
now,
0,
0,
"/data/yg-ft/outputs/completed-sft-sample/adapter",
),
)
def _insert_dataset(
self,
conn: sqlite3.Connection,
dataset_id: str,
name: str,
dataset_type: str,
description: str,
file_name: str,
content: str,
now: str,
) -> None:
lines = [line for line in content.splitlines() if line.strip()]
size = f"{max(1, len(content.encode('utf-8')) // 1024)} KB"
file_id = f"{dataset_id}_file_1"
version_id = f"{file_id}_v1"
conn.execute(
"""
INSERT INTO datasets
(id, name, type, storage_type, source, size, count, description, create_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(dataset_id, name, dataset_type, "local", "upload", size, len(lines), description, now),
)
conn.execute(
"""
INSERT INTO dataset_files
(id, dataset_id, name, size, content, active_version_id, versions, create_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
file_id,
dataset_id,
file_name,
size,
content,
version_id,
json_dumps([{"id": version_id, "version": 1, "create_time": now, "description": "initial seed"}]),
now,
),
)
def _insert_task(
self,
conn: sqlite3.Connection,
task_id: str,
name: str,
status: str,
progress: int,
create_time: str,
start_time: str | None,
completed_at: str | None,
node_id: str | None,
gpus: list[int],
process_id: int | None,
) -> None:
payload = {
"id": task_id,
"name": name,
"description": "Seeded sample fine-tune task.",
"status": status,
"train_type": "SFT",
"train_method": "lora",
"template": "qwen",
"base_model": "m_qwen25_7b",
"train_dataset_id": "ds_finance_sft",
"auto_merge": False,
"output_model_name": f"{name}-lora",
"gpus": gpus,
"batch_size": 2,
"learning_rate": 0.0002,
"n_epochs": 3,
"save_steps": 50,
"lr_scheduler_type": "cosine",
"max_length": 2048,
"warmup_ratio": 0.03,
"weight_decay": 0.01,
"lora_alpha": 16,
"lora_dropout": 0.05,
"lora_rank": 8,
"quantization_bit": 4,
"export_quantized": False,
"quant_method": "bnb",
"quant_bits": 4,
"quant_group_size": 128,
"export_format": "safetensors",
"progress": progress,
"process_id": process_id,
"train_duration": self._duration(start_time, completed_at),
"create_time": create_time,
}
conn.execute(
"""
INSERT INTO fine_tune_tasks
(id, name, payload, status, progress, process_id, create_time, start_time, completed_at, compute_node_id, gpus)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
task_id,
name,
json_dumps(payload),
status,
progress,
process_id,
create_time,
start_time,
completed_at,
node_id,
json_dumps(gpus),
),
[(u[0], u[1], hash_password(u[2]), u[3], u[4], u[5], json_dumps(u[6]), now, u[7]) for u in users],
)
def _duration(self, start_time: str | None, end_time: str | None = None) -> str:
@@ -590,6 +228,9 @@ class PlatformStore:
return f"{sec}s"
def refresh_runtime_state(self) -> None:
if get_settings().compute_mode != "simulator":
return
with self.connect() as conn:
rows = conn.execute(
"SELECT * FROM fine_tune_tasks WHERE status IN ('syncing','queued','running')"
@@ -644,7 +285,7 @@ class PlatformStore:
(status, progress, completed_at, row["id"]),
)
def _ensure_trained_model(self, conn: sqlite3.Connection, task: dict[str, Any]) -> None:
def _ensure_trained_model(self, conn: PgConnection, task: dict[str, Any]) -> None:
name = task.get("output_model_name") or f"{task['name']}-lora"
exists = conn.execute("SELECT id FROM trained_models WHERE name=?", (name,)).fetchone()
if exists:
@@ -676,9 +317,18 @@ class PlatformStore:
def login(self, username: str, password: str) -> dict[str, Any] | None:
with self.connect() as conn:
row = conn.execute("SELECT * FROM users WHERE username=?", (username,)).fetchone()
if not row or row["password"] != password or row["status"] != "active":
if not row or row["status"] != "active":
return None
matched, legacy_plaintext = verify_password(password, row["password_hash"])
if not matched:
return None
last_login = utcnow()
if legacy_plaintext:
conn.execute(
"UPDATE users SET password_hash=?, last_login=? WHERE id=?",
(hash_password(password), last_login, row["id"]),
)
else:
conn.execute("UPDATE users SET last_login=? WHERE id=?", (last_login, row["id"]))
data = self._user(row)
data["last_login"] = last_login
@@ -691,13 +341,13 @@ class PlatformStore:
conn.execute(
"""
INSERT INTO users
(id, username, password, display_name, role, status, permissions, create_time, protected)
(id, username, password_hash, display_name, role, status, permissions, create_time, protected)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
""",
(
user_id,
payload["username"],
payload.get("password", "platform123"),
hash_password(payload.get("password", "platform123")),
payload.get("display_name") or payload["username"],
payload.get("role", "viewer"),
payload.get("status", "active"),
@@ -732,7 +382,7 @@ class PlatformStore:
raise ValueError("protected user cannot be deleted")
conn.execute("DELETE FROM users WHERE id=?", (user_id,))
def _user(self, row: sqlite3.Row) -> dict[str, Any]:
def _user(self, row: PgRow) -> dict[str, Any]:
return {
"id": row["id"],
"username": row["username"],
@@ -843,7 +493,7 @@ class PlatformStore:
raise KeyError(dataset_id)
return self._dataset(conn, row)
def _dataset(self, conn: sqlite3.Connection, row: sqlite3.Row) -> dict[str, Any]:
def _dataset(self, conn: PgConnection, row: PgRow) -> dict[str, Any]:
files = conn.execute(
"SELECT id, name, size, active_version_id, create_time FROM dataset_files WHERE dataset_id=? ORDER BY create_time",
(row["id"],),
@@ -884,9 +534,6 @@ class PlatformStore:
utcnow(),
),
)
if not payload.get("files"):
content = '{"instruction":"Sample instruction","input":"Sample input","output":"Sample output"}'
self.add_dataset_file(conn, dataset_id, "sample.jsonl", content)
return self._dataset(conn, conn.execute("SELECT * FROM datasets WHERE id=?", (dataset_id,)).fetchone())
def update_dataset(self, dataset_id: str, payload: dict[str, Any]) -> dict[str, Any]:
@@ -918,7 +565,7 @@ class PlatformStore:
conn.execute("DELETE FROM dataset_files WHERE dataset_id=?", (dataset_id,))
conn.execute("DELETE FROM datasets WHERE id=?", (dataset_id,))
def add_dataset_file(self, conn: sqlite3.Connection, dataset_id: str, name: str, content: str) -> dict[str, Any]:
def add_dataset_file(self, conn: PgConnection, dataset_id: str, name: str, content: str) -> dict[str, Any]:
now = utcnow()
file_id = new_id("file")
version_id = f"{file_id}_v1"
@@ -947,7 +594,7 @@ class PlatformStore:
)
return {"id": file_id, "name": name, "size": size}
def dataset_file(self, file_id: str) -> sqlite3.Row:
def dataset_file(self, file_id: str) -> PgRow:
with self.connect() as conn:
row = conn.execute("SELECT * FROM dataset_files WHERE id=?", (file_id,)).fetchone()
if not row:
@@ -1008,7 +655,7 @@ class PlatformStore:
raise KeyError(task_id)
return self._task(row)
def _task(self, row: sqlite3.Row) -> dict[str, Any]:
def _task(self, row: PgRow) -> dict[str, Any]:
payload = json_loads(row["payload"], {})
payload.update(
{
@@ -1028,6 +675,12 @@ class PlatformStore:
def create_task(self, payload: dict[str, Any]) -> dict[str, Any]:
task_id = str(payload.get("task_id") or payload.get("id") or new_id("ft"))
name = payload.get("name") or f"fine-tune-{task_id[-6:]}"
base_model = payload.get("base_model") or payload.get("base_model_id")
train_dataset_id = payload.get("train_dataset_id")
if not base_model:
raise ValueError("base_model or base_model_id is required")
if not train_dataset_id:
raise ValueError("train_dataset_id is required")
now = utcnow()
task = {
"id": task_id,
@@ -1037,8 +690,8 @@ class PlatformStore:
"train_type": payload.get("train_type", "SFT"),
"train_method": payload.get("train_method", "lora"),
"template": payload.get("template", "qwen"),
"base_model": payload.get("base_model") or payload.get("base_model_id") or "m_qwen25_7b",
"train_dataset_id": payload.get("train_dataset_id") or "ds_finance_sft",
"base_model": base_model,
"train_dataset_id": train_dataset_id,
"auto_merge": bool(payload.get("auto_merge", False)),
"output_model_name": payload.get("output_model_name") or f"{name}-lora",
"gpus": payload.get("gpus") or [],
@@ -1173,7 +826,7 @@ class PlatformStore:
"pending": "waiting for start",
"syncing": "syncing model and dataset to compute node",
"queued": "waiting for GPU slot",
"running": "training with LLaMA-Factory simulator",
"running": "training with LLaMA-Factory",
"completed": "training completed",
"failed": "training stopped",
}
@@ -1183,7 +836,7 @@ class PlatformStore:
"status": status,
"progress": progress,
"step": labels.get(status, status),
"speed": "42.5 samples/s" if status == "running" else "--",
"speed": task.get("train_speed") or "--",
"eta": eta,
}
@@ -1234,6 +887,40 @@ class PlatformStore:
)
return next(n for n in self.compute_nodes() if n["id"] == node_id)
def create_compute_node(self, payload: dict[str, Any]) -> dict[str, Any]:
node_id = payload.get("id") or new_id("node")
now = utcnow()
tags = payload.get("tags") or []
with self.connect() as conn:
conn.execute(
"""
INSERT INTO compute_nodes
(id, code, name, api_base_url, file_gateway_url, enabled, scheduler_status,
scheduler_weight, tags, gpu_count, current_running_jobs, max_parallel_jobs,
data_root, model_root, log_root, last_health_check_at, health_detail)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)
""",
(
node_id,
payload["code"],
payload.get("name") or payload["code"],
payload["api_base_url"],
payload.get("file_gateway_url") or payload["api_base_url"],
1 if payload.get("enabled", True) else 0,
payload.get("scheduler_status", "offline"),
int(payload.get("scheduler_weight", 100)),
json_dumps(tags),
int(payload.get("gpu_count", 0)),
int(payload.get("max_parallel_jobs", 1)),
payload.get("data_root", "/data/yg-ft"),
payload.get("model_root", "/models"),
payload.get("log_root", "/data/yg-ft/training-logs"),
now,
json_dumps(payload.get("health_detail") or {"status": "registered"}),
),
)
return next(node for node in self.compute_nodes() if node["id"] == node_id)
def gpus(self) -> list[dict[str, Any]]:
self.refresh_runtime_state()
with self.connect() as conn:
@@ -1367,30 +1054,7 @@ class PlatformStore:
"SELECT * FROM resource_replicas WHERE node_id=? ORDER BY create_time DESC",
(node_id,),
).fetchall()
if rows:
return [dict(row) for row in rows]
return [
{
"id": f"rep_{node_id}_model_qwen",
"node_id": node_id,
"resource_type": "model",
"resource_id": "m_qwen25_7b",
"local_path": "/models/Qwen2.5-7B-Instruct",
"status": "available",
"sync_status": "completed",
"create_time": utcnow(),
},
{
"id": f"rep_{node_id}_dataset_finance",
"node_id": node_id,
"resource_type": "dataset",
"resource_id": "ds_finance_sft",
"local_path": "/data/yg-ft/datasets/finance-sft-sample.jsonl",
"status": "available",
"sync_status": "completed",
"create_time": utcnow(),
},
]
def sync_job(self, sync_id: str) -> dict[str, Any]:
self.refresh_runtime_state()
@@ -1423,7 +1087,7 @@ class PlatformStore:
progress = int(task.get("progress", 0) or 0)
points = max(1, min(80, progress))
lines = [
f"[INFO] task={task['name']} engine=llama_factory mode=simulator status={task['status']}",
f"[INFO] task={task['name']} engine=llama_factory status={task['status']}",
f"[INFO] base_model={task.get('base_model')} dataset={task.get('train_dataset_id')} gpus={task.get('gpus', [])}",
"[INFO] command=llamafactory-cli train --stage sft --finetuning_type lora --do_train true",
]

View File

@@ -8,13 +8,12 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./runtime/platform.db")
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+psycopg://yg_ft:change_me@localhost:15432/yg_ft")
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
future=True,
connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {},
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False, future=True)

View File

@@ -0,0 +1,133 @@
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_name TEXT NOT NULL,
role TEXT NOT NULL,
status TEXT NOT NULL,
permissions TEXT NOT NULL,
create_time TEXT NOT NULL,
last_login TEXT,
protected INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS models (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
purpose TEXT NOT NULL,
model_source TEXT NOT NULL,
description TEXT,
path TEXT,
api_url TEXT,
api_key TEXT,
online_model_name TEXT,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS trained_models (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
train_methods TEXT NOT NULL,
base_model_path TEXT,
create_time TEXT NOT NULL,
merged INTEGER NOT NULL DEFAULT 0,
merging INTEGER NOT NULL DEFAULT 0,
merged_path TEXT
);
CREATE TABLE IF NOT EXISTS datasets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
storage_type TEXT NOT NULL,
source TEXT NOT NULL,
task_id TEXT,
size TEXT,
count INTEGER NOT NULL DEFAULT 0,
description TEXT,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS dataset_files (
id TEXT PRIMARY KEY,
dataset_id TEXT NOT NULL REFERENCES datasets(id) ON DELETE CASCADE,
name TEXT NOT NULL,
size TEXT,
content TEXT NOT NULL,
active_version_id TEXT NOT NULL,
versions TEXT NOT NULL,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS compute_nodes (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
api_base_url TEXT NOT NULL,
file_gateway_url TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
scheduler_status TEXT NOT NULL,
scheduler_weight INTEGER NOT NULL DEFAULT 100,
tags TEXT NOT NULL,
gpu_count INTEGER NOT NULL DEFAULT 0,
current_running_jobs INTEGER NOT NULL DEFAULT 0,
max_parallel_jobs INTEGER NOT NULL DEFAULT 2,
data_root TEXT NOT NULL,
model_root TEXT NOT NULL,
log_root TEXT NOT NULL,
last_health_check_at TEXT,
health_detail TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS gpus (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
gpu_index INTEGER NOT NULL,
uuid TEXT NOT NULL,
name TEXT NOT NULL,
memory_total_gb DOUBLE PRECISION NOT NULL,
power_limit_w DOUBLE PRECISION NOT NULL,
base_temperature INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS fine_tune_tasks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
payload TEXT NOT NULL,
status TEXT NOT NULL,
progress INTEGER NOT NULL DEFAULT 0,
process_id INTEGER,
create_time TEXT NOT NULL,
start_time TEXT,
completed_at TEXT,
compute_node_id TEXT REFERENCES compute_nodes(id) ON DELETE SET NULL,
gpus TEXT NOT NULL,
sync_job_id TEXT
);
CREATE TABLE IF NOT EXISTS resource_replicas (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
local_path TEXT NOT NULL,
status TEXT NOT NULL,
sync_status TEXT NOT NULL,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS resource_sync_jobs (
id TEXT PRIMARY KEY,
target_node_id TEXT NOT NULL,
resources TEXT NOT NULL,
status TEXT NOT NULL,
progress INTEGER NOT NULL DEFAULT 0,
create_time TEXT NOT NULL,
completed_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_fine_tune_status ON fine_tune_tasks(status);
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 INDEX IF NOT EXISTS idx_replicas_resource ON resource_replicas(resource_type, resource_id);

View File

@@ -2,14 +2,14 @@
name = "yg-ft-backend"
version = "0.1.0"
description = "Backend service for the model fine-tuning platform"
requires-python = ">=3.11"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.111.0",
"uvicorn[standard]>=0.30.0",
"python-multipart>=0.0.9",
"pydantic>=2.7.0",
"sqlalchemy>=2.0.30",
"asyncpg>=0.29.0",
"psycopg[binary]>=3.2.1",
"alembic>=1.13.1",
"redis>=5.0.4",
"httpx>=0.27.0",
@@ -26,7 +26,7 @@ dev = [
[tool.ruff]
line-length = 100
target-version = "py311"
target-version = "py312"
[tool.pytest.ini_options]
testpaths = ["tests"]

View File

@@ -3,7 +3,7 @@ uvicorn[standard]>=0.30.0
python-multipart>=0.0.9
pydantic>=2.7.0
sqlalchemy>=2.0.30
asyncpg>=0.29.0
psycopg[binary]>=3.2.1
alembic>=1.13.1
redis>=5.0.4
httpx>=0.27.0

View File

@@ -14,7 +14,7 @@ compute/
tests/
```
## 第一版职责
## 开发职责
- GPU 发现、状态上报、锁定和释放。
- 本地磁盘工作区管理。
@@ -22,3 +22,8 @@ compute/
- LLaMA-Factory 命令生成、日志解析、产物收集。
- 分片上传、短时下载、离线导入。
- 通过服务间 token 接受应用平台调用。
## 运行模式
- 默认 `COMPUTE_EXECUTION_MODE=real`Compute API 只暴露健康检查和接口契约;真实训练执行器完成前,创建作业会返回未实现错误。
- 仅隔离联调时可设置 `COMPUTE_EXECUTION_MODE=simulator`,启用内存状态机和合成 GPU/日志数据。该模式不得作为生产运行路径。

View File

@@ -22,7 +22,12 @@ def create_app() -> FastAPI:
def host_id() -> str:
return os.getenv("COMPUTE_HOST_ID", "gpu-node-01")
def execution_mode() -> str:
return os.getenv("COMPUTE_EXECUTION_MODE", os.getenv("COMPUTE_MODE", "real")).lower()
def job_status(job: dict[str, Any]) -> dict[str, Any]:
if execution_mode() != "simulator":
return job
elapsed = max(0, int(now() - job["created_at"]))
if job["status"] not in {"stopped", "failed", "completed"}:
if elapsed < 5:
@@ -41,7 +46,7 @@ def create_app() -> FastAPI:
progress = int(job.get("progress", 0) or 0)
points = max(1, min(80, progress))
lines = [
f"[INFO] compute_host_id={host_id()} job_id={job['id']} engine=llama_factory mode=simulator",
f"[INFO] compute_host_id={host_id()} job_id={job['id']} engine=llama_factory",
f"[INFO] command={' '.join(job['command'])}",
]
for step in range(1, points + 1):
@@ -70,6 +75,8 @@ def create_app() -> FastAPI:
return "\n".join(lines)
def gpu_resources() -> list[dict[str, Any]]:
if execution_mode() != "simulator":
return []
active_jobs = [job_status(job) for job in jobs.values() if job["status"] in {"queued", "running"}]
gpus: list[dict[str, Any]] = []
for idx in range(4):
@@ -121,6 +128,7 @@ def create_app() -> FastAPI:
"data_root_exists": data_root.exists(),
"llama_factory_home": str(llama_factory_home),
"llama_factory_home_exists": llama_factory_home.exists(),
"execution_mode": execution_mode(),
}
@app.get(f"{route_prefix}/v1/compute/jobs")
@@ -137,6 +145,11 @@ def create_app() -> FastAPI:
command = build_command(payload, os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"))
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
if execution_mode() != "simulator":
raise HTTPException(
status_code=501,
detail="real compute executor is not implemented yet; set COMPUTE_EXECUTION_MODE=simulator only for isolated development",
)
job_id = str(payload.get("id") or f"job_{int(now() * 1000)}")
job = {
"id": job_id,

View File

@@ -35,7 +35,7 @@ def build_command(config: dict[str, Any], llama_factory_home: str = "/app/LLaMA-
model_path = config.get("base_model") or config.get("model_name_or_path")
dataset = config.get("dataset") or config.get("dataset_dir")
output_dir = config.get("output_dir") or f"/data/yg-ft/outputs/{config.get('name', 'sample-job')}"
output_dir = config.get("output_dir") or f"/data/yg-ft/outputs/{config.get('name', 'training-job')}"
command = [
"llamafactory-cli",
"train",

View File

@@ -69,6 +69,12 @@ $images | ForEach-Object { docker pull $_ }
- `COMPUTE_API_PORT=19100`
- `FILE_GATEWAY_PORT=19101`
## 运行模式
- 应用侧默认 `COMPUTE_MODE=real`,任务状态必须由真实算力同步逻辑更新。
- 算力侧默认 `COMPUTE_EXECUTION_MODE=real`,真实执行器未完成前不会伪造训练作业。
- 仅隔离联调时可显式设置 `COMPUTE_MODE=simulator``COMPUTE_EXECUTION_MODE=simulator`,该模式不得用于测试环境、生产环境或生产升级基线。
## 应用服务器部署
应用服务器包含前端 Nginx、Backend API、PostgreSQL、Redis。
@@ -112,7 +118,7 @@ http://<app-server-ip>:16801
如果使用企业统一 PostgreSQL/Redis修改 `docker/app/.env`
```env
DATABASE_URL=postgresql+asyncpg://<user>:<password>@<postgres-host>:15432/<db>
DATABASE_URL=postgresql+psycopg://<user>:<password>@<postgres-host>:15432/<db>
REDIS_URL=redis://<redis-host>:16379/0
USE_BUILTIN_POSTGRES=false
USE_BUILTIN_REDIS=false

View File

@@ -14,7 +14,7 @@ REDIS_PORT=16379
POSTGRES_DB=yg_ft
POSTGRES_USER=yg_ft
POSTGRES_PASSWORD=change_me
DATABASE_URL=postgresql+asyncpg://yg_ft:change_me@postgres:5432/yg_ft
DATABASE_URL=postgresql+psycopg://yg_ft:change_me@postgres:5432/yg_ft
REDIS_URL=redis://redis:6379/0
@@ -38,6 +38,7 @@ COMPUTE_SERVICE_TOKEN=change_me
FILE_GATEWAY_BASE_URL=http://compute-api:9101
# The application side polls Compute API for job state to avoid opening reverse network access.
COMPUTE_MODE=real
COMPUTE_STATUS_SYNC_MODE=polling
COMPUTE_POLL_INTERVAL_SECONDS=10
COMPUTE_POLL_BATCH_SIZE=100

View File

@@ -32,7 +32,7 @@ services:
APP_ENV: ${APP_ENV:-prod}
APP_NAME: ${APP_NAME:-YG Fine-Tune Platform API}
MODELTF_ROUTE_PREFIX: ${MODELTF_ROUTE_PREFIX:-/modelTF}
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://yg_ft:change_me@postgres:5432/yg_ft}
DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://yg_ft:change_me@postgres:5432/yg_ft}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
USE_BUILTIN_POSTGRES: ${USE_BUILTIN_POSTGRES:-true}
USE_BUILTIN_REDIS: ${USE_BUILTIN_REDIS:-true}
@@ -45,6 +45,7 @@ services:
COMPUTE_API_BASE_URL: ${COMPUTE_API_BASE_URL:-http://compute-api:9100}
COMPUTE_SERVICE_TOKEN: ${COMPUTE_SERVICE_TOKEN:-change_me}
FILE_GATEWAY_BASE_URL: ${FILE_GATEWAY_BASE_URL:-http://compute-api:9101}
COMPUTE_MODE: ${COMPUTE_MODE:-real}
COMPUTE_STATUS_SYNC_MODE: ${COMPUTE_STATUS_SYNC_MODE:-polling}
COMPUTE_POLL_INTERVAL_SECONDS: ${COMPUTE_POLL_INTERVAL_SECONDS:-10}
COMPUTE_POLL_BATCH_SIZE: ${COMPUTE_POLL_BATCH_SIZE:-100}

View File

@@ -1,5 +1,6 @@
COMPUTE_ENV=prod
COMPUTE_HOST_ID=gpu-node-01
COMPUTE_EXECUTION_MODE=real
MODELTF_ROUTE_PREFIX=/modelTF
# Five-digit host ports exposed outside the compute server.
COMPUTE_API_PORT=19100

View File

@@ -8,6 +8,7 @@ services:
environment:
COMPUTE_ENV: ${COMPUTE_ENV:-prod}
COMPUTE_HOST_ID: ${COMPUTE_HOST_ID:-gpu-node-01}
COMPUTE_EXECUTION_MODE: ${COMPUTE_EXECUTION_MODE:-real}
MODELTF_ROUTE_PREFIX: ${MODELTF_ROUTE_PREFIX:-/modelTF}
COMPUTE_SERVICE_TOKEN: ${COMPUTE_SERVICE_TOKEN:-change_me}
ENABLE_APP_CALLBACK: ${ENABLE_APP_CALLBACK:-false}

View File

@@ -1,6 +1,8 @@
# 模型微调平台后端接口设计
> 后端建议使用 FastAPI统一挂载 `/modelTF` 前缀。本文根据当前 Vue 前端路由、API 模块、mock 数据和页面交互反推接口,并补充完整微调平台必须具备的用户中心、权限控制、审计、异步任务、文件版本与监控能力。
> 后端建议使用 FastAPI统一挂载 `/modelTF` 前缀。本文根据当前 Vue 前端路由、API 模块和页面交互契约梳理接口,并补充完整微调平台必须具备的用户中心、权限控制、审计、异步任务、文件版本与监控能力。前端 Mock 仅作为隔离开发辅助,不作为接口设计准则。
菜单、二级路由、规划菜单、接口和数据库的总览映射见 `docs/menu-functional-requirements.md`。后续新增接口时,必须同步标注对应页面/功能模块。
## 1. 通用约定
@@ -88,7 +90,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
}
```
说明:前端当前登录接口已经要求返回 `user`mock 里只返回 token正式后端必须返回完整用户信息。
说明:前端当前登录接口已经要求返回 `user`,正式后端必须返回完整用户信息。
### 2.2 当前用户
@@ -158,7 +160,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
}
```
说明:首页当前完全使用前端 mock后端提供聚合接口,避免前端拼多接口导致加载慢。
说明:首页需要由后端提供正式聚合接口,避免前端拼多接口导致加载慢。
### 3.2 健康指标
@@ -318,7 +320,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
## 6. 数据处理
当前数据处理页面为本地模拟,正式后端建议实现以下接口。
当前数据处理页面需要后端正式承接上传、切片、生成、编辑和发布流程,建议实现以下接口。
### 6.1 任务列表与详情
@@ -678,7 +680,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
## 11. 后端开发需要补齐的关键点
1. 前端路由已有 `user-settings``user-create``user-permission``permission-denied`,但当前仓库缺少对应 Vue 文件;后端仍应先实现用户中心和权限接口。
2. 数据处理主流程目前全在浏览器本地模拟,后端需要正式实现上传、切片、LLM 生成、结果编辑、发布数据集。
2. 数据处理主流程需要后端正式实现上传、切片、LLM 生成、结果编辑、发布数据集。
3. 训练、评测、数据处理、模型加载都不应同步阻塞 HTTP建议接 Celery/RQ/Arq 或 FastAPI BackgroundTasks + 独立 worker。
4. 文件内容不要全部入库;数据库保存元数据、版本、校验和、对象存储路径,内容放本地 NAS/MinIO。
5. API Key、外部数据源密码必须加密存储接口只回显脱敏。

View File

@@ -63,7 +63,7 @@
### 4.1 适用场景
- PoC、试点环境、演示环境。
- 开发联调、单机试运行、资源受限的早期上线环境。
- 小团队共用一台单机多 GPU 服务器。
- 网络隔离要求不高,部署资源有限。
@@ -272,7 +272,7 @@ compute/engines/openrlhf/
```env
APP_ENV=prod
MODELTF_ROUTE_PREFIX=/modelTF
DATABASE_URL=postgresql+asyncpg://yg_ft:***@postgres:5432/yg_ft
DATABASE_URL=postgresql+psycopg://yg_ft:***@postgres:5432/yg_ft
REDIS_URL=redis://redis:6379/0
LOG_DIR=/opt/yg-ft/logs/backend
COMPUTE_API_BASE_URL=https://compute.internal:19100
@@ -327,7 +327,7 @@ CUDA_VISIBLE_DEVICES=0,1,2,3
- 后端 `GET /modelTF/health` 正常。
- Compute API 健康检查正常。
- Compute Agent 能识别 GPU、显存、CUDA 版本。
- LLaMA-Factory 能在命令行完成最小训练样例
- LLaMA-Factory 能在命令行完成最小训练作业
- 应用平台能提交训练任务到 Compute API。
- 任务状态能从算力平台同步回应用平台。
- 数据集上传、离线导入、产物下载路径权限正确。

View File

@@ -1,10 +1,10 @@
# 第一版系统开发计划
# 当前系统主链路开发计划
> 说明:本计划虽然以“可演示闭环”为阶段目标,但实现内容按正式系统第一版开发,不在工程模块、配置项、接口标签、数据库对象中使用临时代码命名。初始化数据仅作为样例种子数据,后续可在此基础上继续扩展为生产能力
> 说明:本计划描述当前正在开发的系统能力。代码、接口和 SQL 均按后续生产演进基线维护,不以一次性演示、静态 Mock 或样例数据作为开发准则。联调辅助能力必须显式配置启用,并不得成为默认运行路径
## 1. 阶段目标
第一版系统需要在没有完整生产环境、没有真实 GPU 或没有 LLaMA-Factory 运行环境时,仍然可以跑通模型微调平台的主链路:
当前阶段需要完成模型微调平台的主链路工程基础
```text
登录
@@ -12,24 +12,26 @@
-> 数据集管理
-> 创建微调任务
-> 调度算力节点与 GPU
-> 模拟资源副本检查同步
-> 检查同步模型/数据集资源
-> 启动训练任务
-> 查看任务状态、GPU 占用、训练日志、loss 曲线
-> 轮询任务状态、GPU 占用、训练日志、loss 曲线
-> 训练完成后登记训练产物
```
该阶段不是一次性展示代码,而是后续系统继续开发的基础版本。算力、训练框架、资源同步、任务状态机均按真实平台边界设计,当前仅以 `simulator` 模式补齐无 GPU 环境下的可运行能力
该阶段是正式系统的第一批可运行能力,不再初始化业务样例数据。系统只允许初始化内置管理员/运维账号模型、数据集、算力节点、GPU、训练任务和资源副本必须通过页面、接口、算力 Agent 扫描或正式导入流程产生
## 2. 运行模式
| 模式 | 说明 | 当前状态 |
| 模式 | 说明 | 当前要求 |
| --- | --- | --- |
| `simulator` | 模拟 GPU、训练进程、日志、指标、checkpoint 和训练产物 | 第一版默认实现 |
| `real` | 预留真实 `nvidia-smi`、LLaMA-Factory 进程启动、真实日志解析和产物扫描 | 后续接入 |
| `real` | 面向真实部署,等待 Compute API、Agent、File Gateway 和 LLaMA-Factory 执行器回写状态 | 默认模式 |
| `simulator` | 仅用于隔离联调,无真实 GPU 时临时推进任务状态、GPU 状态和训练日志 | 必须显式开启,不得用于生产基线 |
第一版实现必须保留 `real` 模式边界,不允许把训练命令直接写死在应用平台后端。训练相关能力统一沉淀在 `compute/engines/` 适配层
后端默认 `COMPUTE_MODE=real`。在该模式下,任务状态不再按时间自动推进,必须由后续真实算力同步逻辑更新。算力服务默认 `COMPUTE_EXECUTION_MODE=real`,真实训练执行器未完成前,创建训练作业会返回明确的未实现错误,避免误认为已经完成生产训练能力
## 3. 第一批开发范围
训练相关能力必须沉淀在 `compute/engines/` 适配层,不允许在应用平台后端直接拼接或执行训练命令。
## 3. 当前开发范围
### 3.1 应用平台后端
@@ -38,24 +40,27 @@
```text
backend/app/
api/v1/endpoints/platform.py
db/platform_store.py
db/session.py
core/
db/
```
覆盖能力:
建立能力:
- 统一 API 响应结构 `{ code, message, data }`
- 初始化样例用户、模型、数据集、算力节点、GPU 和微调任务
- 登录、当前用户、用户列表与权限
- 模型管理、训练产物列表、权重合并任务占位
- PostgreSQL 运行表初始化,当前执行脚本位于 `backend/app/db/sql/001_platform_runtime.sql`
- 内置管理员账号初始化,业务数据不再自动写入样例记录
- 登录、当前用户、用户列表与权限页面接口
- 模型管理、训练产物列表、权重合并任务入口。
- 数据集管理、文件上传、预览、版本管理和下载。
- 微调任务创建、启动、停止、删除、进度查询、checkpoint 查询。
- 系统健康指标、系统信息、训练日志、系统日志。
- 系统健康指标、系统信息、训练日志、系统日志接口
- 算力节点、GPU、队列、资源副本、资源同步任务接口。
开发约定
- 当前本地可运行存储使用 SQLite配置项为 `LOCAL_DB_PATH`
- 生产数据库仍以 PostgreSQL 为目标,正式表结构以 `docs/postgres-schema.sql` 为准
- `backend/app/db/session.py` 已提供 SQLAlchemy session 入口,后续替换正式 ORM 时从此处收口
待继续开发:
- 接入正式 ORM/Repository/Service 分层和 Alembic 迁移
- 将任务状态更新改为应用侧定时轮询 Compute API/File Gateway 后落库
- 完成项目/模型/数据集级权限隔离校验
- 完成审批流、审计留存、配额、资源申请和多租户上下文。
- 增加正式异常码、接口鉴权、中间件、幂等控制和分页规范。
### 3.2 算力平台服务
@@ -64,72 +69,77 @@ backend/app/
```text
compute/
api/main.py
engines/llama_factory/adapter.py
agent/
engines/llama_factory/
file_gateway/
```
覆盖能力:
建立能力:
- `/modelTF/health``/modelTF/v1/compute/health` 节点健康检查。
- `/modelTF/compute/resources/gpus` GPU 状态模拟
- `/modelTF/compute/jobs` 创建、查询、停止训练任务
- `/modelTF/compute/jobs/{id}/logs` 拉取训练日志与解析指标
- `/modelTF/compute/files/upload``/modelTF/compute/files/{id}/download` 文件网关占位。
- LLaMA-Factory 参数校验、命令生成、日志指标解析。
- LLaMA-Factory 参数校验、命令生成和训练日志指标解析
- Compute API 作业、GPU、文件网关接口壳
- 显式 `simulator` 模式下的内存状态机,用于隔离联调
设计约定
- 多算力节点阶段,每台单机多 GPU 服务器都部署一套 Compute API、Agent、File Gateway 和训练框架
- 应用平台通过 `compute_nodes` 中维护的 `api_base_url``file_gateway_url` 主动轮询算力节点
- 算力节点之间默认不互访,资源副本和同步状态由应用平台统一编排
待继续开发
- 真实 GPU 发现:接入 `nvidia-smi` 或 NVML
- GPU 锁定与释放:一张 GPU 同一时间只分配给一个训练或推理任务
- LLaMA-Factory 真实执行器:生成 YAML/命令、启动进程、停止进程、采集 PID
- 训练日志采集:读取宿主机挂载日志文件,解析 loss、learning rate、epoch 等指标。
- Checkpoint/adapter/merged model 扫描与产物登记。
- File Gateway本地磁盘文件上传、下载、校验、导入和跨节点资源同步。
### 3.3 前端页面
已接入页面:
- `/login`:登录接口。
- `/model-manage`:模型列表模型来源。
- `/dataset``/dataset/:id/preview`:数据集列表、预览版本。
- `/model-manage`:模型列表模型来源、训练产物
- `/dataset``/dataset/:id/preview`:数据集列表、预览版本。
- `/fine-tune``/fine-tune/create`:微调任务创建、启动、状态轮询。
- `/training-log/:id`:训练日志和 loss 曲线。
- `/hardware`:平台 GPU 与系统性能。
- `/compute`算力节点、GPU、队列、资源副本。
新增前端能力:
- `frontend/src/api/modules/compute.ts`:算力节点 API 包装。
- `frontend/src/views/compute/ComputeNodesView.vue`节点、GPU、队列、资源副本四个视图。
- 权限枚举增加 `compute`,菜单增加“算力节点”入口
前端 Mock 默认关闭。仅在隔离前端开发时可设置 `VITE_ENABLE_MOCK=true`,真实联调和后续生产演进均以 `/modelTF` 后端接口为准。
待继续开发:
- 补齐多租户、项目管理、审批中心、审计中心、配额管理页面
- 完成算力节点管理表单,包括节点地址、权重、标签、启用状态和健康检查结果。
- 完成模型/数据集导入页面,支持本地路径扫描和归属项目选择。
- 推理服务页面需接入真实后端任务接口,移除页面内本地假对话路径。
### 3.4 数据库
第一版运行时使用本地 SQLite 以降低开发环境门槛,但数据库模型仍按 PostgreSQL 正式设计推进。
当前运行 SQL
核心表范围:
- 用户、租户、项目、角色、权限。
- 模型、训练产物、数据集、数据集文件、存储对象。
- 微调任务、训练指标、checkpoint、审计日志。
- 算力节点、GPU 设备、算力任务、GPU 分配。
- 资源副本、资源同步任务。
```text
backend/app/db/sql/001_platform_runtime.sql
```
优化方向
- 任务状态、租户/项目隔离字段、资源副本定位字段需要建立索引。
- 审计日志、训练指标、日志归档后续建议按时间分区。
- 大文件只落本地磁盘或对象存储,数据库保存元数据和版本关系。
架构目标 SQL
```text
docs/postgres-schema.sql
```
当前运行 SQL 用于支持已开发接口落库;架构目标 SQL 包含用户中心、多租户、项目隔离、审批、审计、配额、评测等完整模型。后续需要通过 Alembic 将二者收敛为统一迁移体系,生产升级只走迁移脚本,不依赖手工改表。
## 4. 验收标准
第一版完成后应满足:
- 前端不启用静态 Mock 时,可以通过 FastAPI 获取主要页面数据。
- 登录后可以看到模型、数据集、微调任务和算力节点
- 创建微调任务后可以启动任务,任务状态自动流转
- GPU 状态会随任务进入 `syncing``queued``running` 发生变化
- 训练日志持续生成,日志详情页能解析 loss、grad norm、learning rate、epoch
- 任务完成后训练产物出现在模型管理的训练产物列表中
- 算力节点页面可展示节点地址、权重、标签、启用状态、队列和资源副本。
- 工程命名、配置和接口分组均可作为后续正式开发基础。
- 启动后端必须连接 PostgreSQL不允许回退到 SQLite。
- 后端启动只初始化系统内置账号不初始化模型、数据集、算力节点、GPU、训练任务等业务样例数据。
- 前端默认请求真实 `/modelTF` 接口,除非显式设置 `VITE_ENABLE_MOCK=true`
- 默认 `real` 模式下任务状态自动伪造完成,必须等待真实算力同步
- 显式 `simulator` 模式只能用于隔离联调,部署文档必须标注不得用于生产
- 登录后可以进入主界面,并可通过页面/API 创建真实业务记录
- 代码、接口路由、配置项、数据库表名不得使用 `demo` 命名
## 5. 后续开发计划
| 阶段 | 重点 | 说明 |
| --- | --- | --- |
| 第二阶段 | PostgreSQL ORM 与 Alembic | 将 `platform_store.py` 的能力迁移为正式 ORM、迁移脚本和 repository/service 分层 |
| 第三阶段 | 真实 Compute Agent | 接入 `nvidia-smi`、GPU 锁定、进程管理、训练日志文件采集 |
| 第四阶段 | LLaMA-Factory 真实训练 | 生成 YAML/命令、启动训练进程、停止任务、扫描 checkpoint 和 adapter |
| 第五阶段 | 企业治理 | 多租户、项目/模型/数据集隔离、审批流、审计留存、配额和资源申请 |
| 第六阶段 | 多节点调度 | 基于 `compute_nodes`、资源副本和节点标签实现自动/手动调度策略 |
| 阶段 1 | 数据库迁移体系 | 将当前运行 SQL 与架构 SQL 收敛到 Alembic 迁移 |
| 阶段 2 | 后端领域分层 | 拆分用户、模型、数据集、训练、算力、审计等模块 |
| 阶段 3 | 真实 Compute Agent | GPU 发现、资源锁定、进程管理、日志采集 |
| 阶段 4 | LLaMA-Factory 训练执行 | YAML/命令生成、进程启动/停止、checkpoint 和 adapter 扫描 |
| 阶段 5 | 企业治理 | 多租户、项目隔离、审批流、审计留存、配额和资源申请 |
| 阶段 6 | 多算力节点调度 | 基于 `compute_nodes`、标签、权重、资源副本和节点健康实现调度策略 |

View File

@@ -0,0 +1,98 @@
# 菜单与功能需求总览
> 本文根据当前前端侧边栏、路由、需求文档、接口文档、部署文档和 SQL 脚本整理。当前代码和 SQL 均按正式系统开发基线维护Mock、Simulator 只能作为显式联调能力,不作为默认开发准则。
## 1. 菜单分层
### 1.1 当前侧边栏菜单
| 一级分组 | 菜单 | 路由 | 权限码 | 当前状态 | 主要功能 |
| --- | --- | --- | --- | --- | --- |
| 服务看板 | 服务看板 | `/dashboard` | `dashboard` | 已有页面,接口需继续完善 | 总览指标、服务状态、训练统计、最近任务、健康入口 |
| 模型服务 | 模型训练 | `/fine-tune` | `fine-tune` | 已接入主链路 | 训练任务列表、创建训练、启动/停止、进度、训练日志、checkpoint |
| 模型服务 | 模型评测 | `/model-eval` | `model-eval` | 前端页面已有,后端待完整实现 | 评测任务、评测维度、样本评分、综合结果 |
| 模型服务 | 模型推理 | `/model-inference` | `model-inference` | 前端页面已有,后端待完整实现 | 推理任务、模型加载、单模型对话、模型对比入口 |
| 模型服务 | 模型管理 | `/model-manage` | `model-manage` | 已接入主链路 | 基座模型登记、本地/API 模型、训练产物、权重合并、模型导出 |
| 数据治理 | 数据集管理 | `/dataset` | `dataset` | 已接入主链路 | 数据集列表、上传、预览、在线编辑、版本、下载、删除审批入口 |
| 数据治理 | 数据处理 | `/data-process` | `data-process` | 前端页面已有,后端待完整实现 | 文档上传、切片预览、LLM 生成、结果编辑、发布数据集 |
| 其他工具 | 数据类型转换 | `/data-convert` | `data-convert` | 前端页面已有,后端待实现 | JSON/JSONL/Markdown 等格式转换任务 |
| 算力资源 | 算力节点 | `/compute` | `compute` | 已接入节点管理接口 | 节点地址、权重、标签、启用状态、GPU、队列、资源副本 |
| 系统设置 | 用户设置 | `/user-settings` | `user-settings` | 已接入基础用户接口 | 用户列表、创建用户、启停、页面权限 |
| 系统设置 | 平台性能 | `/hardware` | `hardware` | 已有接口,需接真实采集 | CPU、内存、磁盘、GPU、进程、网络监控 |
| 系统设置 | 查看日志 | `/logs` | `logs` | 已有接口,需接真实日志文件 | 后端日志、error 日志、训练日志索引、日志内容查看 |
### 1.2 当前二级和隐藏路由
| 页面 | 路由 | 归属菜单 | 说明 |
| --- | --- | --- | --- |
| 登录 | `/login` | 独立入口 | 登录后进入主界面 |
| 使用文档 | `/guide` | 独立入口 | 当前系统使用说明 |
| 创建训练任务 | `/fine-tune/create` | 模型训练 | 训练参数、模型/数据集/GPU 选择 |
| 训练日志 | `/training-log/:id` | 模型训练 | 日志、指标、checkpoint、任务概览 |
| 新建评测 | `/model-eval/create` | 模型评测 | 模型、数据集、维度、GPU 选择 |
| 评测详情 | `/model-eval/:id` | 模型评测 | 维度汇总、样本结果、人工复核预留 |
| 评测维度创建/编辑 | `/model-eval/dimension/create``/model-eval/dimension/:id/edit` | 模型评测 | 评测规则、Prompt、评分器配置 |
| 新建推理 | `/model-inference/create` | 模型推理 | 推理任务和模型加载配置 |
| 模型对话 | `/model-inference/chat/:id` | 模型推理 | 单模型对话 |
| 模型对比 | `/model-compare/chat/:id``/model-compare/result` | 模型推理 | 多模型对比和结果页 |
| 添加/编辑模型 | `/model-manage/create``/model-manage/:id/edit` | 模型管理 | 模型登记、用途、来源、路径/API 配置 |
| 合并权重 | `/model-manage/merge` | 模型管理 | LoRA/Adapter 合并任务 |
| 数据处理创建/详情 | `/data-process/create``/data-process/:id` | 数据处理 | 数据处理向导和任务详情 |
| 数据集创建/编辑/预览 | `/dataset/create``/dataset/:id/edit``/dataset/:id/preview` | 数据集管理 | 数据集元数据、文件、版本与内容 |
| 自定义工具 | `/tools``/tools/create``/tools/:id/edit` | 规划入口 | 路由存在,当前侧边栏未展示,后续可归入“其他工具” |
| 算力子页 | `/compute/gpus``/compute/queue``/compute/nodes` | 算力节点 | 当前可作为页签或深链 |
| 创建用户/权限设置 | `/user-settings/create``/user-settings/:id/permission` | 用户设置 | 用户创建和页面权限 |
| 无权限页 | `/permission-denied` | 系统页 | 路由守卫无权限跳转 |
### 1.3 企业治理待补菜单
| 建议菜单分组 | 菜单 | 建议路由 | 优先级 | 必要性 |
| --- | --- | --- | --- | --- |
| 组织与项目 | 租户管理 | `/tenants``/tenants/:id` | P0 | 多租户隔离、配额、留存策略入口 |
| 组织与项目 | 项目空间 | `/projects``/projects/:id``/projects/:id/members` | P0 | 项目级模型/数据集/任务隔离 |
| 组织与项目 | 资源授权 | `/projects/:id/permissions` 或资源详情弹窗 | P0 | 模型/数据集/任务级 ACL |
| 治理中心 | 审批中心 | `/approvals``/approvals/:id` | P0 | 删除、发布、导出、停止他人任务等高风险动作 |
| 治理中心 | 审批设置 | `/approval-settings` | P1 | 审批模板、审批人规则、超时策略 |
| 治理中心 | 审计中心 | `/audit-logs``/login-logs``/download-logs` | P1 | 操作审计、登录审计、下载审计、导出 |
| 运维中心 | 存储管理 | `/storage` | P1 | 本地磁盘占用、临时文件、checkpoint 清理、留存 |
| 运维中心 | 训练引擎管理 | `/training-engines` | P2 | LLaMA-Factory 和后续引擎能力 schema、健康检查 |
| 模型服务 | 模型服务治理 | `/model-services``/model-services/:id` | P1 | 测试/生产服务发布、调用统计、下线审批 |
## 2. 菜单对应接口和数据库
| 菜单/模块 | 主要接口 | 当前运行 SQL | 目标 SQL |
| --- | --- | --- | --- |
| 登录、用户设置 | `/modelTF/login``/modelTF/me``/modelTF/users` | `users` | `users``login_sessions``permissions``role_permissions``user_permission_overrides` |
| 服务看板 | `/modelTF/dashboard/overview``/modelTF/health` | 复用模型/数据集/任务/算力表 | `system_metric_snapshots``web_logs`、各业务表聚合 |
| 模型管理 | `/modelTF/model-manage``/modelTF/model-manage/trained-models``/modelTF/model-manage/merge` | `models``trained_models` | `models``trained_models``storage_objects``local_import_jobs``resource_acl` |
| 数据集管理 | `/modelTF/dataset-manage``/modelTF/dataset-manage/upload/{id}``/preview``/versions` | `datasets``dataset_files` | `datasets``dataset_files``dataset_file_versions``dataset_records``storage_objects` |
| 模型训练 | `/modelTF/fine-tune``/start``/progress``/checkpoints` | `fine_tune_tasks``trained_models` | `fine_tune_tasks``fine_tune_metrics``fine_tune_checkpoints``compute_jobs``gpu_allocations` |
| 训练日志 | `/modelTF/training-log-files``/modelTF/training-log-content` | 由任务表生成索引 | 日志文件元数据、`fine_tune_metrics``audit_logs` |
| 算力节点 | `/modelTF/compute/nodes``/compute/gpus``/compute/queue``/compute/nodes/{id}/replicas` | `compute_nodes``gpus``resource_replicas``resource_sync_jobs` | `compute_nodes``gpu_devices``compute_node_engines``compute_jobs``resource_replicas``resource_sync_jobs` |
| 平台性能 | `/modelTF/system-info``/modelTF/compute/gpus` | `gpus`、任务表 | `system_metric_snapshots``gpu_devices``compute_jobs` |
| 查看日志 | `/modelTF/log-files``/modelTF/log-content``/modelTF/web-log` | 文件日志 | `web_logs``audit_logs`,大日志进入日志平台 |
| 模型评测 | `/modelTF/model-eval``/modelTF/dimension` | 当前运行 SQL 未覆盖 | `eval_tasks``eval_dimensions``eval_sample_results``eval_dimension_summaries` |
| 模型推理/对比 | `/modelTF/model-compare``/modelTF/model-chat/*` | 当前运行 SQL 未覆盖 | `inference_tasks``inference_task_models``chat_sessions``chat_messages` |
| 数据处理 | `/modelTF/data-process/*` | 当前运行 SQL 未覆盖 | `data_process_tasks``data_process_source_files``data_process_preview_items``data_process_results` |
| 数据转换/自定义工具 | `/modelTF/data-convert/jobs``/modelTF/tools` | 当前运行 SQL 未覆盖 | `data_convert_jobs``custom_tools` |
| 租户/项目/资源授权 | `/modelTF/tenants``/modelTF/projects``/modelTF/resources/{type}/{id}/acl` | 当前运行 SQL 未覆盖 | `tenants``tenant_users``projects``project_members``resource_acl` |
| 审批/审计/留存/配额 | `/modelTF/approvals``/modelTF/audit-logs``/modelTF/retention-policies``/modelTF/quotas/usage` | 当前运行 SQL 未覆盖 | `approval_templates``approval_instances``approval_steps``audit_logs``retention_policies``quotas``quota_usage` |
## 3. 文档和脚本检查结论
| 对象 | 当前结论 | 本次补充 |
| --- | --- | --- |
| 需求文档 | `docs/platform-architecture-requirements.md``docs/system-development-plan.md` 已覆盖多租户、项目隔离、审批、审计、多算力节点、应用/算力分离部署;缺少一份按当前菜单组织的总览 | 新增本文作为菜单和功能需求总览 |
| 接口文档 | `docs/backend-api-design.md` 已统一 `/modelTF`,并已有页面/接口映射;需要明确引用菜单总览,避免开发只看接口不看页面入口 | 在接口文档增加菜单总览引用 |
| 开发计划 | `docs/system-development-plan.md` 已按工作包列出页面、接口和 DB需要把本文作为任务认领入口 | 在开发计划增加菜单总览引用 |
| 部署文档 | `docs/deployment-plan.md``docker/README.md` 已覆盖应用/算力分离、单机多 GPU、本地磁盘、真实模式默认、Docker 拆分 | 暂无新增部署配置要求 |
| 目标 SQL | `docs/postgres-schema.sql` 覆盖完整目标模型,包含用户、权限、多租户、项目、审批、审计、模型、数据集、训练、评测、推理、算力、存储、导入、服务治理 | 暂不需要新增目标表 |
| 当前运行 SQL | `backend/app/db/sql/001_platform_runtime.sql` 只覆盖已接入运行接口的最小表集 | 后续每实现一个 P0/P1 菜单模块,应同步补运行 SQL 或迁移脚本;不能再以样例数据补功能 |
## 4. 后续补充原则
- 新增侧边栏菜单时必须同步补齐路由、权限码、接口文档、DB 表/迁移、审计动作、部署依赖。
- 新增后端接口时,必须在 `docs/backend-api-design.md` 标注对应页面/功能模块。
- 新增表结构时,目标模型写入 `docs/postgres-schema.sql`,当前可执行落库写入 `backend/app/db/sql/` 或 Alembic 迁移。
- 与训练、评测、推理、数据处理相关的异步任务必须落库,不能依赖前端本地状态。
- 与算力相关的功能默认走真实模式Simulator 只能显式开启用于隔离联调。

View File

@@ -1038,7 +1038,7 @@ YG_FT/
## 16. 页面模块开发工作包
本节用于多人并行开发时认领任务。每个工作包都标明对应页面、前端内容、后端接口、DB 表和部署/算力依赖。
本节用于多人并行开发时认领任务。每个工作包都标明对应页面、前端内容、后端接口、DB 表和部署/算力依赖。当前菜单、二级路由、规划菜单和接口/数据库映射总览见 `docs/menu-functional-requirements.md`
### 16.1 基础入口与用户权限

View File

@@ -26,14 +26,13 @@ npm run dev
后端 API 默认通过 Vite 代理转发到 `http://localhost:17861`(见 `vite.config.ts`)。
开发环境默认启用前端 Mock。如需联调真实后端使用
开发环境默认联调真实后端接口。如需进行隔离前端开发,可显式启用 Mock
```bash
VITE_ENABLE_MOCK=false npm run dev
VITE_ENABLE_MOCK=true npm run dev
```
生产构建默认不包含 Mock仅在演示构建中可显式设置
`VITE_ENABLE_MOCK=true`
真实联调、测试环境和生产环境不应启用 Mock。
## 构建

View File

@@ -1,7 +1,7 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
/** 是否启用前端 Mock开发环境默认开启,生产环境默认关闭。 */
/** 是否启用前端 Mock默认关闭,仅限隔离前端开发时显式开启。 */
readonly VITE_ENABLE_MOCK?: 'true' | 'false'
}

View File

@@ -17,10 +17,8 @@ app.use(createPinia())
app.use(router)
async function bootstrap() {
// 开发环境默认使用 Mock生产环境只有显式开启时才加载整套 Mock 数据
// 这样真实部署不会被前端适配器截断请求,也不会把 Mock 数据打进首屏包。
// Mock 仅允许显式开启。开发环境默认联调真实后端契约
const shouldEnableMock = import.meta.env.VITE_ENABLE_MOCK === 'true'
|| (import.meta.env.DEV && import.meta.env.VITE_ENABLE_MOCK !== 'false')
if (shouldEnableMock) {
const { installMockAdapter } = await import('@/mock/adapter')