feat: 更新后端平台模块、数据库、Compute引擎及多项配置文档
- 更新 backend 平台 API、platform_store、session 数据库模块 - 新增 backend SQL 初始化脚本 - 更新 compute 引擎适配器及 README - 更新 Docker 部署配置(app/compute) - 更新前端入口、环境类型声明及 README - 新增 docs/menu-functional-requirements.md 菜单功能需求文档 - 更新多项项目文档 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
19
README.md
19
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
YG_FT 是一个面向企业治理场景的模型微调平台,覆盖用户中心、多租户、项目隔离、数据集管理、模型管理、训练任务、评测、推理、审批流、审计留存、算力调度和训练引擎适配。
|
YG_FT 是一个面向企业治理场景的模型微调平台,覆盖用户中心、多租户、项目隔离、数据集管理、模型管理、训练任务、评测、推理、审批流、审计留存、算力调度和训练引擎适配。
|
||||||
|
|
||||||
当前前端已有基础页面,后端与算力平台已按多人协作开发方式建立工程骨架,并开始实现第一版可演示闭环能力。第一版实现不是临时代码,工程模块、配置项、接口标签和数据库对象均按后续正式开发可延续的方式命名。
|
当前前端已有基础页面,后端与算力平台已按多人协作开发方式建立工程骨架,并开始实现正式系统主链路能力。当前代码和 SQL 均作为后续生产演进基线维护,不再以一次性演示或静态 Mock 为开发准则。
|
||||||
|
|
||||||
## 总体架构
|
## 总体架构
|
||||||
|
|
||||||
@@ -38,12 +38,12 @@ YG_FT/
|
|||||||
| 训练引擎 | 当前固定接入 LLaMA-Factory,预留其他训练平台适配标准 | `compute/engines/` |
|
| 训练引擎 | 当前固定接入 LLaMA-Factory,预留其他训练平台适配标准 | `compute/engines/` |
|
||||||
| 数据层 | PostgreSQL、Redis、本地文件存储、日志归档 | `docs/postgres-schema.sql` |
|
| 数据层 | PostgreSQL、Redis、本地文件存储、日志归档 | `docs/postgres-schema.sql` |
|
||||||
|
|
||||||
## 第一版能力
|
## 当前开发基线
|
||||||
|
|
||||||
- 使用 FastAPI 提供统一 API 响应结构 `{ code, message, data }`。
|
- 使用 FastAPI 提供统一 API 响应结构 `{ code, message, data }`。
|
||||||
- 本地运行阶段使用 SQLite 初始化样例用户、模型、数据集、算力节点、GPU 和微调任务。
|
- 本地运行阶段统一使用 PostgreSQL,后端启动时会在 PG 中初始化当前运行表和系统内置账号;模型、数据集、算力节点、GPU、微调任务等业务数据必须通过页面、接口或正式导入流程产生。
|
||||||
- 支持登录、模型管理、数据集管理、微调任务创建/启动/停止/进度轮询。
|
- 支持登录、模型管理、数据集管理、微调任务创建/启动/停止/进度轮询。
|
||||||
- 支持训练日志、loss 指标、checkpoint 和训练产物的模拟生成。
|
- 支持训练日志、loss 指标、checkpoint 和训练产物接口;真实训练执行器接入前,联调状态机必须通过显式环境变量开启。
|
||||||
- 支持多算力节点、GPU、任务队列、资源副本和资源同步状态接口。
|
- 支持多算力节点、GPU、任务队列、资源副本和资源同步状态接口。
|
||||||
- 前端新增 `/compute` 算力节点页面,展示节点地址、权重、标签、启用状态、GPU、队列和资源副本。
|
- 前端新增 `/compute` 算力节点页面,展示节点地址、权重、标签、启用状态、GPU、队列和资源副本。
|
||||||
- `compute/engines/llama_factory/adapter.py` 提供 LLaMA-Factory 参数校验、命令生成和日志解析基础能力。
|
- `compute/engines/llama_factory/adapter.py` 提供 LLaMA-Factory 参数校验、命令生成和日志解析基础能力。
|
||||||
@@ -69,12 +69,14 @@ GET /modelTF/fine-tune
|
|||||||
GET /modelTF/compute/nodes
|
GET /modelTF/compute/nodes
|
||||||
```
|
```
|
||||||
|
|
||||||
本地运行时默认数据库路径:
|
本地运行时默认 PostgreSQL 连接:
|
||||||
|
|
||||||
```text
|
```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
|
```bash
|
||||||
@@ -92,7 +94,7 @@ cd compute
|
|||||||
uvicorn api.main:app --reload --port 19100
|
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/platform-architecture-requirements.md`:平台需求、功能模块、页面补全建议。
|
||||||
|
- `docs/menu-functional-requirements.md`:当前菜单、二级路由、规划菜单、功能需求、接口和数据库映射。
|
||||||
- `docs/backend-api-design.md`:FastAPI 接口分组、参数定义、权限说明。
|
- `docs/backend-api-design.md`:FastAPI 接口分组、参数定义、权限说明。
|
||||||
- `docs/postgres-schema.sql`:PostgreSQL 数据库脚本,包含权限、用户中心、多租户、审批、审计等模型。
|
- `docs/postgres-schema.sql`:PostgreSQL 数据库脚本,包含权限、用户中心、多租户、审批、审计等模型。
|
||||||
- `docs/system-development-plan.md`:多人协作开发计划,按前端、后端、DB、部署拆分。
|
- `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/backend-logging.md`:后端日志模块使用说明。
|
||||||
- `docs/deployment-plan.md`:后期部署方案,覆盖单机算力服务器部署与应用/算力分离部署。
|
- `docs/deployment-plan.md`:后期部署方案,覆盖单机算力服务器部署与应用/算力分离部署。
|
||||||
- `docker/README.md`:Docker 部署入口,包含应用服务器和算力服务器两套 Compose 使用方式。
|
- `docker/README.md`:Docker 部署入口,包含应用服务器和算力服务器两套 Compose 使用方式。
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, File, HTTPException, Query, UploadFile
|
from fastapi import APIRouter, Body, File, HTTPException, Query, UploadFile
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
@@ -147,7 +148,7 @@ async def delete_model(model_id: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
@router.post("/model-manage/merge")
|
@router.post("/model-manage/merge")
|
||||||
async def merge_model(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
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}")
|
@router.get("/dataset-manage/preview/{file_id}")
|
||||||
@@ -289,8 +290,11 @@ async def fine_tune_list() -> dict[str, Any]:
|
|||||||
|
|
||||||
@router.post("/fine-tune")
|
@router.post("/fine-tune")
|
||||||
async def create_fine_tune(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
async def create_fine_tune(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||||
task = get_platform_store().create_task(payload)
|
try:
|
||||||
return ok({"id": task["id"]})
|
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")
|
@router.post("/fine-tune/start")
|
||||||
@@ -361,7 +365,10 @@ async def compute_nodes() -> dict[str, Any]:
|
|||||||
|
|
||||||
@router.post("/compute/nodes")
|
@router.post("/compute/nodes")
|
||||||
async def create_compute_node(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
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}")
|
@router.put("/compute/nodes/{node_id}")
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ class Settings:
|
|||||||
app_env: str = os.getenv("APP_ENV", "local")
|
app_env: str = os.getenv("APP_ENV", "local")
|
||||||
route_prefix: str = os.getenv("MODELTF_ROUTE_PREFIX", "/modelTF")
|
route_prefix: str = os.getenv("MODELTF_ROUTE_PREFIX", "/modelTF")
|
||||||
app_mode: str = os.getenv("APP_MODE", "local")
|
app_mode: str = os.getenv("APP_MODE", "local")
|
||||||
local_db_path: str = os.getenv("LOCAL_DB_PATH", "./runtime/platform.db")
|
database_url: str = os.getenv("DATABASE_URL", "postgresql+psycopg://yg_ft:change_me@localhost:15432/yg_ft")
|
||||||
compute_mode: str = os.getenv("COMPUTE_MODE", "simulator")
|
compute_mode: str = os.getenv("COMPUTE_MODE", "real")
|
||||||
compute_status_sync_mode: str = os.getenv("COMPUTE_STATUS_SYNC_MODE", "polling")
|
compute_status_sync_mode: str = os.getenv("COMPUTE_STATUS_SYNC_MODE", "polling")
|
||||||
compute_poll_interval_seconds: int = _int_env("COMPUTE_POLL_INTERVAL_SECONDS", 3)
|
compute_poll_interval_seconds: int = _int_env("COMPUTE_POLL_INTERVAL_SECONDS", 3)
|
||||||
log_level: str = os.getenv("LOG_LEVEL", "INFO")
|
log_level: str = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
import math
|
import math
|
||||||
import sqlite3
|
import secrets
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
@@ -10,6 +12,8 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterator
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
@@ -53,175 +57,133 @@ def new_id(prefix: str) -> str:
|
|||||||
return f"{prefix}_{uuid.uuid4().hex[:12]}"
|
return f"{prefix}_{uuid.uuid4().hex[:12]}"
|
||||||
|
|
||||||
|
|
||||||
class PlatformStore:
|
PASSWORD_HASH_ITERATIONS = 390_000
|
||||||
"""Small SQLite-backed store for the first runnable platform version.
|
|
||||||
|
|
||||||
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
|
def hash_password(password: str) -> str:
|
||||||
provisioning enterprise infrastructure first.
|
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()
|
settings = get_settings()
|
||||||
raw_path = db_path or settings.local_db_path
|
self.database_url = _psycopg_url(database_url or settings.database_url)
|
||||||
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.ensure_schema()
|
self.ensure_schema()
|
||||||
self.ensure_seed_data()
|
self.ensure_seed_data()
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def connect(self) -> Iterator[sqlite3.Connection]:
|
def connect(self) -> Iterator["PgConnection"]:
|
||||||
conn = sqlite3.connect(self.db_path)
|
raw_conn = psycopg.connect(self.database_url)
|
||||||
conn.row_factory = sqlite3.Row
|
conn = PgConnection(raw_conn)
|
||||||
try:
|
try:
|
||||||
yield conn
|
yield conn
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def ensure_schema(self) -> None:
|
def ensure_schema(self) -> None:
|
||||||
|
schema_path = Path(__file__).with_name("sql") / "001_platform_runtime.sql"
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
conn.executescript(
|
conn.executescript(schema_path.read_text(encoding="utf-8"))
|
||||||
"""
|
columns = conn.execute(
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
"SELECT column_name FROM information_schema.columns WHERE table_name='users'"
|
||||||
id TEXT PRIMARY KEY,
|
).fetchall()
|
||||||
username TEXT NOT NULL UNIQUE,
|
column_names = {row["column_name"] for row in columns}
|
||||||
password TEXT NOT NULL,
|
if "password" in column_names and "password_hash" not in column_names:
|
||||||
display_name TEXT NOT NULL,
|
conn.execute("ALTER TABLE users RENAME COLUMN password TO password_hash")
|
||||||
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);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
def ensure_seed_data(self) -> None:
|
def ensure_seed_data(self) -> None:
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
@@ -245,336 +207,12 @@ class PlatformStore:
|
|||||||
conn.executemany(
|
conn.executemany(
|
||||||
"""
|
"""
|
||||||
INSERT INTO users
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
[(u[0], u[1], u[2], u[3], u[4], u[5], json_dumps(u[6]), now, u[7]) for u in users],
|
[(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],
|
||||||
)
|
)
|
||||||
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _duration(self, start_time: str | None, end_time: str | None = None) -> str:
|
def _duration(self, start_time: str | None, end_time: str | None = None) -> str:
|
||||||
start = parse_time(start_time)
|
start = parse_time(start_time)
|
||||||
if not start:
|
if not start:
|
||||||
@@ -590,6 +228,9 @@ class PlatformStore:
|
|||||||
return f"{sec}s"
|
return f"{sec}s"
|
||||||
|
|
||||||
def refresh_runtime_state(self) -> None:
|
def refresh_runtime_state(self) -> None:
|
||||||
|
if get_settings().compute_mode != "simulator":
|
||||||
|
return
|
||||||
|
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT * FROM fine_tune_tasks WHERE status IN ('syncing','queued','running')"
|
"SELECT * FROM fine_tune_tasks WHERE status IN ('syncing','queued','running')"
|
||||||
@@ -644,7 +285,7 @@ class PlatformStore:
|
|||||||
(status, progress, completed_at, row["id"]),
|
(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"
|
name = task.get("output_model_name") or f"{task['name']}-lora"
|
||||||
exists = conn.execute("SELECT id FROM trained_models WHERE name=?", (name,)).fetchone()
|
exists = conn.execute("SELECT id FROM trained_models WHERE name=?", (name,)).fetchone()
|
||||||
if exists:
|
if exists:
|
||||||
@@ -676,10 +317,19 @@ class PlatformStore:
|
|||||||
def login(self, username: str, password: str) -> dict[str, Any] | None:
|
def login(self, username: str, password: str) -> dict[str, Any] | None:
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
row = conn.execute("SELECT * FROM users WHERE username=?", (username,)).fetchone()
|
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
|
return None
|
||||||
last_login = utcnow()
|
last_login = utcnow()
|
||||||
conn.execute("UPDATE users SET last_login=? WHERE id=?", (last_login, row["id"]))
|
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 = self._user(row)
|
||||||
data["last_login"] = last_login
|
data["last_login"] = last_login
|
||||||
return data
|
return data
|
||||||
@@ -691,13 +341,13 @@ class PlatformStore:
|
|||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO users
|
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)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
user_id,
|
user_id,
|
||||||
payload["username"],
|
payload["username"],
|
||||||
payload.get("password", "platform123"),
|
hash_password(payload.get("password", "platform123")),
|
||||||
payload.get("display_name") or payload["username"],
|
payload.get("display_name") or payload["username"],
|
||||||
payload.get("role", "viewer"),
|
payload.get("role", "viewer"),
|
||||||
payload.get("status", "active"),
|
payload.get("status", "active"),
|
||||||
@@ -732,7 +382,7 @@ class PlatformStore:
|
|||||||
raise ValueError("protected user cannot be deleted")
|
raise ValueError("protected user cannot be deleted")
|
||||||
conn.execute("DELETE FROM users WHERE id=?", (user_id,))
|
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 {
|
return {
|
||||||
"id": row["id"],
|
"id": row["id"],
|
||||||
"username": row["username"],
|
"username": row["username"],
|
||||||
@@ -843,7 +493,7 @@ class PlatformStore:
|
|||||||
raise KeyError(dataset_id)
|
raise KeyError(dataset_id)
|
||||||
return self._dataset(conn, row)
|
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(
|
files = conn.execute(
|
||||||
"SELECT id, name, size, active_version_id, create_time FROM dataset_files WHERE dataset_id=? ORDER BY create_time",
|
"SELECT id, name, size, active_version_id, create_time FROM dataset_files WHERE dataset_id=? ORDER BY create_time",
|
||||||
(row["id"],),
|
(row["id"],),
|
||||||
@@ -884,9 +534,6 @@ class PlatformStore:
|
|||||||
utcnow(),
|
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())
|
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]:
|
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 dataset_files WHERE dataset_id=?", (dataset_id,))
|
||||||
conn.execute("DELETE FROM datasets WHERE 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()
|
now = utcnow()
|
||||||
file_id = new_id("file")
|
file_id = new_id("file")
|
||||||
version_id = f"{file_id}_v1"
|
version_id = f"{file_id}_v1"
|
||||||
@@ -947,7 +594,7 @@ class PlatformStore:
|
|||||||
)
|
)
|
||||||
return {"id": file_id, "name": name, "size": size}
|
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:
|
with self.connect() as conn:
|
||||||
row = conn.execute("SELECT * FROM dataset_files WHERE id=?", (file_id,)).fetchone()
|
row = conn.execute("SELECT * FROM dataset_files WHERE id=?", (file_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1008,7 +655,7 @@ class PlatformStore:
|
|||||||
raise KeyError(task_id)
|
raise KeyError(task_id)
|
||||||
return self._task(row)
|
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 = json_loads(row["payload"], {})
|
||||||
payload.update(
|
payload.update(
|
||||||
{
|
{
|
||||||
@@ -1028,6 +675,12 @@ class PlatformStore:
|
|||||||
def create_task(self, payload: dict[str, Any]) -> dict[str, Any]:
|
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"))
|
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:]}"
|
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()
|
now = utcnow()
|
||||||
task = {
|
task = {
|
||||||
"id": task_id,
|
"id": task_id,
|
||||||
@@ -1037,8 +690,8 @@ class PlatformStore:
|
|||||||
"train_type": payload.get("train_type", "SFT"),
|
"train_type": payload.get("train_type", "SFT"),
|
||||||
"train_method": payload.get("train_method", "lora"),
|
"train_method": payload.get("train_method", "lora"),
|
||||||
"template": payload.get("template", "qwen"),
|
"template": payload.get("template", "qwen"),
|
||||||
"base_model": payload.get("base_model") or payload.get("base_model_id") or "m_qwen25_7b",
|
"base_model": base_model,
|
||||||
"train_dataset_id": payload.get("train_dataset_id") or "ds_finance_sft",
|
"train_dataset_id": train_dataset_id,
|
||||||
"auto_merge": bool(payload.get("auto_merge", False)),
|
"auto_merge": bool(payload.get("auto_merge", False)),
|
||||||
"output_model_name": payload.get("output_model_name") or f"{name}-lora",
|
"output_model_name": payload.get("output_model_name") or f"{name}-lora",
|
||||||
"gpus": payload.get("gpus") or [],
|
"gpus": payload.get("gpus") or [],
|
||||||
@@ -1173,7 +826,7 @@ class PlatformStore:
|
|||||||
"pending": "waiting for start",
|
"pending": "waiting for start",
|
||||||
"syncing": "syncing model and dataset to compute node",
|
"syncing": "syncing model and dataset to compute node",
|
||||||
"queued": "waiting for GPU slot",
|
"queued": "waiting for GPU slot",
|
||||||
"running": "training with LLaMA-Factory simulator",
|
"running": "training with LLaMA-Factory",
|
||||||
"completed": "training completed",
|
"completed": "training completed",
|
||||||
"failed": "training stopped",
|
"failed": "training stopped",
|
||||||
}
|
}
|
||||||
@@ -1183,7 +836,7 @@ class PlatformStore:
|
|||||||
"status": status,
|
"status": status,
|
||||||
"progress": progress,
|
"progress": progress,
|
||||||
"step": labels.get(status, status),
|
"step": labels.get(status, status),
|
||||||
"speed": "42.5 samples/s" if status == "running" else "--",
|
"speed": task.get("train_speed") or "--",
|
||||||
"eta": eta,
|
"eta": eta,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1234,6 +887,40 @@ class PlatformStore:
|
|||||||
)
|
)
|
||||||
return next(n for n in self.compute_nodes() if n["id"] == node_id)
|
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]]:
|
def gpus(self) -> list[dict[str, Any]]:
|
||||||
self.refresh_runtime_state()
|
self.refresh_runtime_state()
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
@@ -1367,30 +1054,7 @@ class PlatformStore:
|
|||||||
"SELECT * FROM resource_replicas WHERE node_id=? ORDER BY create_time DESC",
|
"SELECT * FROM resource_replicas WHERE node_id=? ORDER BY create_time DESC",
|
||||||
(node_id,),
|
(node_id,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
if rows:
|
return [dict(row) for row in 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]:
|
def sync_job(self, sync_id: str) -> dict[str, Any]:
|
||||||
self.refresh_runtime_state()
|
self.refresh_runtime_state()
|
||||||
@@ -1423,7 +1087,7 @@ class PlatformStore:
|
|||||||
progress = int(task.get("progress", 0) or 0)
|
progress = int(task.get("progress", 0) or 0)
|
||||||
points = max(1, min(80, progress))
|
points = max(1, min(80, progress))
|
||||||
lines = [
|
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', [])}",
|
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",
|
"[INFO] command=llamafactory-cli train --stage sft --finetuning_type lora --do_train true",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ from sqlalchemy import create_engine
|
|||||||
from sqlalchemy.orm import Session, sessionmaker
|
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(
|
engine = create_engine(
|
||||||
DATABASE_URL,
|
DATABASE_URL,
|
||||||
pool_pre_ping=True,
|
pool_pre_ping=True,
|
||||||
future=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)
|
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False, future=True)
|
||||||
|
|
||||||
|
|||||||
133
backend/app/db/sql/001_platform_runtime.sql
Normal file
133
backend/app/db/sql/001_platform_runtime.sql
Normal 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);
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
name = "yg-ft-backend"
|
name = "yg-ft-backend"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Backend service for the model fine-tuning platform"
|
description = "Backend service for the model fine-tuning platform"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.111.0",
|
"fastapi>=0.111.0",
|
||||||
"uvicorn[standard]>=0.30.0",
|
"uvicorn[standard]>=0.30.0",
|
||||||
"python-multipart>=0.0.9",
|
"python-multipart>=0.0.9",
|
||||||
"pydantic>=2.7.0",
|
"pydantic>=2.7.0",
|
||||||
"sqlalchemy>=2.0.30",
|
"sqlalchemy>=2.0.30",
|
||||||
"asyncpg>=0.29.0",
|
"psycopg[binary]>=3.2.1",
|
||||||
"alembic>=1.13.1",
|
"alembic>=1.13.1",
|
||||||
"redis>=5.0.4",
|
"redis>=5.0.4",
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
@@ -26,7 +26,7 @@ dev = [
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py311"
|
target-version = "py312"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ uvicorn[standard]>=0.30.0
|
|||||||
python-multipart>=0.0.9
|
python-multipart>=0.0.9
|
||||||
pydantic>=2.7.0
|
pydantic>=2.7.0
|
||||||
sqlalchemy>=2.0.30
|
sqlalchemy>=2.0.30
|
||||||
asyncpg>=0.29.0
|
psycopg[binary]>=3.2.1
|
||||||
alembic>=1.13.1
|
alembic>=1.13.1
|
||||||
redis>=5.0.4
|
redis>=5.0.4
|
||||||
httpx>=0.27.0
|
httpx>=0.27.0
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ compute/
|
|||||||
tests/
|
tests/
|
||||||
```
|
```
|
||||||
|
|
||||||
## 第一版职责
|
## 开发职责
|
||||||
|
|
||||||
- GPU 发现、状态上报、锁定和释放。
|
- GPU 发现、状态上报、锁定和释放。
|
||||||
- 本地磁盘工作区管理。
|
- 本地磁盘工作区管理。
|
||||||
@@ -22,3 +22,8 @@ compute/
|
|||||||
- LLaMA-Factory 命令生成、日志解析、产物收集。
|
- LLaMA-Factory 命令生成、日志解析、产物收集。
|
||||||
- 分片上传、短时下载、离线导入。
|
- 分片上传、短时下载、离线导入。
|
||||||
- 通过服务间 token 接受应用平台调用。
|
- 通过服务间 token 接受应用平台调用。
|
||||||
|
|
||||||
|
## 运行模式
|
||||||
|
|
||||||
|
- 默认 `COMPUTE_EXECUTION_MODE=real`,Compute API 只暴露健康检查和接口契约;真实训练执行器完成前,创建作业会返回未实现错误。
|
||||||
|
- 仅隔离联调时可设置 `COMPUTE_EXECUTION_MODE=simulator`,启用内存状态机和合成 GPU/日志数据。该模式不得作为生产运行路径。
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ def create_app() -> FastAPI:
|
|||||||
def host_id() -> str:
|
def host_id() -> str:
|
||||||
return os.getenv("COMPUTE_HOST_ID", "gpu-node-01")
|
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]:
|
def job_status(job: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
if execution_mode() != "simulator":
|
||||||
|
return job
|
||||||
elapsed = max(0, int(now() - job["created_at"]))
|
elapsed = max(0, int(now() - job["created_at"]))
|
||||||
if job["status"] not in {"stopped", "failed", "completed"}:
|
if job["status"] not in {"stopped", "failed", "completed"}:
|
||||||
if elapsed < 5:
|
if elapsed < 5:
|
||||||
@@ -41,7 +46,7 @@ def create_app() -> FastAPI:
|
|||||||
progress = int(job.get("progress", 0) or 0)
|
progress = int(job.get("progress", 0) or 0)
|
||||||
points = max(1, min(80, progress))
|
points = max(1, min(80, progress))
|
||||||
lines = [
|
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'])}",
|
f"[INFO] command={' '.join(job['command'])}",
|
||||||
]
|
]
|
||||||
for step in range(1, points + 1):
|
for step in range(1, points + 1):
|
||||||
@@ -70,6 +75,8 @@ def create_app() -> FastAPI:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def gpu_resources() -> list[dict[str, Any]]:
|
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"}]
|
active_jobs = [job_status(job) for job in jobs.values() if job["status"] in {"queued", "running"}]
|
||||||
gpus: list[dict[str, Any]] = []
|
gpus: list[dict[str, Any]] = []
|
||||||
for idx in range(4):
|
for idx in range(4):
|
||||||
@@ -121,6 +128,7 @@ def create_app() -> FastAPI:
|
|||||||
"data_root_exists": data_root.exists(),
|
"data_root_exists": data_root.exists(),
|
||||||
"llama_factory_home": str(llama_factory_home),
|
"llama_factory_home": str(llama_factory_home),
|
||||||
"llama_factory_home_exists": llama_factory_home.exists(),
|
"llama_factory_home_exists": llama_factory_home.exists(),
|
||||||
|
"execution_mode": execution_mode(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get(f"{route_prefix}/v1/compute/jobs")
|
@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"))
|
command = build_command(payload, os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"))
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(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 = str(payload.get("id") or f"job_{int(now() * 1000)}")
|
||||||
job = {
|
job = {
|
||||||
"id": job_id,
|
"id": job_id,
|
||||||
|
|||||||
@@ -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")
|
model_path = config.get("base_model") or config.get("model_name_or_path")
|
||||||
dataset = config.get("dataset") or config.get("dataset_dir")
|
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 = [
|
command = [
|
||||||
"llamafactory-cli",
|
"llamafactory-cli",
|
||||||
"train",
|
"train",
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ $images | ForEach-Object { docker pull $_ }
|
|||||||
- `COMPUTE_API_PORT=19100`
|
- `COMPUTE_API_PORT=19100`
|
||||||
- `FILE_GATEWAY_PORT=19101`
|
- `FILE_GATEWAY_PORT=19101`
|
||||||
|
|
||||||
|
## 运行模式
|
||||||
|
|
||||||
|
- 应用侧默认 `COMPUTE_MODE=real`,任务状态必须由真实算力同步逻辑更新。
|
||||||
|
- 算力侧默认 `COMPUTE_EXECUTION_MODE=real`,真实执行器未完成前不会伪造训练作业。
|
||||||
|
- 仅隔离联调时可显式设置 `COMPUTE_MODE=simulator` 或 `COMPUTE_EXECUTION_MODE=simulator`,该模式不得用于测试环境、生产环境或生产升级基线。
|
||||||
|
|
||||||
## 应用服务器部署
|
## 应用服务器部署
|
||||||
|
|
||||||
应用服务器包含前端 Nginx、Backend API、PostgreSQL、Redis。
|
应用服务器包含前端 Nginx、Backend API、PostgreSQL、Redis。
|
||||||
@@ -112,7 +118,7 @@ http://<app-server-ip>:16801
|
|||||||
如果使用企业统一 PostgreSQL/Redis,修改 `docker/app/.env`:
|
如果使用企业统一 PostgreSQL/Redis,修改 `docker/app/.env`:
|
||||||
|
|
||||||
```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
|
REDIS_URL=redis://<redis-host>:16379/0
|
||||||
USE_BUILTIN_POSTGRES=false
|
USE_BUILTIN_POSTGRES=false
|
||||||
USE_BUILTIN_REDIS=false
|
USE_BUILTIN_REDIS=false
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ REDIS_PORT=16379
|
|||||||
POSTGRES_DB=yg_ft
|
POSTGRES_DB=yg_ft
|
||||||
POSTGRES_USER=yg_ft
|
POSTGRES_USER=yg_ft
|
||||||
POSTGRES_PASSWORD=change_me
|
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
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ COMPUTE_SERVICE_TOKEN=change_me
|
|||||||
FILE_GATEWAY_BASE_URL=http://compute-api:9101
|
FILE_GATEWAY_BASE_URL=http://compute-api:9101
|
||||||
|
|
||||||
# The application side polls Compute API for job state to avoid opening reverse network access.
|
# The application side polls Compute API for job state to avoid opening reverse network access.
|
||||||
|
COMPUTE_MODE=real
|
||||||
COMPUTE_STATUS_SYNC_MODE=polling
|
COMPUTE_STATUS_SYNC_MODE=polling
|
||||||
COMPUTE_POLL_INTERVAL_SECONDS=10
|
COMPUTE_POLL_INTERVAL_SECONDS=10
|
||||||
COMPUTE_POLL_BATCH_SIZE=100
|
COMPUTE_POLL_BATCH_SIZE=100
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ services:
|
|||||||
APP_ENV: ${APP_ENV:-prod}
|
APP_ENV: ${APP_ENV:-prod}
|
||||||
APP_NAME: ${APP_NAME:-YG Fine-Tune Platform API}
|
APP_NAME: ${APP_NAME:-YG Fine-Tune Platform API}
|
||||||
MODELTF_ROUTE_PREFIX: ${MODELTF_ROUTE_PREFIX:-/modelTF}
|
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}
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||||
USE_BUILTIN_POSTGRES: ${USE_BUILTIN_POSTGRES:-true}
|
USE_BUILTIN_POSTGRES: ${USE_BUILTIN_POSTGRES:-true}
|
||||||
USE_BUILTIN_REDIS: ${USE_BUILTIN_REDIS:-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_API_BASE_URL: ${COMPUTE_API_BASE_URL:-http://compute-api:9100}
|
||||||
COMPUTE_SERVICE_TOKEN: ${COMPUTE_SERVICE_TOKEN:-change_me}
|
COMPUTE_SERVICE_TOKEN: ${COMPUTE_SERVICE_TOKEN:-change_me}
|
||||||
FILE_GATEWAY_BASE_URL: ${FILE_GATEWAY_BASE_URL:-http://compute-api:9101}
|
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_STATUS_SYNC_MODE: ${COMPUTE_STATUS_SYNC_MODE:-polling}
|
||||||
COMPUTE_POLL_INTERVAL_SECONDS: ${COMPUTE_POLL_INTERVAL_SECONDS:-10}
|
COMPUTE_POLL_INTERVAL_SECONDS: ${COMPUTE_POLL_INTERVAL_SECONDS:-10}
|
||||||
COMPUTE_POLL_BATCH_SIZE: ${COMPUTE_POLL_BATCH_SIZE:-100}
|
COMPUTE_POLL_BATCH_SIZE: ${COMPUTE_POLL_BATCH_SIZE:-100}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
COMPUTE_ENV=prod
|
COMPUTE_ENV=prod
|
||||||
COMPUTE_HOST_ID=gpu-node-01
|
COMPUTE_HOST_ID=gpu-node-01
|
||||||
|
COMPUTE_EXECUTION_MODE=real
|
||||||
MODELTF_ROUTE_PREFIX=/modelTF
|
MODELTF_ROUTE_PREFIX=/modelTF
|
||||||
# Five-digit host ports exposed outside the compute server.
|
# Five-digit host ports exposed outside the compute server.
|
||||||
COMPUTE_API_PORT=19100
|
COMPUTE_API_PORT=19100
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
COMPUTE_ENV: ${COMPUTE_ENV:-prod}
|
COMPUTE_ENV: ${COMPUTE_ENV:-prod}
|
||||||
COMPUTE_HOST_ID: ${COMPUTE_HOST_ID:-gpu-node-01}
|
COMPUTE_HOST_ID: ${COMPUTE_HOST_ID:-gpu-node-01}
|
||||||
|
COMPUTE_EXECUTION_MODE: ${COMPUTE_EXECUTION_MODE:-real}
|
||||||
MODELTF_ROUTE_PREFIX: ${MODELTF_ROUTE_PREFIX:-/modelTF}
|
MODELTF_ROUTE_PREFIX: ${MODELTF_ROUTE_PREFIX:-/modelTF}
|
||||||
COMPUTE_SERVICE_TOKEN: ${COMPUTE_SERVICE_TOKEN:-change_me}
|
COMPUTE_SERVICE_TOKEN: ${COMPUTE_SERVICE_TOKEN:-change_me}
|
||||||
ENABLE_APP_CALLBACK: ${ENABLE_APP_CALLBACK:-false}
|
ENABLE_APP_CALLBACK: ${ENABLE_APP_CALLBACK:-false}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# 模型微调平台后端接口设计
|
# 模型微调平台后端接口设计
|
||||||
|
|
||||||
> 后端建议使用 FastAPI,统一挂载 `/modelTF` 前缀。本文根据当前 Vue 前端路由、API 模块、mock 数据和页面交互反推接口,并补充完整微调平台必须具备的用户中心、权限控制、审计、异步任务、文件版本与监控能力。
|
> 后端建议使用 FastAPI,统一挂载 `/modelTF` 前缀。本文根据当前 Vue 前端路由、API 模块和页面交互契约梳理接口,并补充完整微调平台必须具备的用户中心、权限控制、审计、异步任务、文件版本与监控能力。前端 Mock 仅作为隔离开发辅助,不作为接口设计准则。
|
||||||
|
|
||||||
|
菜单、二级路由、规划菜单、接口和数据库的总览映射见 `docs/menu-functional-requirements.md`。后续新增接口时,必须同步标注对应页面/功能模块。
|
||||||
|
|
||||||
## 1. 通用约定
|
## 1. 通用约定
|
||||||
|
|
||||||
@@ -88,7 +90,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:前端当前登录接口已经要求返回 `user`,mock 里只返回 token,正式后端必须返回完整用户信息。
|
说明:前端当前登录接口已经要求返回 `user`,正式后端必须返回完整用户信息。
|
||||||
|
|
||||||
### 2.2 当前用户
|
### 2.2 当前用户
|
||||||
|
|
||||||
@@ -158,7 +160,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:首页当前完全使用前端 mock,后端应提供聚合接口,避免前端拼多接口导致加载慢。
|
说明:首页需要由后端提供正式聚合接口,避免前端拼多接口导致加载慢。
|
||||||
|
|
||||||
### 3.2 健康指标
|
### 3.2 健康指标
|
||||||
|
|
||||||
@@ -318,7 +320,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
|||||||
|
|
||||||
## 6. 数据处理
|
## 6. 数据处理
|
||||||
|
|
||||||
当前数据处理页面为本地模拟,正式后端建议实现以下接口。
|
当前数据处理页面需要后端正式承接上传、切片、生成、编辑和发布流程,建议实现以下接口。
|
||||||
|
|
||||||
### 6.1 任务列表与详情
|
### 6.1 任务列表与详情
|
||||||
|
|
||||||
@@ -678,7 +680,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
|||||||
## 11. 后端开发需要补齐的关键点
|
## 11. 后端开发需要补齐的关键点
|
||||||
|
|
||||||
1. 前端路由已有 `user-settings`、`user-create`、`user-permission`、`permission-denied`,但当前仓库缺少对应 Vue 文件;后端仍应先实现用户中心和权限接口。
|
1. 前端路由已有 `user-settings`、`user-create`、`user-permission`、`permission-denied`,但当前仓库缺少对应 Vue 文件;后端仍应先实现用户中心和权限接口。
|
||||||
2. 数据处理主流程目前全在浏览器本地模拟,后端需要正式实现上传、切片、LLM 生成、结果编辑、发布数据集。
|
2. 数据处理主流程需要后端正式实现上传、切片、LLM 生成、结果编辑、发布数据集。
|
||||||
3. 训练、评测、数据处理、模型加载都不应同步阻塞 HTTP;建议接 Celery/RQ/Arq 或 FastAPI BackgroundTasks + 独立 worker。
|
3. 训练、评测、数据处理、模型加载都不应同步阻塞 HTTP;建议接 Celery/RQ/Arq 或 FastAPI BackgroundTasks + 独立 worker。
|
||||||
4. 文件内容不要全部入库;数据库保存元数据、版本、校验和、对象存储路径,内容放本地 NAS/MinIO。
|
4. 文件内容不要全部入库;数据库保存元数据、版本、校验和、对象存储路径,内容放本地 NAS/MinIO。
|
||||||
5. API Key、外部数据源密码必须加密存储,接口只回显脱敏。
|
5. API Key、外部数据源密码必须加密存储,接口只回显脱敏。
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
### 4.1 适用场景
|
### 4.1 适用场景
|
||||||
|
|
||||||
- PoC、试点环境、演示环境。
|
- 开发联调、单机试运行、资源受限的早期上线环境。
|
||||||
- 小团队共用一台单机多 GPU 服务器。
|
- 小团队共用一台单机多 GPU 服务器。
|
||||||
- 网络隔离要求不高,部署资源有限。
|
- 网络隔离要求不高,部署资源有限。
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ compute/engines/openrlhf/
|
|||||||
```env
|
```env
|
||||||
APP_ENV=prod
|
APP_ENV=prod
|
||||||
MODELTF_ROUTE_PREFIX=/modelTF
|
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
|
REDIS_URL=redis://redis:6379/0
|
||||||
LOG_DIR=/opt/yg-ft/logs/backend
|
LOG_DIR=/opt/yg-ft/logs/backend
|
||||||
COMPUTE_API_BASE_URL=https://compute.internal:19100
|
COMPUTE_API_BASE_URL=https://compute.internal:19100
|
||||||
@@ -327,7 +327,7 @@ CUDA_VISIBLE_DEVICES=0,1,2,3
|
|||||||
- 后端 `GET /modelTF/health` 正常。
|
- 后端 `GET /modelTF/health` 正常。
|
||||||
- Compute API 健康检查正常。
|
- Compute API 健康检查正常。
|
||||||
- Compute Agent 能识别 GPU、显存、CUDA 版本。
|
- Compute Agent 能识别 GPU、显存、CUDA 版本。
|
||||||
- LLaMA-Factory 能在命令行完成最小训练样例。
|
- LLaMA-Factory 能在命令行完成最小训练作业。
|
||||||
- 应用平台能提交训练任务到 Compute API。
|
- 应用平台能提交训练任务到 Compute API。
|
||||||
- 任务状态能从算力平台同步回应用平台。
|
- 任务状态能从算力平台同步回应用平台。
|
||||||
- 数据集上传、离线导入、产物下载路径权限正确。
|
- 数据集上传、离线导入、产物下载路径权限正确。
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# 第一版系统开发计划
|
# 当前系统主链路开发计划
|
||||||
|
|
||||||
> 说明:本计划虽然以“可演示闭环”为阶段目标,但实现内容按正式系统第一版开发,不在工程模块、配置项、接口标签、数据库对象中使用临时代码命名。初始化数据仅作为样例种子数据,后续可在此基础上继续扩展为生产能力。
|
> 说明:本计划描述当前正在开发的系统能力。代码、接口和 SQL 均按后续生产演进基线维护,不以一次性演示、静态 Mock 或样例数据作为开发准则。联调辅助能力必须显式配置启用,并不得成为默认运行路径。
|
||||||
|
|
||||||
## 1. 阶段目标
|
## 1. 阶段目标
|
||||||
|
|
||||||
第一版系统需要在没有完整生产环境、没有真实 GPU 或没有 LLaMA-Factory 运行环境时,仍然可以跑通模型微调平台的主链路:
|
当前阶段需要完成模型微调平台的主链路工程基础:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
登录
|
登录
|
||||||
@@ -12,24 +12,26 @@
|
|||||||
-> 数据集管理
|
-> 数据集管理
|
||||||
-> 创建微调任务
|
-> 创建微调任务
|
||||||
-> 调度算力节点与 GPU
|
-> 调度算力节点与 GPU
|
||||||
-> 模拟资源副本检查和同步
|
-> 检查并同步模型/数据集资源
|
||||||
-> 启动训练任务
|
-> 启动训练任务
|
||||||
-> 查看任务状态、GPU 占用、训练日志、loss 曲线
|
-> 轮询任务状态、GPU 占用、训练日志、loss 曲线
|
||||||
-> 训练完成后登记训练产物
|
-> 训练完成后登记训练产物
|
||||||
```
|
```
|
||||||
|
|
||||||
该阶段不是一次性展示代码,而是后续系统继续开发的基础版本。算力、训练框架、资源同步、任务状态机均按真实平台边界设计,当前仅以 `simulator` 模式补齐无 GPU 环境下的可运行能力。
|
该阶段是正式系统的第一批可运行能力,不再初始化业务样例数据。系统只允许初始化内置管理员/运维账号,模型、数据集、算力节点、GPU、训练任务和资源副本必须通过页面、接口、算力 Agent 扫描或正式导入流程产生。
|
||||||
|
|
||||||
## 2. 运行模式
|
## 2. 运行模式
|
||||||
|
|
||||||
| 模式 | 说明 | 当前状态 |
|
| 模式 | 说明 | 当前要求 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `simulator` | 模拟 GPU、训练进程、日志、指标、checkpoint 和训练产物 | 第一版默认实现 |
|
| `real` | 面向真实部署,等待 Compute API、Agent、File Gateway 和 LLaMA-Factory 执行器回写状态 | 默认模式 |
|
||||||
| `real` | 预留真实 `nvidia-smi`、LLaMA-Factory 进程启动、真实日志解析和产物扫描 | 后续接入 |
|
| `simulator` | 仅用于隔离联调,无真实 GPU 时临时推进任务状态、GPU 状态和训练日志 | 必须显式开启,不得用于生产基线 |
|
||||||
|
|
||||||
第一版实现必须保留 `real` 模式边界,不允许把训练命令直接写死在应用平台后端。训练相关能力统一沉淀在 `compute/engines/` 适配层。
|
后端默认 `COMPUTE_MODE=real`。在该模式下,任务状态不再按时间自动推进,必须由后续真实算力同步逻辑更新。算力服务默认 `COMPUTE_EXECUTION_MODE=real`,真实训练执行器未完成前,创建训练作业会返回明确的未实现错误,避免误认为已经完成生产训练能力。
|
||||||
|
|
||||||
## 3. 第一批开发范围
|
训练相关能力必须沉淀在 `compute/engines/` 适配层,不允许在应用平台后端直接拼接或执行训练命令。
|
||||||
|
|
||||||
|
## 3. 当前开发范围
|
||||||
|
|
||||||
### 3.1 应用平台后端
|
### 3.1 应用平台后端
|
||||||
|
|
||||||
@@ -38,24 +40,27 @@
|
|||||||
```text
|
```text
|
||||||
backend/app/
|
backend/app/
|
||||||
api/v1/endpoints/platform.py
|
api/v1/endpoints/platform.py
|
||||||
db/platform_store.py
|
core/
|
||||||
db/session.py
|
db/
|
||||||
```
|
```
|
||||||
|
|
||||||
已覆盖能力:
|
已建立能力:
|
||||||
- 统一 API 响应结构 `{ code, message, data }`。
|
- 统一 API 响应结构 `{ code, message, data }`。
|
||||||
- 初始化样例用户、模型、数据集、算力节点、GPU 和微调任务。
|
- PostgreSQL 运行表初始化,当前执行脚本位于 `backend/app/db/sql/001_platform_runtime.sql`。
|
||||||
- 登录、当前用户、用户列表与权限。
|
- 内置管理员账号初始化,业务数据不再自动写入样例记录。
|
||||||
- 模型管理、训练产物列表、权重合并任务占位。
|
- 登录、当前用户、用户列表与权限页面接口。
|
||||||
|
- 模型管理、训练产物列表、权重合并任务入口。
|
||||||
- 数据集管理、文件上传、预览、版本管理和下载。
|
- 数据集管理、文件上传、预览、版本管理和下载。
|
||||||
- 微调任务创建、启动、停止、删除、进度查询、checkpoint 查询。
|
- 微调任务创建、启动、停止、删除、进度查询、checkpoint 查询。
|
||||||
- 系统健康指标、系统信息、训练日志、系统日志。
|
- 系统健康指标、系统信息、训练日志、系统日志接口。
|
||||||
- 算力节点、GPU、队列、资源副本、资源同步任务接口。
|
- 算力节点、GPU、队列、资源副本、资源同步任务接口。
|
||||||
|
|
||||||
开发约定:
|
待继续开发:
|
||||||
- 当前本地可运行存储使用 SQLite,配置项为 `LOCAL_DB_PATH`。
|
- 接入正式 ORM/Repository/Service 分层和 Alembic 迁移。
|
||||||
- 生产数据库仍以 PostgreSQL 为目标,正式表结构以 `docs/postgres-schema.sql` 为准。
|
- 将任务状态更新改为应用侧定时轮询 Compute API/File Gateway 后落库。
|
||||||
- `backend/app/db/session.py` 已提供 SQLAlchemy session 入口,后续替换正式 ORM 时从此处收口。
|
- 完成项目/模型/数据集级权限隔离校验。
|
||||||
|
- 完成审批流、审计留存、配额、资源申请和多租户上下文。
|
||||||
|
- 增加正式异常码、接口鉴权、中间件、幂等控制和分页规范。
|
||||||
|
|
||||||
### 3.2 算力平台服务
|
### 3.2 算力平台服务
|
||||||
|
|
||||||
@@ -64,72 +69,77 @@ backend/app/
|
|||||||
```text
|
```text
|
||||||
compute/
|
compute/
|
||||||
api/main.py
|
api/main.py
|
||||||
engines/llama_factory/adapter.py
|
agent/
|
||||||
|
engines/llama_factory/
|
||||||
|
file_gateway/
|
||||||
```
|
```
|
||||||
|
|
||||||
已覆盖能力:
|
已建立能力:
|
||||||
- `/modelTF/health` 与 `/modelTF/v1/compute/health` 节点健康检查。
|
- `/modelTF/health` 与 `/modelTF/v1/compute/health` 节点健康检查。
|
||||||
- `/modelTF/compute/resources/gpus` GPU 状态模拟。
|
- LLaMA-Factory 参数校验、命令生成和训练日志指标解析。
|
||||||
- `/modelTF/compute/jobs` 创建、查询、停止训练任务。
|
- Compute API 作业、GPU、文件网关接口壳。
|
||||||
- `/modelTF/compute/jobs/{id}/logs` 拉取训练日志与解析指标。
|
- 显式 `simulator` 模式下的内存状态机,用于隔离联调。
|
||||||
- `/modelTF/compute/files/upload` 与 `/modelTF/compute/files/{id}/download` 文件网关占位。
|
|
||||||
- LLaMA-Factory 参数校验、命令生成、日志指标解析。
|
|
||||||
|
|
||||||
设计约定:
|
待继续开发:
|
||||||
- 多算力节点阶段,每台单机多 GPU 服务器都部署一套 Compute API、Agent、File Gateway 和训练框架。
|
- 真实 GPU 发现:接入 `nvidia-smi` 或 NVML。
|
||||||
- 应用平台通过 `compute_nodes` 中维护的 `api_base_url`、`file_gateway_url` 主动轮询算力节点。
|
- GPU 锁定与释放:一张 GPU 同一时间只分配给一个训练或推理任务。
|
||||||
- 算力节点之间默认不互访,资源副本和同步状态由应用平台统一编排。
|
- LLaMA-Factory 真实执行器:生成 YAML/命令、启动进程、停止进程、采集 PID。
|
||||||
|
- 训练日志采集:读取宿主机挂载日志文件,解析 loss、learning rate、epoch 等指标。
|
||||||
|
- Checkpoint/adapter/merged model 扫描与产物登记。
|
||||||
|
- File Gateway:本地磁盘文件上传、下载、校验、导入和跨节点资源同步。
|
||||||
|
|
||||||
### 3.3 前端页面
|
### 3.3 前端页面
|
||||||
|
|
||||||
已接入页面:
|
已接入页面:
|
||||||
- `/login`:登录接口。
|
- `/login`:登录接口。
|
||||||
- `/model-manage`:模型列表与模型来源。
|
- `/model-manage`:模型列表、模型来源、训练产物。
|
||||||
- `/dataset`、`/dataset/:id/preview`:数据集列表、预览和版本。
|
- `/dataset`、`/dataset/:id/preview`:数据集列表、预览、版本。
|
||||||
- `/fine-tune`、`/fine-tune/create`:微调任务创建、启动、状态轮询。
|
- `/fine-tune`、`/fine-tune/create`:微调任务创建、启动、状态轮询。
|
||||||
- `/training-log/:id`:训练日志和 loss 曲线。
|
- `/training-log/:id`:训练日志和 loss 曲线。
|
||||||
- `/hardware`:平台 GPU 与系统性能。
|
- `/hardware`:平台 GPU 与系统性能。
|
||||||
- `/compute`:算力节点、GPU、队列、资源副本。
|
- `/compute`:算力节点、GPU、队列、资源副本。
|
||||||
|
|
||||||
新增前端能力:
|
前端 Mock 默认关闭。仅在隔离前端开发时可设置 `VITE_ENABLE_MOCK=true`,真实联调和后续生产演进均以 `/modelTF` 后端接口为准。
|
||||||
- `frontend/src/api/modules/compute.ts`:算力节点 API 包装。
|
|
||||||
- `frontend/src/views/compute/ComputeNodesView.vue`:节点、GPU、队列、资源副本四个视图。
|
待继续开发:
|
||||||
- 权限枚举增加 `compute`,菜单增加“算力节点”入口。
|
- 补齐多租户、项目管理、审批中心、审计中心、配额管理页面。
|
||||||
|
- 完成算力节点管理表单,包括节点地址、权重、标签、启用状态和健康检查结果。
|
||||||
|
- 完成模型/数据集导入页面,支持本地路径扫描和归属项目选择。
|
||||||
|
- 推理服务页面需接入真实后端任务接口,移除页面内本地假对话路径。
|
||||||
|
|
||||||
### 3.4 数据库
|
### 3.4 数据库
|
||||||
|
|
||||||
第一版运行时使用本地 SQLite 以降低开发环境门槛,但数据库模型仍按 PostgreSQL 正式设计推进。
|
当前运行 SQL:
|
||||||
|
|
||||||
核心表范围:
|
```text
|
||||||
- 用户、租户、项目、角色、权限。
|
backend/app/db/sql/001_platform_runtime.sql
|
||||||
- 模型、训练产物、数据集、数据集文件、存储对象。
|
```
|
||||||
- 微调任务、训练指标、checkpoint、审计日志。
|
|
||||||
- 算力节点、GPU 设备、算力任务、GPU 分配。
|
|
||||||
- 资源副本、资源同步任务。
|
|
||||||
|
|
||||||
优化方向:
|
架构目标 SQL:
|
||||||
- 任务状态、租户/项目隔离字段、资源副本定位字段需要建立索引。
|
|
||||||
- 审计日志、训练指标、日志归档后续建议按时间分区。
|
```text
|
||||||
- 大文件只落本地磁盘或对象存储,数据库保存元数据和版本关系。
|
docs/postgres-schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
当前运行 SQL 用于支持已开发接口落库;架构目标 SQL 包含用户中心、多租户、项目隔离、审批、审计、配额、评测等完整模型。后续需要通过 Alembic 将二者收敛为统一迁移体系,生产升级只走迁移脚本,不依赖手工改表。
|
||||||
|
|
||||||
## 4. 验收标准
|
## 4. 验收标准
|
||||||
|
|
||||||
第一版完成后应满足:
|
- 启动后端必须连接 PostgreSQL,不允许回退到 SQLite。
|
||||||
- 前端不启用静态 Mock 时,可以通过 FastAPI 获取主要页面数据。
|
- 后端启动只初始化系统内置账号,不初始化模型、数据集、算力节点、GPU、训练任务等业务样例数据。
|
||||||
- 登录后可以看到模型、数据集、微调任务和算力节点。
|
- 前端默认请求真实 `/modelTF` 接口,除非显式设置 `VITE_ENABLE_MOCK=true`。
|
||||||
- 创建微调任务后可以启动任务,任务状态自动流转。
|
- 默认 `real` 模式下任务状态不自动伪造完成,必须等待真实算力同步。
|
||||||
- GPU 状态会随任务进入 `syncing`、`queued`、`running` 发生变化。
|
- 显式 `simulator` 模式只能用于隔离联调,部署文档必须标注不得用于生产。
|
||||||
- 训练日志持续生成,日志详情页能解析 loss、grad norm、learning rate、epoch。
|
- 登录后可以进入主界面,并可通过页面/API 创建真实业务记录。
|
||||||
- 任务完成后训练产物出现在模型管理的训练产物列表中。
|
- 代码、接口路由、配置项、数据库表名不得使用 `demo` 命名。
|
||||||
- 算力节点页面可展示节点地址、权重、标签、启用状态、队列和资源副本。
|
|
||||||
- 工程命名、配置和接口分组均可作为后续正式开发基础。
|
|
||||||
|
|
||||||
## 5. 后续开发计划
|
## 5. 后续开发计划
|
||||||
|
|
||||||
| 阶段 | 重点 | 说明 |
|
| 阶段 | 重点 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 第二阶段 | PostgreSQL ORM 与 Alembic | 将 `platform_store.py` 的能力迁移为正式 ORM、迁移脚本和 repository/service 分层 |
|
| 阶段 1 | 数据库迁移体系 | 将当前运行 SQL 与架构 SQL 收敛到 Alembic 迁移 |
|
||||||
| 第三阶段 | 真实 Compute Agent | 接入 `nvidia-smi`、GPU 锁定、进程管理、训练日志文件采集 |
|
| 阶段 2 | 后端领域分层 | 拆分用户、模型、数据集、训练、算力、审计等模块 |
|
||||||
| 第四阶段 | LLaMA-Factory 真实训练 | 生成 YAML/命令、启动训练进程、停止任务、扫描 checkpoint 和 adapter |
|
| 阶段 3 | 真实 Compute Agent | GPU 发现、资源锁定、进程管理、日志采集 |
|
||||||
| 第五阶段 | 企业治理 | 多租户、项目/模型/数据集隔离、审批流、审计留存、配额和资源申请 |
|
| 阶段 4 | LLaMA-Factory 训练执行 | YAML/命令生成、进程启动/停止、checkpoint 和 adapter 扫描 |
|
||||||
| 第六阶段 | 多节点调度 | 基于 `compute_nodes`、资源副本和节点标签实现自动/手动调度策略 |
|
| 阶段 5 | 企业治理 | 多租户、项目隔离、审批流、审计留存、配额和资源申请 |
|
||||||
|
| 阶段 6 | 多算力节点调度 | 基于 `compute_nodes`、标签、权重、资源副本和节点健康实现调度策略 |
|
||||||
|
|||||||
98
docs/menu-functional-requirements.md
Normal file
98
docs/menu-functional-requirements.md
Normal 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 只能显式开启用于隔离联调。
|
||||||
@@ -1038,7 +1038,7 @@ YG_FT/
|
|||||||
|
|
||||||
## 16. 页面模块开发工作包
|
## 16. 页面模块开发工作包
|
||||||
|
|
||||||
本节用于多人并行开发时认领任务。每个工作包都标明对应页面、前端内容、后端接口、DB 表和部署/算力依赖。
|
本节用于多人并行开发时认领任务。每个工作包都标明对应页面、前端内容、后端接口、DB 表和部署/算力依赖。当前菜单、二级路由、规划菜单和接口/数据库映射总览见 `docs/menu-functional-requirements.md`。
|
||||||
|
|
||||||
### 16.1 基础入口与用户权限
|
### 16.1 基础入口与用户权限
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,13 @@ npm run dev
|
|||||||
|
|
||||||
后端 API 默认通过 Vite 代理转发到 `http://localhost:17861`(见 `vite.config.ts`)。
|
后端 API 默认通过 Vite 代理转发到 `http://localhost:17861`(见 `vite.config.ts`)。
|
||||||
|
|
||||||
开发环境默认启用前端 Mock。如需联调真实后端,使用:
|
开发环境默认联调真实后端接口。如需进行隔离前端开发,可显式启用 Mock:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VITE_ENABLE_MOCK=false npm run dev
|
VITE_ENABLE_MOCK=true npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
生产构建默认不包含 Mock;仅在演示构建中可显式设置
|
真实联调、测试环境和生产环境不应启用 Mock。
|
||||||
`VITE_ENABLE_MOCK=true`。
|
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
|
|||||||
2
frontend/src/env.d.ts
vendored
2
frontend/src/env.d.ts
vendored
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
/** 是否启用前端 Mock;开发环境默认开启,生产环境默认关闭。 */
|
/** 是否启用前端 Mock;默认关闭,仅限隔离前端开发时显式开启。 */
|
||||||
readonly VITE_ENABLE_MOCK?: 'true' | 'false'
|
readonly VITE_ENABLE_MOCK?: 'true' | 'false'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ app.use(createPinia())
|
|||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
// 开发环境默认使用 Mock;生产环境只有显式开启时才加载整套 Mock 数据。
|
// Mock 仅允许显式开启。开发环境默认联调真实后端契约。
|
||||||
// 这样真实部署不会被前端适配器截断请求,也不会把 Mock 数据打进首屏包。
|
|
||||||
const shouldEnableMock = import.meta.env.VITE_ENABLE_MOCK === 'true'
|
const shouldEnableMock = import.meta.env.VITE_ENABLE_MOCK === 'true'
|
||||||
|| (import.meta.env.DEV && import.meta.env.VITE_ENABLE_MOCK !== 'false')
|
|
||||||
|
|
||||||
if (shouldEnableMock) {
|
if (shouldEnableMock) {
|
||||||
const { installMockAdapter } = await import('@/mock/adapter')
|
const { installMockAdapter } = await import('@/mock/adapter')
|
||||||
|
|||||||
Reference in New Issue
Block a user