From 4050c120d53cf9a0b5b7c1b6a2dc4303d0a5d2ac Mon Sep 17 00:00:00 2001 From: wuyongtao Date: Thu, 16 Jul 2026 11:52:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Docker=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=8E=E9=A1=B9=E7=9B=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Dockerfile, docker-compose.yml, .dockerignore, nginx 配置 - 添加后端 API 设计文档、平台架构需求文档、系统开发计划 - 添加 PostgreSQL schema 设计 - 更新 README.md 和 design-qa.md --- .dockerignore | 11 + Dockerfile | 19 + docker-compose.yml | 15 + docker/nginx.conf.template | 30 + docs/backend-api-design.md | 1070 +++++++++++++++ docs/platform-architecture-requirements.md | 744 +++++++++++ docs/postgres-schema.sql | 1386 ++++++++++++++++++++ docs/system-development-plan.md | 1094 +++++++++++++++ 8 files changed, 4369 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 docker/nginx.conf.template create mode 100644 docs/backend-api-design.md create mode 100644 docs/platform-architecture-requirements.md create mode 100644 docs/postgres-schema.sql create mode 100644 docs/system-development-plan.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e3697f7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.gitignore +node_modules +frontend/node_modules +frontend/dist +frontend/.vite +npm-debug.log* +docker-compose*.yml +README.md +design-qa.md +docs \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5ef763d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1 + +FROM node:20-alpine AS build +WORKDIR /app/frontend + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ +RUN npm run build + +FROM nginx:1.27-alpine +COPY docker/nginx.conf.template /etc/nginx/templates/default.conf.template +COPY --from=build /app/frontend/dist /usr/share/nginx/html + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..897c8e7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + yg-ft-frontend: + build: + context: . + dockerfile: Dockerfile + image: yg-ft-frontend:latest + container_name: yg-ft-frontend + ports: + - "6801:80" + environment: + # Change this if the backend is deployed somewhere else. + API_PROXY_PASS: "http://host.docker.internal:7861" + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped \ No newline at end of file diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template new file mode 100644 index 0000000..5a4dc65 --- /dev/null +++ b/docker/nginx.conf.template @@ -0,0 +1,30 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 200m; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_pass ${API_PROXY_PASS}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ { + try_files $uri =404; + expires 30d; + add_header Cache-Control "public, immutable"; + } +} \ No newline at end of file diff --git a/docs/backend-api-design.md b/docs/backend-api-design.md new file mode 100644 index 0000000..32ac458 --- /dev/null +++ b/docs/backend-api-design.md @@ -0,0 +1,1070 @@ +# 模型微调平台后端接口设计 + +> 后端建议使用 FastAPI,统一挂载 `/api` 前缀。本文根据当前 Vue 前端路由、API 模块、mock 数据和页面交互反推接口,并补充完整微调平台必须具备的用户中心、权限控制、审计、异步任务、文件版本与监控能力。 + +## 1. 通用约定 + +### 1.1 统一响应 + +```json +{ + "code": 0, + "message": "ok", + "data": {} +} +``` + +- `code=0` 成功;非 0 为业务错误。 +- HTTP 状态码仍用于认证失败、权限不足、参数错误、系统异常。 +- 前端当前 axios 已按 `{ code, data, message }` 解包。 + +### 1.2 认证与权限 + +- 登录成功返回 JWT access token,前端后续请求增加 `Authorization: Bearer `。 +- 当前前端权限码:`dashboard`、`fine-tune`、`model-eval`、`model-inference`、`model-manage`、`dataset`、`data-process`、`data-convert`、`hardware`、`logs`、`user-settings`。 +- 后端 RBAC 建议按 `permission.code + role_permission + user_permission_override` 实现。 +- 所有写操作记录审计日志。 + +### 1.3 分页、排序、筛选 + +当前前端大多直接取数组,后端建议同时支持分页,便于数据量增长: + +```text +page=1&page_size=20&keyword=xxx&sort=-created_at +``` + +分页响应: + +```json +{ + "items": [], + "total": 0, + "page": 1, + "page_size": 20 +} +``` + +### 1.4 异步任务 + +训练、评测、模型加载、数据处理、文件转换都应落为异步任务: + +- 创建任务:返回 `task_id`。 +- 查询详情:返回状态、进度、错误信息、运行统计。 +- 实时进度:优先 SSE,必要时 WebSocket。 + +通用状态建议:`pending`、`running`、`completed`、`failed`、`stopped`。 + +## 2. 用户中心与系统权限 + +### 2.1 登录 + +`POST /api/login` + +请求: + +```json +{ + "username": "admin", + "password": "password" +} +``` + +响应: + +```json +{ + "token": "jwt-token", + "user": { + "id": "uuid", + "username": "admin", + "display_name": "系统管理员", + "role": "admin", + "status": "active", + "permissions": ["dashboard", "fine-tune"], + "create_time": "2026-07-16T10:00:00+08:00", + "last_login": "2026-07-16T10:00:00+08:00", + "protected": true + } +} +``` + +说明:前端当前登录接口已经要求返回 `user`,mock 里只返回 token,正式后端必须返回完整用户信息。 + +### 2.2 当前用户 + +`GET /api/me` + +用于刷新页面后恢复用户信息和权限,避免完全依赖 localStorage。 + +### 2.3 用户管理 + +| 方法 | 路径 | 说明 | 权限 | +| --- | --- | --- | --- | +| GET | `/api/users` | 用户列表 | `user-settings` | +| POST | `/api/users` | 创建用户 | `user-settings` | +| PUT | `/api/users/{id}` | 更新角色、状态、页面权限 | `user-settings` | +| DELETE | `/api/users/{id}` | 删除用户 | `user-settings` | +| PUT | `/api/users/{id}/password` | 重置密码 | `user-settings` | + +创建用户请求: + +```json +{ + "username": "zhangsan", + "display_name": "张三", + "password": "InitialPass123", + "role": "operator", + "status": "active", + "permissions": ["dashboard", "fine-tune", "dataset"] +} +``` + +更新权限请求: + +```json +{ + "role": "viewer", + "status": "active", + "permissions": ["dashboard", "logs"] +} +``` + +## 3. 服务看板与系统监控 + +### 3.1 首页看板 + +`GET /api/dashboard/overview?period=7d` + +返回: + +```json +{ + "health": { + "state": "normal", + "online_services": 12, + "running_tasks": 5, + "pending_alerts": 2 + }, + "service_statuses": [ + { "name": "模型推理", "state": "normal", "instances_online": 6, "instances_total": 6 } + ], + "training_stats": [ + { "date": "2026-07-16", "train_count": 11, "gpu_count": 5, "avg_score": 89.0 } + ], + "recent_tasks": [], + "operation_distribution": [], + "login_duration_rank": [], + "recent_login_users": [] +} +``` + +说明:首页当前完全使用前端 mock,后端应提供聚合接口,避免前端拼多接口导致加载慢。 + +### 3.2 健康指标 + +`GET /api/health` + +响应字段兼容前端 `HealthMetrics`: + +```json +{ + "cpu_percent": 32, + "memory_percent": 58, + "disk_percent": 45 +} +``` + +### 3.3 平台性能 + +`GET /api/system-info` + +返回 CPU、内存、磁盘、GPU、网络、系统运行时间。GPU 进程字段建议包含 `pid`、`name`、`memory_used_gb`、`task_name`、`user`。 + +### 3.4 日志 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/log-files?date=2026-07-16` | 系统日志文件列表 | +| GET | `/api/log-content?file=system.log` | 系统日志内容 | +| GET | `/api/training-log-files` | 训练日志文件列表 | +| GET | `/api/training-log-content?file=xxx.log` | 训练日志内容 | +| POST | `/api/web-log` | 前端错误/行为日志 | + +日志内容应支持 `tail`、`offset`、`limit` 参数,避免一次返回超大文件。 + +## 4. 模型管理 + +### 4.1 模型登记 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/model-manage` | 模型列表 | +| GET | `/api/model-manage/{id}` | 模型详情 | +| GET | `/api/model-manage/name/{name}` | 按名称查询 | +| POST | `/api/model-manage` | 创建模型 | +| PUT | `/api/model-manage/{id}` | 编辑模型 | +| DELETE | `/api/model-manage/{id}` | 删除模型 | +| PUT | `/api/model-manage/{id}/purpose` | 修改用途 | +| GET | `/api/model-manage/local-models` | 扫描本地模型目录 | + +创建/编辑请求: + +```json +{ + "name": "Qwen2.5-7B-Instruct", + "type": "LLM", + "purpose": "training", + "model_source": "local", + "description": "训练基座", + "path": "/data/models/qwen2.5-7b", + "api_url": null, + "api_key": null, + "online_model_name": null +} +``` + +字段说明: + +- `type`: `LLM`、`CV`、`NLP`、`Embedding`、`Other`。 +- `purpose`: `training`、`inference`、`evaluation`。 +- `model_source`: `local`、`api`。 +- `api_key` 后端加密存储,列表接口只返回脱敏值。 + +### 4.2 已训练模型与权重合并 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/model-manage/trained-models` | 已训练模型列表 | +| DELETE | `/api/model-manage/trained-models/{id}?type=merged\|lora` | 删除训练产物 | +| POST | `/api/model-manage/merge` | 合并 LoRA 权重 | +| GET | `/api/model-manage/trained-models/{model_name}/export` | 导出模型文件 | + +合并请求: + +```json +{ + "model_name": "qwen-ft-finance-001", + "train_method": "lora", + "base_model_path": "/data/models/qwen2.5-7b" +} +``` + +响应: + +```json +{ + "merge_task_id": "uuid", + "status": "pending" +} +``` + +## 5. 数据集管理 + +### 5.1 数据集 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/dataset-manage` | 数据集列表 | +| GET | `/api/dataset-manage/{id}` | 数据集详情 | +| POST | `/api/dataset-manage` | 创建数据集 | +| PUT | `/api/dataset-manage/{id}` | 更新数据集 | +| DELETE | `/api/dataset-manage/{id}` | 删除数据集 | +| POST | `/api/dataset-manage/upload/{dataset_id}` | 上传文件,字段名 `files` | +| GET | `/api/dataset-manage/download/{dataset_id}` | 打包下载数据集 | +| GET | `/api/dataset-manage/download/{dataset_id}/{file_id}` | 下载单文件 | + +创建数据集: + +```json +{ + "name": "金融问答-训练集", + "type": "train", + "storage_type": "local", + "source": "upload", + "description": "金融领域问答", + "task_id": null +} +``` + +数据集类型: + +- `type`: `train`、`test`、`eval`、`val`、`other`。 +- `storage_type`: `local`、`minio`、`cloud`。 +- `source`: `upload`、`task`。 + +### 5.2 文件预览与版本 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/dataset-manage/preview/{file_id}` | 当前版本内容预览 | +| GET | `/api/dataset-manage/versions/{file_id}` | 文件版本列表 | +| GET | `/api/dataset-manage/versions/{file_id}/{version_id}` | 读取历史版本 | +| POST | `/api/dataset-manage/versions/{file_id}` | 保存为新版本 | +| PUT | `/api/dataset-manage/versions/{file_id}/active` | 切换当前版本 | +| DELETE | `/api/dataset-manage/versions/{file_id}/{version_id}` | 删除非当前、非初始版本 | + +创建新版本: + +```json +{ + "content": "{\"instruction\":\"...\"}\n", + "description": "在线编辑", + "base_version_id": "uuid", + "expected_current_version_id": "uuid" +} +``` + +说明:`expected_current_version_id` 用于乐观锁,防止多人编辑覆盖。 + +## 6. 数据处理 + +当前数据处理页面为本地模拟,正式后端建议实现以下接口。 + +### 6.1 任务列表与详情 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/data-process` | 数据处理任务列表 | +| POST | `/api/data-process` | 创建草稿任务 | +| GET | `/api/data-process/{id}` | 任务详情 | +| PUT | `/api/data-process/{id}` | 更新任务配置 | +| DELETE | `/api/data-process/{id}` | 删除任务 | +| POST | `/api/data-process/{id}/start` | 启动处理 | +| POST | `/api/data-process/{id}/stop` | 停止处理 | +| GET | `/api/data-process/{id}/progress` | 查询进度 | +| GET | `/api/data-process/{id}/events` | SSE 实时进度 | + +创建任务: + +```json +{ + "name": "客服问答数据清洗", + "description": "清洗并生成 SFT 数据", + "process_type": "structured", + "config": { + "preprocess_options": ["clean_invalid", "detect_structure", "deduplicate"], + "dataset_split": { "train": 80, "validation": 10, "test": 10 }, + "generation_model_id": "uuid", + "generation_prompt": "请生成训练数据", + "temperature": 0.7, + "max_tokens": 1024, + "json_mode": false, + "quality_filter_enabled": true + } +} +``` + +### 6.2 源文件与外部数据源 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/api/data-process/{id}/source-files` | 上传源文件,字段名 `files` | +| DELETE | `/api/data-process/{id}/source-files/{file_id}` | 移除源文件 | +| POST | `/api/data-process/{id}/external/test` | 测试外部数据源连接 | +| POST | `/api/data-process/{id}/external/pull` | 拉取外部数据并生成源文件 | + +外部数据源请求: + +```json +{ + "type": "mysql", + "url": "mysql://host:3306/db", + "auth_mode": "password", + "username": "user", + "password": "secret", + "token": null, + "limit": 1000 +} +``` + +安全要求:连接密码/token 不落明文,任务详情只回显脱敏配置。 + +### 6.3 预览切片 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/api/data-process/{id}/preview/build` | 根据源文件和配置生成预览切片 | +| GET | `/api/data-process/{id}/preview` | 查询预览切片 | +| PUT | `/api/data-process/{id}/preview/{preview_id}` | 编辑切片内容 | +| POST | `/api/data-process/{id}/preview` | 手动新增切片 | +| DELETE | `/api/data-process/{id}/preview/{preview_id}` | 删除切片 | + +预览切片字段: + +```json +{ + "id": "uuid", + "source_file_id": "uuid", + "original_content": "...", + "edited_content": "...", + "source_start": 0, + "source_end": 100, + "source_start_line": 1, + "source_end_line": 5, + "token_count": 50, + "status": "original" +} +``` + +### 6.4 生成结果与发布数据集 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/api/data-process/{id}/generate` | 启动 LLM 生成 | +| GET | `/api/data-process/{id}/results` | 查询结果明细 | +| PUT | `/api/data-process/{id}/results/{result_id}` | 编辑结果 | +| POST | `/api/data-process/{id}/results/{result_id}/restore` | 恢复原始结果 | +| POST | `/api/data-process/{id}/publish` | 发布为数据集 | + +结果字段: + +```json +{ + "instruction": "生成简洁客服回复", + "input": "用户反馈页面加载慢", + "output": "已收到反馈,我们正在排查。", + "status": "valid" +} +``` + +发布请求: + +```json +{ + "dataset_name": "客服问答清洗集", + "dataset_type": "train", + "storage_type": "local", + "split": { "train": 80, "validation": 10, "test": 10 }, + "format": "alpaca_jsonl" +} +``` + +## 7. 模型微调 + +### 7.1 训练任务 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/fine-tune` | 训练任务列表 | +| GET | `/api/fine-tune/{id}` | 训练任务详情 | +| GET | `/api/fine-tune/check-name?name=xxx` | 任务名查重 | +| POST | `/api/fine-tune` | 创建训练任务记录 | +| POST | `/api/fine-tune/start` | 启动训练 | +| PUT | `/api/fine-tune/{id}` | 更新任务 | +| POST | `/api/fine-tune/stop/{id}` | 停止任务 | +| DELETE | `/api/fine-tune/{id}` | 删除任务 | +| GET | `/api/fine-tune/progress/{id}` | 获取训练进度 | +| GET | `/api/fine-tune/{id}/events` | SSE 训练日志/进度 | +| POST | `/api/fine-tune/tensorboard/start` | 启动 TensorBoard | + +启动训练请求兼容前端 `FineTuneStartPayload`: + +```json +{ + "task_id": "uuid", + "name": "finance-sft-001", + "description": "金融 SFT", + "train_type": "SFT", + "train_method": "lora", + "template": "qwen", + "base_model": "uuid", + "train_dataset_id": "uuid", + "auto_merge": true, + "output_model_name": "qwen-finance-sft-v1", + "gpus": [0, 1], + "batch_size": 1, + "learning_rate": 0.0001, + "n_epochs": 1, + "save_steps": 100, + "lr_scheduler_type": "cosine", + "max_length": 512, + "warmup_ratio": 0.05, + "weight_decay": 0.01, + "lora_alpha": 16, + "lora_dropout": 0.1, + "lora_rank": 8, + "quantization_bit": 4, + "export_quantized": false, + "quant_method": "", + "quant_bits": 0, + "quant_group_size": 0, + "export_format": "" +} +``` + +### 7.2 训练日志详情页 + +训练日志页还会联合调用: + +- `GET /api/fine-tune/{id}` 获取任务参数。 +- `GET /api/dataset-manage/{id}` 获取训练集信息。 +- `GET /api/system-info` 获取 GPU 状态。 +- `GET /api/training-log-files`、`GET /api/training-log-content` 获取日志。 + +建议新增: + +`GET /api/fine-tune/{id}/overview` + +一次返回任务、数据集、GPU、日志摘要、指标曲线,减少页面聚合复杂度。 + +## 8. 模型评测 + +### 8.1 评测任务 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/model-eval` | 评测任务列表 | +| GET | `/api/model-eval/{id}` | 评测详情 | +| POST | `/api/model-eval/start` | 启动评测 | +| DELETE | `/api/model-eval/{id}` | 删除评测 | +| GET | `/api/model-eval/{id}/events` | SSE 评测进度 | + +启动评测: + +```json +{ + "eval_task_name": "金融模型评测-v1", + "eval_type": "custom", + "model_id": "uuid", + "gpu_id": 0, + "dataset_id": "uuid", + "dimension_id": "uuid", + "data_source": "dataset", + "leaderboard": true, + "basic_metrics": { + "bleu": { "enabled": true, "ngram": 4 }, + "rouge": { "enabled": true, "methods": ["rouge-1", "rouge-l"] }, + "cosine": { "enabled": false }, + "output_precision": 3 + } +} +``` + +详情响应应包含: + +- 综合分数、最大分、总体评价、改进建议。 +- 维度汇总。 +- 样本级输入、参考答案、模型输出、评分、原因、错误类型。 + +### 8.2 评测维度 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/dimension` | 维度列表 | +| GET | `/api/dimension/{id}` | 维度详情 | +| POST | `/api/dimension` | 创建维度 | +| PUT | `/api/dimension/{id}` | 编辑维度 | +| DELETE | `/api/dimension/{id}` | 删除维度 | + +维度请求: + +```json +{ + "name": "回答准确性", + "type": "classification", + "description": "评估回答是否准确", + "eval_model": "uuid", + "eval_method": "standard", + "eval_prompt": "你是专业评测专家...", + "is_active": true, + "bleu_n": null, + "output_precision": 3, + "score_min": 0, + "score_max": 100, + "pass_threshold": 70 +} +``` + +## 9. 模型推理与对比 + +前端将“推理”和“模型对比”共用 `model-compare` 接口。 + +### 9.1 推理/对比任务 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/model-compare` | 推理/对比任务列表 | +| GET | `/api/model-compare/{id}` | 任务详情 | +| POST | `/api/model-compare` | 创建任务 | +| DELETE | `/api/model-compare/{id}` | 删除任务 | +| POST | `/api/model-compare/{id}/load` | 加载任务内模型 | +| POST | `/api/model-compare/{id}/unload` | 卸载任务内模型 | +| GET | `/api/model-compare/{id}/load-status` | 查询加载状态 | +| POST | `/api/model-compare/{id}/load-status` | 更新加载状态 | +| POST | `/api/model-compare/{id}/start-model` | 启动单个模型服务 | +| POST | `/api/model-compare/stop-by-pid` | 按 PID 停止模型 | +| POST | `/api/model-compare/all/stop-all` | 停止全部旧模型服务 | + +创建任务: + +```json +{ + "name": "金融模型对比", + "description": "对比基座与微调模型", + "models": [ + { + "model_id": "uuid", + "model_name": "Qwen2.5-7B-Instruct", + "model_path": "/data/models/qwen2.5-7b", + "gpu_id": 0, + "source": "database" + } + ] +} +``` + +### 9.2 对话接口 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/api/model-compare/stream-chat` | 流式对话,建议 SSE/chunked | +| POST | `/api/model-compare/chat-with-port` | 指定端口非流式对话 | +| POST | `/api/model-chat/batch` | API 模型批量对话 | +| POST | `/api/model-chat/local/chat` | 本地模型对话 | +| POST | `/api/model-chat/local/preload` | 预加载本地模型 | +| POST | `/api/model-chat/trained/preload` | 预加载已训练模型 | + +流式请求: + +```json +{ + "task_id": "uuid", + "model_id": "uuid", + "messages": [ + { "role": "user", "content": "解释什么是 ROE" } + ], + "temperature": 0.7, + "max_tokens": 1024, + "stream": true +} +``` + +建议落库会话与消息,便于对比结果页查看历史。 + +## 10. 数据转换与工具中心 + +### 10.1 数据转换 + +当前 JSON 转 JSONL 页面是 UI 原型,建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/api/data-convert/jobs` | 创建转换任务,multipart 上传源文件 | +| GET | `/api/data-convert/jobs/{id}` | 转换任务详情 | +| GET | `/api/data-convert/jobs/{id}/download` | 下载转换结果 | +| DELETE | `/api/data-convert/jobs/{id}` | 删除转换任务 | + +请求字段: + +- `source_file`: `.json` 文件。 +- `output_name`: 输出文件名。 +- `encoding`: 默认 `UTF-8`。 +- `convert_type`: `json_to_jsonl`。 + +### 10.2 自定义工具 + +当前自定义工具仅 localStorage,若要多用户共享,建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/tools` | 工具列表 | +| POST | `/api/tools` | 创建工具 | +| GET | `/api/tools/{id}` | 工具详情 | +| PUT | `/api/tools/{id}` | 编辑工具 | +| DELETE | `/api/tools/{id}` | 删除工具 | + +字段:`name`、`description`、`url`、`icon`、`visibility`、`owner_id`。 + +## 11. 后端开发需要补齐的关键点 + +1. 前端路由已有 `user-settings`、`user-create`、`user-permission`、`permission-denied`,但当前仓库缺少对应 Vue 文件;后端仍应先实现用户中心和权限接口。 +2. 数据处理主流程目前全在浏览器本地模拟,后端需要正式实现上传、切片、LLM 生成、结果编辑、发布数据集。 +3. 训练、评测、数据处理、模型加载都不应同步阻塞 HTTP;建议接 Celery/RQ/Arq 或 FastAPI BackgroundTasks + 独立 worker。 +4. 文件内容不要全部入库;数据库保存元数据、版本、校验和、对象存储路径,内容放本地 NAS/MinIO。 +5. API Key、外部数据源密码必须加密存储,接口只回显脱敏。 +6. 日志和监控数据增长快,需要分区或保留策略。 +7. 建议实现 OpenAPI schema,并用 Pydantic enum 与数据库 enum 对齐。 + +## 12. 仍需确认的问题 + +1. 部署形态:单机多 GPU、K8s、多训练节点,还是只在一台服务器上调度? +2. 文件存储:使用本地磁盘、NAS、MinIO,还是对象存储?是否需要断点续传? +3. 训练框架:是否固定使用 LLaMA-Factory?是否还要支持 Transformers 原生、DeepSpeed、Accelerate? +4. 权限粒度:页面级权限是否足够,还是需要到数据集/模型/任务的所有者与项目空间级权限? +5. 多租户/项目空间:是否需要组织、项目、团队隔离? +6. 审批流程:模型发布、数据集删除、停止训练等危险操作是否需要审批? +7. 评测方式:只支持规则指标 + LLM Judge,还是需要人工标注/复核闭环? +8. 推理服务:是否需要长驻服务、自动端口管理、并发限流、会话历史长期保存? +9. 合规安全:数据脱敏、敏感词检测、审计留存周期、模型 API Key 管理是否有公司规范? +10. 数据库规模预期:数据集样本量、日志保留周期、监控采样频率,会影响分区和索引策略。 + +## 13. 企业治理与算力平台补充接口 + +根据 `system-development-plan.md`,以下能力已从待确认项升级为第一版设计范围:单机多 GPU、本地磁盘、应用平台/算力平台分离、多租户、项目级资源隔离、审批流、审计留存、LLaMA-Factory 引擎插件化。原有业务接口需要统一增加 `tenant_id`、`project_id` 上下文,列表接口默认只返回当前用户可访问项目内资源。 + +### 13.1 租户管理 + +| 方法 | 路径 | 说明 | 权限 | +| --- | --- | --- | --- | +| GET | `/api/tenants` | 租户列表 | 平台管理员 | +| POST | `/api/tenants` | 创建租户 | 平台管理员 | +| GET | `/api/tenants/{id}` | 租户详情 | 租户管理员 | +| PUT | `/api/tenants/{id}` | 更新租户 | 平台管理员 | +| PUT | `/api/tenants/{id}/quota` | 设置租户配额 | 平台管理员 | +| PUT | `/api/tenants/{id}/retention-policy` | 设置租户留存策略 | 平台管理员 | + +创建租户: + +```json +{ + "name": "研发一部", + "code": "rd-1", + "status": "active", + "quota": { + "gpu_concurrency": 4, + "storage_bytes": 10995116277760, + "max_projects": 20 + }, + "retention_policy": { + "audit_days": 180, + "training_log_days": 90, + "metric_days": 30, + "temp_file_days": 1 + } +} +``` + +### 13.2 项目空间 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/projects` | 当前用户可访问项目列表 | +| POST | `/api/projects` | 创建项目 | +| GET | `/api/projects/{id}` | 项目详情 | +| PUT | `/api/projects/{id}` | 更新项目 | +| POST | `/api/projects/{id}/archive` | 归档项目 | +| GET | `/api/projects/{id}/members` | 项目成员 | +| POST | `/api/projects/{id}/members` | 添加成员 | +| PUT | `/api/projects/{id}/members/{user_id}` | 修改项目角色 | +| DELETE | `/api/projects/{id}/members/{user_id}` | 移除成员 | + +创建项目: + +```json +{ + "tenant_id": "uuid", + "name": "金融模型微调", + "code": "finance-ft", + "description": "金融问答模型训练与评测", + "quota": { + "gpu_concurrency": 2, + "storage_bytes": 2199023255552, + "max_running_jobs": 3 + } +} +``` + +项目角色建议:`owner`、`maintainer`、`developer`、`reviewer`、`viewer`。 + +### 13.3 资源级授权 + +模型、数据集、训练任务、评测任务、推理任务、数据处理任务都必须支持资源级 ACL。 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/resources/{resource_type}/{resource_id}/acl` | 查询资源授权 | +| PUT | `/api/resources/{resource_type}/{resource_id}/acl` | 覆盖资源授权 | +| POST | `/api/resources/{resource_type}/{resource_id}/share` | 快速分享给用户/项目角色 | + +授权请求: + +```json +{ + "entries": [ + { + "subject_type": "user", + "subject_id": "uuid", + "permissions": ["read", "execute", "download"] + }, + { + "subject_type": "project_role", + "subject_id": "developer", + "permissions": ["read", "write", "execute"] + } + ] +} +``` + +权限码:`read`、`write`、`execute`、`download`、`delete`、`manage_acl`。 + +### 13.4 审批中心 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/approvals` | 审批列表,支持 `type=pending/mine/done` | +| POST | `/api/approvals` | 发起审批 | +| GET | `/api/approvals/{id}` | 审批详情 | +| POST | `/api/approvals/{id}/approve` | 通过 | +| POST | `/api/approvals/{id}/reject` | 驳回 | +| POST | `/api/approvals/{id}/cancel` | 撤回 | +| GET | `/api/approval-templates` | 审批模板列表 | +| PUT | `/api/approval-templates/{id}` | 更新审批模板 | + +发起审批: + +```json +{ + "action": "model_publish", + "resource_type": "trained_model", + "resource_id": "uuid", + "project_id": "uuid", + "reason": "发布金融问答模型测试服务", + "payload": { + "service_level": "production", + "gpu_id": 0, + "max_concurrency": 8 + } +} +``` + +第一版建议触发审批的动作:删除数据集、删除模型、生产服务发布、导出模型、下载敏感数据集、提高 GPU 配额、停止他人任务。 + +### 13.5 算力资源与队列 + +应用平台对前端暴露 `/api/compute/*`,实际由 Compute Gateway 调用算力平台内部接口。 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/compute/nodes` | 算力节点列表 | +| GET | `/api/compute/gpus` | GPU 状态 | +| GET | `/api/compute/queue` | 任务队列 | +| GET | `/api/compute/jobs/{id}` | 算力任务详情 | +| POST | `/api/compute/jobs/{id}/retry` | 重试任务 | +| POST | `/api/compute/jobs/{id}/priority` | 调整优先级 | +| POST | `/api/internal/compute-callbacks/jobs` | 算力平台任务回调 | + +GPU 响应字段: + +```json +{ + "node_id": "uuid", + "gpu_index": 0, + "uuid": "GPU-xxx", + "name": "NVIDIA A800", + "status": "running", + "memory_total_mb": 81920, + "memory_used_mb": 40960, + "utilization_percent": 72, + "temperature": 61, + "current_job_id": "uuid", + "current_project_id": "uuid" +} +``` + +### 13.6 算力平台内部接口 + +以下接口只允许应用平台调用,不直接暴露给浏览器。 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/compute/jobs` | 创建训练/评测/数据处理/推理任务 | +| GET | `/compute/jobs/{id}` | 查询任务 | +| POST | `/compute/jobs/{id}/stop` | 停止任务 | +| GET | `/compute/jobs/{id}/logs` | 拉取日志 | +| GET | `/compute/resources/gpus` | 查询 GPU | +| POST | `/compute/files/upload` | 上传到算力本地磁盘 | +| GET | `/compute/files/{id}/download` | 下载文件 | + +创建算力任务: + +```json +{ + "tenant_id": "uuid", + "project_id": "uuid", + "job_type": "fine_tune", + "engine": "llama_factory", + "priority": "normal", + "resource_request": { + "gpu_count": 1, + "gpu_ids": [0], + "memory_gb": 64 + }, + "workspace": { + "root": "/data/ft-platform/tenants/{tenant_id}/projects/{project_id}/jobs/{job_id}" + }, + "payload": { + "model_path": "/data/ft-platform/.../models/base/qwen", + "dataset_path": "/data/ft-platform/.../datasets/train.jsonl", + "training_args": {} + }, + "callback_url": "http://app/api/internal/compute-callbacks/jobs" +} +``` + +### 13.7 文件网关与离线导入 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/api/files/upload-session` | 创建分片上传会话 | +| PUT | `/api/files/upload-session/{id}/parts/{part_no}` | 上传分片 | +| POST | `/api/files/upload-session/{id}/complete` | 完成上传 | +| GET | `/api/files/{id}/preview` | 文件预览 | +| GET | `/api/files/{id}/download-url` | 获取短时下载链接 | +| POST | `/api/import/local-model` | 从算力节点本地路径导入模型 | +| POST | `/api/import/local-dataset` | 从算力节点本地路径导入数据集 | + +离线导入模型: + +```json +{ + "tenant_id": "uuid", + "project_id": "uuid", + "compute_node_id": "uuid", + "path": "/data/models/qwen2.5-7b", + "name": "Qwen2.5-7B-Instruct", + "purpose": "training" +} +``` + +### 13.8 训练引擎管理 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/training-engines` | 训练引擎列表 | +| GET | `/api/training-engines/{id}` | 引擎详情 | +| GET | `/api/training-engines/{id}/schema` | 参数 schema | +| POST | `/api/training-engines/{id}/health-check` | 健康检查 | + +LLaMA-Factory 引擎声明: + +```json +{ + "code": "llama_factory", + "name": "LLaMA-Factory", + "version": "0.9.x", + "supported_task_types": ["SFT", "DPO", "CPT"], + "supported_methods": ["lora", "qlora", "full"], + "supported_formats": ["alpaca", "sharegpt", "dpo_pair", "pretrain_text"], + "schema": {} +} +``` + +### 13.9 Checkpoint 与恢复训练 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/fine-tune/{id}/checkpoints` | checkpoint 列表 | +| POST | `/api/fine-tune/{id}/retry` | 失败任务重试 | +| POST | `/api/fine-tune/{id}/resume` | 从 checkpoint 恢复训练 | +| DELETE | `/api/fine-tune/{id}/checkpoints/{checkpoint_id}` | 删除 checkpoint,可能触发审批 | +| PUT | `/api/fine-tune/{id}/checkpoint-retention` | 设置 checkpoint 保留策略 | + +默认保留策略:最近 3 个、最优 2 个、已发布模型关联 checkpoint 不自动删除、失败任务保留 14 天。 + +### 13.10 审计、留存、配额和成本统计 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/audit-logs` | 操作审计 | +| GET | `/api/login-logs` | 登录审计 | +| GET | `/api/download-logs` | 下载审计 | +| GET | `/api/retention-policies` | 留存策略 | +| PUT | `/api/retention-policies/{id}` | 更新留存策略 | +| GET | `/api/quotas/usage` | 配额使用 | +| GET | `/api/usage/summary` | GPU 小时、磁盘、推理调用统计 | + +第一版只做用量统计,不做账单计费;第二期可扩展成本核算。 + +## 14. 接口与页面功能模块映射 + +本节用于接口开发和前后端联调。后端开发人员可按页面模块确认接口覆盖范围;前端开发人员可按页面查找需要调用的 API。 + +### 14.1 认证、用户和权限 + +| 页面模块 | 路由/入口 | 接口 | 说明 | +| --- | --- | --- | --- | +| 登录页 | `/login` | `POST /api/login`、`GET /api/me`、`POST /api/logout` | 登录、恢复用户、退出 | +| 用户中心 | `/user-settings` | `GET /api/users` | 用户列表、搜索、状态筛选 | +| 创建用户 | `/user-settings/create` | `POST /api/users` | 创建本地用户 | +| 用户权限 | `/user-settings/:id/permission` | `PUT /api/users/{id}`、`PUT /api/users/{id}/password` | 用户角色、状态、页面权限、重置密码 | +| 无权限页 | `/permission-denied` | 无专属接口,可调用 `GET /api/me` | 展示当前用户权限和返回入口 | + +### 14.2 租户、项目和资源授权 + +| 页面模块 | 路由/入口 | 接口 | 说明 | +| --- | --- | --- | --- | +| 租户管理 | `/tenants` | `GET /api/tenants`、`POST /api/tenants` | 租户列表、创建租户 | +| 租户详情 | `/tenants/:id` | `GET /api/tenants/{id}`、`PUT /api/tenants/{id}`、`PUT /api/tenants/{id}/quota`、`PUT /api/tenants/{id}/retention-policy` | 租户配置、配额、留存 | +| 项目列表 | `/projects` | `GET /api/projects`、`POST /api/projects` | 项目列表、创建项目 | +| 项目详情 | `/projects/:id` | `GET /api/projects/{id}`、`PUT /api/projects/{id}`、`POST /api/projects/{id}/archive` | 项目概览、归档 | +| 项目成员 | `/projects/:id/members` | `GET /api/projects/{id}/members`、`POST /api/projects/{id}/members`、`PUT /api/projects/{id}/members/{user_id}`、`DELETE /api/projects/{id}/members/{user_id}` | 成员和项目角色 | +| 资源授权 | 资源详情弹窗或 `/projects/:id/permissions` | `GET /api/resources/{resource_type}/{resource_id}/acl`、`PUT /api/resources/{resource_type}/{resource_id}/acl`、`POST /api/resources/{resource_type}/{resource_id}/share` | 模型/数据集/任务级 ACL | + +### 14.3 看板、监控和日志 + +| 页面模块 | 路由/入口 | 接口 | 说明 | +| --- | --- | --- | --- | +| 服务看板 | `/dashboard` | `GET /api/dashboard/overview`、`GET /api/health` | 首页聚合、轻量健康指标 | +| 平台性能 | `/hardware` | `GET /api/system-info`、`GET /api/compute/gpus` | CPU、内存、磁盘、GPU、任务占用 | +| 系统日志 | `/logs` | `GET /api/log-files`、`GET /api/log-content` | 系统日志列表和内容 | +| 训练日志页 | `/training-log/:id` | `GET /api/fine-tune/{id}/overview`、`GET /api/training-log-files`、`GET /api/training-log-content` | 训练日志、指标、GPU 状态 | + +### 14.4 模型管理 + +| 页面模块 | 路由/入口 | 接口 | 说明 | +| --- | --- | --- | --- | +| 模型列表 | `/model-manage` | `GET /api/model-manage`、`DELETE /api/model-manage/{id}`、`PUT /api/model-manage/{id}/purpose` | 模型列表、删除审批入口、用途变更 | +| 模型创建/编辑 | `/model-manage/create`、`/model-manage/:id/edit` | `GET /api/model-manage/{id}`、`POST /api/model-manage`、`PUT /api/model-manage/{id}`、`GET /api/model-manage/local-models` | 本地/API 模型登记 | +| 离线导入模型 | 模型创建页或导入弹窗 | `POST /api/import/local-model` | 从算力节点本地路径导入 | +| 已训练模型 | 模型列表/选择弹窗 | `GET /api/model-manage/trained-models`、`DELETE /api/model-manage/trained-models/{id}` | 训练产物列表和删除 | +| 权重合并 | `/model-manage/merge` | `POST /api/model-manage/merge` | LoRA 合并任务 | +| 模型导出 | 模型列表/详情 | `GET /api/model-manage/trained-models/{model_name}/export` | 导出下载,必要时触发审批 | + +### 14.5 数据集与数据处理 + +| 页面模块 | 路由/入口 | 接口 | 说明 | +| --- | --- | --- | --- | +| 数据集列表 | `/dataset` | `GET /api/dataset-manage`、`DELETE /api/dataset-manage/{id}`、`GET /api/dataset-manage/download/{id}` | 数据集列表、删除、打包下载 | +| 数据集创建/编辑 | `/dataset/create`、`/dataset/:id/edit` | `POST /api/dataset-manage`、`PUT /api/dataset-manage/{id}`、`POST /api/dataset-manage/upload/{dataset_id}` | 数据集元数据和文件上传 | +| 离线导入数据集 | 数据集创建页或导入弹窗 | `POST /api/import/local-dataset` | 从算力节点目录导入 | +| 数据集预览 | `/dataset/:id/preview` | `GET /api/dataset-manage/preview/{file_id}`、`GET /api/dataset-manage/versions/{file_id}`、`POST /api/dataset-manage/versions/{file_id}`、`PUT /api/dataset-manage/versions/{file_id}/active`、`DELETE /api/dataset-manage/versions/{file_id}/{version_id}` | 文件预览、版本、在线编辑 | +| 数据处理列表 | `/data-process` | `GET /api/data-process`、`DELETE /api/data-process/{id}` | 处理任务列表 | +| 数据处理创建向导 | `/data-process/create` | `POST /api/data-process`、`POST /api/data-process/{id}/source-files`、`POST /api/data-process/{id}/preview/build`、`POST /api/data-process/{id}/generate`、`POST /api/data-process/{id}/publish` | 创建、上传、预览、生成、发布 | +| 数据处理详情 | `/data-process/:id` | `GET /api/data-process/{id}`、`GET /api/data-process/{id}/results`、`GET /api/data-process/{id}/progress`、`GET /api/data-process/{id}/events` | 详情、结果、进度 | +| 数据转换 | `/data-convert` | `POST /api/data-convert/jobs`、`GET /api/data-convert/jobs/{id}`、`GET /api/data-convert/jobs/{id}/download` | JSON/JSONL 转换 | + +### 14.6 微调训练 + +| 页面模块 | 路由/入口 | 接口 | 说明 | +| --- | --- | --- | --- | +| 微调列表 | `/fine-tune` | `GET /api/fine-tune`、`POST /api/fine-tune/stop/{id}`、`DELETE /api/fine-tune/{id}` | 训练任务列表、停止、删除 | +| 微调创建 | `/fine-tune/create` | `GET /api/fine-tune/check-name`、`POST /api/fine-tune`、`POST /api/fine-tune/start`、`GET /api/model-manage`、`GET /api/dataset-manage`、`GET /api/compute/gpus` | 参数配置、GPU 选择、启动训练 | +| 训练详情/日志 | `/training-log/:id` | `GET /api/fine-tune/{id}`、`GET /api/fine-tune/progress/{id}`、`GET /api/fine-tune/{id}/events`、`GET /api/fine-tune/{id}/checkpoints` | 日志、进度、checkpoint | +| 恢复/重试训练 | 训练详情页 | `POST /api/fine-tune/{id}/retry`、`POST /api/fine-tune/{id}/resume` | 从 checkpoint 重试或恢复 | +| Checkpoint 管理 | 训练详情页、存储管理页 | `DELETE /api/fine-tune/{id}/checkpoints/{checkpoint_id}`、`PUT /api/fine-tune/{id}/checkpoint-retention` | 清理策略和删除 | +| TensorBoard | 训练详情页 | `POST /api/fine-tune/tensorboard/start` | 启动 TensorBoard | + +### 14.7 评测、推理、对比和服务发布 + +| 页面模块 | 路由/入口 | 接口 | 说明 | +| --- | --- | --- | --- | +| 评测列表 | `/model-eval` | `GET /api/model-eval`、`DELETE /api/model-eval/{id}` | 评测任务列表 | +| 评测创建 | `/model-eval/create` | `POST /api/model-eval/start`、`GET /api/dimension`、`GET /api/model-manage/trained-models`、`GET /api/dataset-manage`、`GET /api/compute/gpus` | 选择模型、数据集、维度、GPU | +| 评测详情 | `/model-eval/:id` | `GET /api/model-eval/{id}`、`GET /api/model-eval/{id}/events` | 综合结果、样本评分 | +| 评测维度 | `/model-eval/dimension/:id/edit` | `GET /api/dimension/{id}`、`POST /api/dimension`、`PUT /api/dimension/{id}`、`DELETE /api/dimension/{id}` | 维度和 Prompt 管理 | +| 推理列表 | `/model-inference` | `GET /api/model-compare`、`POST /api/model-compare/{id}/load`、`POST /api/model-compare/{id}/unload` | 推理任务和加载状态 | +| 推理创建 | `/model-inference/create` | `POST /api/model-compare`、`GET /api/model-manage`、`GET /api/model-manage/trained-models`、`GET /api/compute/gpus` | 选择模型和 GPU | +| 推理对话 | `/model-inference/chat/:id` | `GET /api/model-compare/{id}`、`POST /api/model-compare/stream-chat`、`POST /api/model-compare/chat-with-port` | 单模型对话 | +| 模型对比 | `/model-compare/chat/:id`、`/model-compare/result` | `POST /api/model-chat/batch`、`POST /api/model-chat/local/chat`、`POST /api/model-chat/local/preload`、`POST /api/model-chat/trained/preload` | 多模型对比和预加载 | +| 模型服务治理 | `/model-services`、`/model-services/:id` | `POST /api/approvals`、`GET /api/compute/jobs/{id}`、`GET /api/usage/summary` | 测试/生产服务发布、调用统计、下线审批 | + +### 14.8 审批、审计、算力和存储运维 + +| 页面模块 | 路由/入口 | 接口 | 说明 | +| --- | --- | --- | --- | +| 审批中心 | `/approvals`、`/approvals/pending`、`/approvals/mine`、`/approvals/:id` | `GET /api/approvals`、`POST /api/approvals`、`GET /api/approvals/{id}`、`POST /api/approvals/{id}/approve`、`POST /api/approvals/{id}/reject`、`POST /api/approvals/{id}/cancel` | 审批列表和审批动作 | +| 审批设置 | `/approval-settings` | `GET /api/approval-templates`、`PUT /api/approval-templates/{id}` | 审批模板 | +| 算力资源 | `/compute`、`/compute/gpus`、`/compute/queue`、`/compute/nodes` | `GET /api/compute/nodes`、`GET /api/compute/gpus`、`GET /api/compute/queue`、`POST /api/compute/jobs/{id}/retry`、`POST /api/compute/jobs/{id}/priority` | GPU、节点、队列 | +| 存储管理 | `/storage` | `GET /api/quotas/usage`、`GET /api/files/{id}/download-url`、`GET /api/retention-policies`、`PUT /api/retention-policies/{id}` | 磁盘占用、下载、留存 | +| 审计中心 | `/audit-logs`、`/login-logs`、`/download-logs` | `GET /api/audit-logs`、`GET /api/login-logs`、`GET /api/download-logs` | 操作、登录、下载审计 | +| 训练引擎管理 | `/training-engines` | `GET /api/training-engines`、`GET /api/training-engines/{id}`、`GET /api/training-engines/{id}/schema`、`POST /api/training-engines/{id}/health-check` | 引擎能力和健康 | diff --git a/docs/platform-architecture-requirements.md b/docs/platform-architecture-requirements.md new file mode 100644 index 0000000..b2591b2 --- /dev/null +++ b/docs/platform-architecture-requirements.md @@ -0,0 +1,744 @@ +# 模型训练平台架构与功能需求设计 + +> 本文基于当前前端页面、已有接口/数据库设计,以及用户补充的 6 条约束进行补全。目标是把平台从“单前端原型 + 后端接口草案”扩展为企业可落地的模型训练平台方案。 + +## 1. 补充需求结论 + +### 1.1 已确认约束 + +1. 部署形态:单机多 GPU。 +2. 文件存储:本地磁盘。 +3. 训练框架:当前固定 LLaMA-Factory,但要预留其他训练平台接入标准。 +4. 权限粒度:需要到项目、模型、数据集级隔离。 +5. 企业治理:需要多租户、审批流、审计留存周期。 +6. 部署边界:算力平台与应用平台分开部署。 + +### 1.2 对整体设计的影响 + +- 不能只做页面级 RBAC,需要引入“租户 -> 项目 -> 资源 -> 成员/角色/ACL”的资源权限模型。 +- 单机多 GPU 不等于简单指定 GPU ID,需要有 GPU 资源池、锁定、排队、抢占策略和异常释放机制。 +- 本地磁盘存储与应用/算力分离存在天然冲突:文件不能只存应用服务器本地,也不能让应用直接读算力服务器目录。建议将训练文件、模型文件、日志文件统一落在算力节点本地磁盘,由算力平台提供文件网关 API;应用平台只保存元数据和访问路径。 +- LLaMA-Factory 应作为第一个训练引擎插件,而不是写死到业务流程里。后续接入其他平台时只需实现同一套训练引擎协议。 +- 审批流、审计和保留策略必须从第一版进入数据模型和接口,否则后期补会牵动大量资源表。 + +## 2. 总体架构设计 + +### 2.1 逻辑分层 + +```mermaid +flowchart LR + U["用户浏览器"] --> APP["应用平台 Web / FastAPI"] + APP --> DB["PostgreSQL"] + APP --> REDIS["Redis / 任务队列"] + APP --> CPAPI["算力平台 API"] + CPAPI --> AGENT["算力节点 Agent"] + AGENT --> GPU["单机多 GPU"] + AGENT --> DISK["本地磁盘工作区"] + AGENT --> LF["LLaMA-Factory 引擎"] +``` + +### 2.2 应用平台职责 + +应用平台负责业务编排和企业治理,不直接执行训练命令: + +- 用户、租户、项目、权限、审批、审计。 +- 模型、数据集、任务、评测、推理任务元数据。 +- 前端 API、OpenAPI、统一认证、统一响应。 +- 训练/评测/数据处理任务创建、状态查询、审批校验。 +- 与算力平台通信,下发任务、查询进度、拉取日志、下载产物。 + +建议部署组件: + +- Nginx:静态前端与反向代理。 +- FastAPI:业务 API。 +- PostgreSQL:业务元数据。 +- Redis:缓存、任务队列、SSE 状态缓存、分布式锁。 +- Worker:审批通知、审计归档、周期清理、异步导出。 + +### 2.3 算力平台职责 + +算力平台负责“实际占用 GPU 和磁盘”的事情: + +- GPU 发现、资源上报、锁定、释放。 +- 本地磁盘工作区管理。 +- 数据集文件接收、校验、解压、版本目录管理。 +- LLaMA-Factory 命令生成、执行、停止、日志采集。 +- 训练产物、LoRA adapter、merged model、checkpoint 管理。 +- 推理服务进程管理、端口分配、健康检查。 +- 系统/GPU 监控指标采集。 + +建议部署组件: + +- Compute API:只暴露给应用平台访问。 +- Compute Agent:本机服务,具备启动/停止进程权限。 +- 本地文件网关:上传、下载、预览、断点续传、文件校验。 +- Engine Adapter:LLaMA-Factory 适配器,后续扩展其他训练引擎。 + +### 2.4 应用与算力平台通信 + +建议统一使用内部 HTTP/gRPC API,并配置服务间认证: + +- 应用平台调用算力平台必须携带 `X-Service-Token` 或 mTLS 证书。 +- 算力平台不信任前端用户身份,只信任应用平台下发的租户、项目、任务和资源上下文。 +- 所有任务回调必须带签名,避免伪造状态。 + +核心通信接口: + +| 方向 | 接口 | 说明 | +| --- | --- | --- | +| 应用 -> 算力 | `POST /compute/jobs` | 创建训练/评测/数据处理/推理任务 | +| 应用 -> 算力 | `POST /compute/jobs/{id}/stop` | 停止任务 | +| 应用 -> 算力 | `GET /compute/jobs/{id}` | 查询任务状态 | +| 应用 -> 算力 | `GET /compute/jobs/{id}/logs` | 拉取日志 | +| 应用 -> 算力 | `GET /compute/resources/gpus` | 查询 GPU 状态 | +| 应用 -> 算力 | `POST /compute/files/upload` | 上传文件到算力本地磁盘 | +| 应用 -> 算力 | `GET /compute/files/{object_id}/download` | 下载文件 | +| 算力 -> 应用 | `POST /api/internal/compute-callbacks/jobs` | 回调任务状态、指标、产物 | + +## 3. 本地磁盘存储设计 + +### 3.1 存储根目录 + +由于算力和应用分离,建议文件主存储放在算力节点本地磁盘: + +```text +/data/ft-platform/ + tenants/{tenant_id}/ + projects/{project_id}/ + datasets/{dataset_id}/ + models/base/{model_id}/ + models/trained/{trained_model_id}/ + jobs/{job_id}/ + input/ + output/ + logs/ + checkpoints/ + tmp/ +``` + +应用平台数据库保存: + +- `storage_type=local`。 +- `storage_node_id`。 +- `relative_path`。 +- `checksum_sha256`。 +- `byte_size`。 +- `tenant_id/project_id/resource_id`。 + +### 3.2 文件访问原则 + +- 前端不直接访问磁盘路径。 +- 应用平台生成短时下载凭证,转发或重定向到算力文件网关。 +- 预览内容只读取前 N 行或指定区间,避免大文件撑爆 API。 +- 大文件上传必须支持分片上传、校验、断点续传。 +- 删除操作采用软删除 + 延迟清理,等待审计留存和审批结果。 + +### 3.3 磁盘配额 + +配额应分三层: + +- 租户配额:总容量、模型容量、数据容量、日志容量。 +- 项目配额:可用容量、最大文件大小、最大任务产物保留数。 +- 用户配额:可上传文件总量、并发任务产物占用。 + +超过配额时: + +- 禁止新建任务或上传文件。 +- 允许下载和清理。 +- 提示可清理的 checkpoint、过期日志、失败任务临时目录。 + +## 4. 单机多 GPU 资源调度 + +### 4.1 GPU 资源模型 + +每张 GPU 需要记录: + +- `gpu_index`、`uuid`、型号、显存、驱动版本。 +- 当前利用率、显存占用、温度、功耗。 +- 当前锁定任务、进程 PID、端口。 +- 状态:`idle`、`reserved`、`running`、`draining`、`offline`、`error`。 + +### 4.2 调度策略 + +第一版建议支持三种模式: + +- 手动指定 GPU:兼容当前前端选择 GPU 的模式。 +- 自动选择 GPU:按空闲显存、温度、任务队列选择。 +- 项目配额调度:项目最多可占用 N 张 GPU,避免单项目占满机器。 + +训练任务启动流程: + +1. 应用平台校验权限、配额、审批状态。 +2. 生成任务,状态为 `pending`。 +3. 算力平台尝试锁定 GPU。 +4. 锁定成功后创建工作区并启动 LLaMA-Factory。 +5. Agent 持续上报进度、日志、指标。 +6. 任务完成后释放 GPU,登记模型产物。 + +### 4.3 并发与排队 + +- 同一 GPU 同时只允许一个训练任务。 +- 推理服务可与训练互斥,默认不允许混跑;如后续允许,需要显存保留策略。 +- 数据处理如果只调用 API 模型,可以不占 GPU;如果调用本地模型生成,需要占用 GPU。 +- 支持任务队列优先级:`low`、`normal`、`high`、`urgent`。 +- 高优任务是否可抢占低优任务,需要审批或管理员权限。 + +## 5. 训练引擎接入标准 + +### 5.1 引擎抽象 + +LLaMA-Factory 是第一实现,但业务系统只依赖统一训练引擎接口: + +```text +TrainingEngine + validate_config(config) + prepare_workspace(job_context) + build_command(job_context) + start(job_context) + stop(job_id) + parse_progress(log_line) + collect_artifacts(job_id) + export_model(job_id, export_config) +``` + +### 5.2 引擎注册信息 + +每个训练引擎需要声明: + +- 引擎编码:`llama_factory`。 +- 支持任务:`SFT`、`DPO`、`CPT`。 +- 支持方法:`lora`、`qlora`、`full`。 +- 支持模型模板:`qwen`、`llama3` 等。 +- 支持数据格式:Alpaca、ShareGPT、DPO pair、pretrain text。 +- 支持量化和导出格式。 +- 参数 schema。 +- 命令模板或启动方式。 + +### 5.3 LLaMA-Factory 适配要求 + +LLaMA-Factory 适配器负责: + +- 将平台训练参数转换为 YAML/CLI 参数。 +- 根据项目工作区生成数据集配置。 +- 自动设置 `CUDA_VISIBLE_DEVICES`。 +- 解析训练日志中的 loss、epoch、learning_rate、ETA。 +- 收集 checkpoint、adapter、merged model、training_args、trainer_state。 +- 支持训练停止和失败恢复。 + +### 5.4 后续接入其他平台的标准 + +其他训练平台只要实现以下契约即可接入: + +- 输入:模型引用、数据集引用、训练配置、资源需求、输出目录。 +- 输出:任务状态、进度、日志、指标、产物清单、失败原因。 +- 生命周期:`prepare`、`start`、`running`、`stop`、`complete`、`cleanup`。 +- 安全:不能越权访问其他租户/项目目录。 +- 可观测:必须输出结构化事件和日志。 + +## 6. 多租户与项目级隔离 + +### 6.1 租户模型 + +需要新增租户管理: + +- 租户名称、编码、状态。 +- 租户管理员。 +- 租户资源配额:GPU 并发数、磁盘容量、最大项目数。 +- 租户审计策略、保留周期、审批策略。 + +### 6.2 项目空间 + +所有业务资源必须归属项目: + +- 数据集。 +- 模型。 +- 训练任务。 +- 评测任务。 +- 推理任务。 +- 数据处理任务。 +- 自定义工具。 + +项目字段: + +- 项目名称、描述、所属租户。 +- 项目管理员、成员。 +- 默认资源权限。 +- GPU/磁盘/任务并发配额。 +- 项目状态:启用、归档、禁用。 + +### 6.3 资源级权限 + +建议采用 RBAC + ACL 混合: + +- RBAC 决定用户是否能访问模块,例如能否进入模型管理。 +- 项目角色决定用户是否能管理项目内资源。 +- 资源 ACL 处理特殊授权,例如某个数据集只给指定成员可读。 + +项目角色建议: + +| 角色 | 权限 | +| --- | --- | +| Project Owner | 项目设置、成员、资源、审批策略全权限 | +| Project Maintainer | 创建/编辑模型、数据集、任务,可发起发布和删除 | +| Developer | 创建训练、评测、推理、数据处理任务 | +| Reviewer | 审批、复核、查看评测结果 | +| Viewer | 只读查看 | + +资源权限建议: + +- `read`:查看资源。 +- `write`:编辑元数据和内容。 +- `execute`:用于训练/评测/推理。 +- `download`:下载文件和模型。 +- `delete`:删除或申请删除。 +- `manage_acl`:管理资源授权。 + +### 6.4 数据隔离要求 + +- API 查询必须默认带 `tenant_id` 和用户可访问项目范围。 +- 数据库所有核心资源表增加 `tenant_id`、`project_id`。 +- 本地磁盘路径包含租户和项目 ID,防止路径混用。 +- 算力任务上下文必须携带租户/项目,Agent 只允许访问对应工作区。 +- 日志、审计、下载链接也必须按租户隔离。 + +## 7. 企业治理设计 + +### 7.1 审批流 + +建议第一版支持可配置审批模板: + +| 场景 | 是否建议审批 | 原因 | +| --- | --- | --- | +| 删除数据集 | 是 | 数据不可逆风险高 | +| 删除模型 | 是 | 影响训练/推理依赖 | +| 模型发布为可推理服务 | 是 | 影响生产资源 | +| 停止他人训练任务 | 是或管理员直通 | 影响计算成本和他人工作 | +| 导出模型 | 可配置 | 涉及资产外流 | +| 下载敏感数据集 | 可配置 | 涉及数据安全 | +| 提高 GPU 配额 | 是 | 涉及资源竞争 | + +审批流能力: + +- 发起审批。 +- 指定审批人/审批组。 +- 多级审批。 +- 通过、驳回、撤回、转交。 +- 审批超时提醒。 +- 审批结果回写原业务动作。 + +### 7.2 审计留存周期 + +建议支持租户级配置: + +- 操作审计:默认 180 天,可配置 90/180/365/永久。 +- 登录审计:默认 180 天。 +- 训练日志:默认 90 天。 +- 系统监控:原始采样默认 30 天,聚合指标保留 1 年。 +- 模型产物:默认长期保留,删除需审批。 +- 临时文件:默认 7 天清理。 +- 失败任务工作区:默认 14 天清理。 + +审计不可被普通管理员物理删除,只能由系统归档任务按策略处理。 + +### 7.3 安全策略 + +需要补充: + +- API Key 加密存储和脱敏展示。 +- 外部数据源密码加密存储或不落库。 +- 下载链接短时有效。 +- 敏感操作二次确认。 +- 审批通过后动作有效期,例如 24 小时内执行。 +- IP 白名单和服务间 token。 +- 操作审计记录 before/after 数据。 + +## 8. 需要补全的页面和功能 + +### 8.1 租户与项目页面 + +当前前端缺失,建议新增: + +1. 租户管理页 + - 租户列表、创建、禁用、配额设置、审计策略。 + - 仅平台管理员可见。 + +2. 项目空间页 + - 项目列表、创建项目、归档项目。 + - 展示项目资源概览:模型数、数据集数、任务数、磁盘占用、GPU 使用。 + +3. 项目成员页 + - 添加/移除成员。 + - 设置项目角色。 + - 查看成员最近操作。 + +4. 项目资源权限页 + - 模型/数据集/任务级授权。 + - 支持按用户、用户组、项目角色授权。 + +### 8.2 用户中心补全 + +前端路由已有但页面文件缺失或未完成: + +- `PermissionDeniedView.vue` +- `UserSettingsView.vue` +- `UserCreateView.vue` +- `UserPermissionView.vue` + +建议功能: + +- 用户列表、创建、禁用、重置密码。 +- 角色管理。 +- 页面权限管理。 +- 用户所属租户/项目。 +- 用户可用 GPU/磁盘配额查看。 +- 登录记录和操作审计入口。 + +### 8.3 审批中心 + +新增页面: + +- 我的申请。 +- 待我审批。 +- 已办审批。 +- 审批详情。 +- 审批模板配置。 + +审批详情需要展示: + +- 申请人、申请时间、动作类型、目标资源。 +- 变更前后信息。 +- 风险提示。 +- 审批记录。 +- 通过/驳回意见。 + +### 8.4 算力资源中心 + +当前只有硬件监控页,建议扩展为算力资源中心: + +- GPU 拓扑和状态。 +- GPU 当前任务占用。 +- GPU 锁定/释放记录。 +- 队列中的任务。 +- 资源配额:租户/项目/用户维度。 +- 算力节点 Agent 状态。 +- 训练引擎健康状态。 + +### 8.5 文件与存储管理 + +新增页面: + +- 存储总览。 +- 租户/项目磁盘占用。 +- 大文件列表。 +- 临时文件清理。 +- Checkpoint 管理。 +- 日志保留策略。 +- 文件下载审计。 + +### 8.6 训练引擎管理 + +新增页面: + +- 引擎列表。 +- LLaMA-Factory 版本和路径。 +- 引擎能力声明。 +- 参数 schema 管理。 +- 引擎健康检查。 +- 引擎接入文档。 + +### 8.7 任务队列与运行控制 + +新增页面: + +- 全局任务队列。 +- 项目任务队列。 +- 任务优先级调整。 +- 任务重试。 +- 任务停止审批。 +- 失败任务诊断。 + +### 8.8 模型发布与服务治理 + +当前推理/对比页面已有基础能力,但缺少企业化发布能力: + +- 模型发布申请。 +- 推理服务实例配置。 +- 端口、GPU、并发、超时、最大上下文限制。 +- 服务启停记录。 +- 调用统计。 +- 服务下线审批。 + +## 9. 后端模块补全 + +### 9.1 新增核心模块 + +| 模块 | 职责 | +| --- | --- | +| tenant | 租户管理、租户配额、租户策略 | +| project | 项目空间、成员、项目角色 | +| resource_acl | 模型/数据集/任务级资源授权 | +| approval | 审批模板、审批实例、审批动作 | +| quota | GPU、磁盘、任务并发配额 | +| compute_gateway | 应用平台与算力平台通信 | +| file_gateway | 本地磁盘文件上传、下载、预览 | +| engine_registry | 训练引擎注册与能力发现 | +| retention | 审计、日志、临时文件保留策略 | + +### 9.2 数据模型补充 + +在前一版 SQL 基础上,应新增或调整: + +- `tenants` +- `tenant_users` +- `projects` +- `project_members` +- `resource_acl` +- `approval_templates` +- `approval_instances` +- `approval_steps` +- `quotas` +- `quota_usage` +- `compute_nodes` +- `gpu_devices` +- `gpu_allocations` +- `compute_jobs` +- `training_engines` +- `retention_policies` + +核心资源表需要补充字段: + +- `tenant_id` +- `project_id` +- `visibility` +- `owner_id` +- `approval_status` +- `storage_node_id` + +需要调整的已有表: + +- `models` +- `trained_models` +- `datasets` +- `dataset_files` +- `data_process_tasks` +- `fine_tune_tasks` +- `eval_tasks` +- `inference_tasks` +- `custom_tools` +- `audit_logs` +- `storage_objects` + +### 9.3 接口补充 + +租户: + +- `GET /api/tenants` +- `POST /api/tenants` +- `GET /api/tenants/{id}` +- `PUT /api/tenants/{id}` +- `PUT /api/tenants/{id}/quota` +- `PUT /api/tenants/{id}/retention-policy` + +项目: + +- `GET /api/projects` +- `POST /api/projects` +- `GET /api/projects/{id}` +- `PUT /api/projects/{id}` +- `POST /api/projects/{id}/archive` +- `GET /api/projects/{id}/members` +- `POST /api/projects/{id}/members` +- `PUT /api/projects/{id}/members/{user_id}` +- `DELETE /api/projects/{id}/members/{user_id}` + +资源授权: + +- `GET /api/resources/{resource_type}/{resource_id}/acl` +- `PUT /api/resources/{resource_type}/{resource_id}/acl` +- `POST /api/resources/{resource_type}/{resource_id}/share` + +审批: + +- `GET /api/approvals` +- `POST /api/approvals` +- `GET /api/approvals/{id}` +- `POST /api/approvals/{id}/approve` +- `POST /api/approvals/{id}/reject` +- `POST /api/approvals/{id}/cancel` + +算力: + +- `GET /api/compute/nodes` +- `GET /api/compute/gpus` +- `GET /api/compute/queue` +- `POST /api/compute/jobs/{id}/retry` +- `POST /api/compute/jobs/{id}/priority` + +训练引擎: + +- `GET /api/training-engines` +- `GET /api/training-engines/{id}` +- `POST /api/training-engines/{id}/health-check` +- `GET /api/training-engines/{id}/schema` + +## 10. 端到端业务流程 + +### 10.1 数据集上传 + +1. 用户进入项目空间。 +2. 用户创建数据集。 +3. 应用平台校验项目写权限和磁盘配额。 +4. 前端上传文件到应用平台。 +5. 应用平台转发到算力文件网关,保存到项目目录。 +6. 算力平台返回文件元数据和 checksum。 +7. 应用平台登记数据集文件版本。 +8. 审计记录上传行为。 + +### 10.2 微调训练 + +1. 用户选择项目、模型、数据集、训练参数和 GPU。 +2. 应用平台检查模型/数据集 `execute` 权限。 +3. 检查项目 GPU 并发配额。 +4. 如果策略要求审批,先创建审批单。 +5. 审批通过后创建 compute job。 +6. 算力平台锁定 GPU,生成 LLaMA-Factory 配置,启动训练。 +7. 训练日志和指标实时回传。 +8. 完成后登记 trained model。 +9. 如启用自动合并,进入合并任务。 +10. 审计记录任务全生命周期。 + +### 10.3 模型发布 + +1. 用户选择训练产物。 +2. 提交发布申请。 +3. 审批通过后算力平台启动推理服务。 +4. 分配端口和 GPU。 +5. 应用平台登记服务实例。 +6. 前端推理页面调用服务。 +7. 监控调用量、延迟、错误率、GPU 占用。 + +## 11. 当前功能完整性评估 + +补充 6 条需求后,平台设计已经覆盖完整模型训练平台的主链路: + +- 数据准备。 +- 数据处理。 +- 模型登记。 +- 微调训练。 +- 训练日志和指标。 +- 模型合并和导出。 +- 模型评测。 +- 推理服务。 +- 模型对比。 +- 用户、权限、租户、项目隔离。 +- 审批、审计、保留策略。 +- 算力资源调度。 + +但如果目标是企业级生产平台,还建议继续确认以下缺口。 + +## 12. 仍需确认的问题 + +1. 本地磁盘是否在算力服务器上,应用服务器是否完全不保存训练文件?如果应用服务器也要保存上传临时文件,需要确认临时文件保留周期和容量。 +2. 单机多 GPU 是否需要支持 MIG、GPU 分片或多进程共享,还是一张 GPU 同一时间只给一个任务? +3. 是否允许训练任务抢占?高优先级任务是否能停止低优先级任务? +4. 是否需要离线导入已有模型和已有数据集目录,还是所有文件都必须从平台上传? +5. 模型发布是否区分“测试服务”和“生产服务”?生产发布是否必须审批? +6. 是否需要数据集脱敏、敏感字段识别和数据质量评分作为内置流程? +7. 是否需要人工评测/人工复核结果沉淀为新数据集? +8. 是否需要训练任务失败后的断点续训? +9. 是否需要 checkpoint 自动清理策略,例如只保留最近 N 个或最好 N 个? +10. 是否需要对外提供标准 API 给其他系统调用训练、评测、推理能力? +11. 是否需要接入企业统一身份认证,例如 LDAP、OIDC、企业微信、钉钉? +12. 是否需要成本核算:按租户/项目统计 GPU 小时、磁盘占用、模型调用量? + +## 13. 推荐决策补充 + +`system-development-plan.md` 已对上述问题给出第一版建议,需求设计以以下决策为准。 + +1. 本地磁盘主存储放在算力服务器,应用服务器只保留上传临时文件。临时文件默认保留 24 小时,成功转发到算力文件网关后可立即进入清理队列。 +2. 第一版不支持 MIG、GPU 分片和多任务共享同一张 GPU。一张 GPU 同一时间只分配给一个训练任务或一个推理服务。GPU 数据模型预留 `partition_type`、`parent_gpu_uuid`、`memory_total_mb`,便于后续扩展 MIG。 +3. 第一版不做自动抢占。支持任务优先级和排队;停止他人任务需要审批或平台管理员权限。 +4. 第一版必须支持离线导入已有模型和数据集目录。导入由算力 Agent 扫描、校验、登记,并归属指定租户和项目。 +5. 模型发布区分测试服务和生产服务。测试服务项目内可启动并默认限流;生产服务必须审批。 +6. 数据脱敏和数据质量评分作为数据处理模块的一等能力进入第一期,先实现规则版脱敏、格式校验、重复率、完整性、长度分布等指标。 +7. 人工评测/复核作为第二期功能,但第一期需在数据库和页面入口预留人工复核状态与修订字段。 +8. 第一版支持从 checkpoint 手动恢复训练,不做自动失败续训。失败任务可选择 checkpoint 重试。 +9. 第一版必须支持 checkpoint 自动清理策略:默认保留最近 3 个、最优 2 个;已发布模型关联 checkpoint 不自动删除;失败任务 checkpoint 默认保留 14 天。 +10. 第一版提供内部 API,第二期再开放面向其他系统的标准 API、API Key、限流和 Webhook。 +11. 第一版使用本地账号,预留 OIDC/LDAP 字段和认证 provider 抽象;第二期接入企业统一身份认证。 +12. 第一版做 GPU 小时、磁盘占用、任务时长、推理调用量等用量统计;第二期再做成本单价和账单核算。 + +以上决策需要同步反映在接口文档、数据库 SQL、前端页面和部署方案中。第一版实现不再阻塞于这些问题的反复确认,除非实际部署环境与假设明显冲突。 + +## 14. 页面功能模块映射 + +本节用于帮助前端、后端、DB 和测试人员理解需求对应到哪些页面与功能模块。页面路径以当前 Vue 路由和新增规划路由为准。 + +### 14.1 平台入口与全局能力 + +| 页面模块 | 路由/入口 | 对应需求 | 主要功能 | +| --- | --- | --- | --- | +| 登录页 | `/login` | 用户认证、本地账号、后续预留 OIDC/LDAP | 登录、会话创建、权限加载 | +| 主布局 | `/` | 全局项目上下文、权限控制 | 菜单、顶部状态、项目切换器、用户信息 | +| 无权限页 | `/permission-denied` | 页面权限和资源权限兜底 | 展示无权限原因、返回可访问页面 | +| 服务看板 | `/dashboard` | 平台运行总览 | 服务健康、任务概览、训练统计、用户操作分布 | + +### 14.2 租户、项目与权限治理 + +| 页面模块 | 建议路由 | 对应需求 | 主要功能 | +| --- | --- | --- | --- | +| 租户管理 | `/tenants`、`/tenants/:id` | 多租户、租户配额、留存策略 | 租户列表、创建/禁用租户、配额、审计留存策略 | +| 项目空间 | `/projects`、`/projects/:id` | 项目级隔离、项目资源聚合 | 项目列表、项目概览、资源统计、项目归档 | +| 项目成员 | `/projects/:id/members` | 项目角色 | 添加成员、移除成员、设置 owner/maintainer/developer/reviewer/viewer | +| 资源授权 | `/projects/:id/permissions` 或资源详情弹窗 | 模型/数据集/任务级 ACL | 按用户、项目角色授权 read/write/execute/download/delete/manage_acl | +| 用户中心 | `/user-settings`、`/user-settings/create`、`/user-settings/:id/permission` | 用户、角色、页面权限 | 用户列表、创建用户、禁用、重置密码、分配页面权限和项目 | + +### 14.3 数据链路 + +| 页面模块 | 路由/入口 | 对应需求 | 主要功能 | +| --- | --- | --- | --- | +| 数据集列表 | `/dataset` | 数据集管理、项目隔离 | 列表、搜索、下载、删除审批入口 | +| 数据集创建/编辑 | `/dataset/create`、`/dataset/:id/edit` | 上传、本地磁盘、文件网关 | 创建数据集、上传文件、离线导入、元数据编辑 | +| 数据集预览 | `/dataset/:id/preview` | 文件版本、在线编辑、乐观锁 | 文件预览、版本切换、保存新版本、下载 | +| 数据处理列表 | `/data-process` | 数据处理任务管理 | 任务列表、状态、输出数据集跳转、删除审批 | +| 数据处理创建向导 | `/data-process/create` | 清洗、切片、生成、质量评分、脱敏 | 任务配置、模型选择、源文件/外部源、预览切片、生成、结果编辑、发布数据集 | +| 数据处理详情 | `/data-process/:id` | 处理统计和结果追踪 | 运行信息、处理统计、失败原因、结果明细 | +| 数据转换 | `/data-convert` | JSON/JSONL 转换 | 上传 JSON、转换任务、下载结果 | + +### 14.4 模型训练链路 + +| 页面模块 | 路由/入口 | 对应需求 | 主要功能 | +| --- | --- | --- | --- | +| 模型管理 | `/model-manage` | 模型登记、离线导入、资源 ACL | 基座模型/API 模型列表、用途变更、授权、删除审批 | +| 模型创建/编辑 | `/model-manage/create`、`/model-manage/:id/edit` | 本地模型/API 模型登记 | 选择本地路径、填写 API 模型信息、加密 API Key | +| 权重合并 | `/model-manage/merge` | LoRA 合并、产物管理 | 选择训练产物、合并权重、生成 merged model | +| 微调任务列表 | `/fine-tune` | 训练任务管理 | 任务列表、状态、进度、停止/删除审批 | +| 微调创建 | `/fine-tune/create` | LLaMA-Factory 训练配置、GPU 调度 | 选择模型/数据集/GPU、训练参数、量化导出、提交审批或启动 | +| 训练日志详情 | `/training-log/:id` | 日志、指标、checkpoint、恢复训练 | 日志 tail、loss 曲线、GPU 状态、checkpoint 列表、恢复/重试 | +| 训练引擎管理 | `/training-engines` | LLaMA-Factory 插件化和后续引擎接入 | 引擎列表、能力声明、schema、健康检查 | + +### 14.5 评测、推理和发布 + +| 页面模块 | 路由/入口 | 对应需求 | 主要功能 | +| --- | --- | --- | --- | +| 评测列表 | `/model-eval` | 模型评测 | 评测任务列表、状态、分数、删除 | +| 评测创建 | `/model-eval/create` | 自动评测、LLM Judge、基础指标 | 选择模型/数据集/维度/GPU、配置指标、启动评测 | +| 评测详情 | `/model-eval/:id` | 样本级结果、人工复核预留 | 综合评价、维度汇总、样本评分、错误类型 | +| 评测维度 | `/model-eval/dimension/:id/edit` | 维度和评测 Prompt 管理 | 创建/编辑维度、评分范围、Prompt、启用状态 | +| 推理列表 | `/model-inference` | 测试推理服务 | 推理任务列表、加载/卸载、服务状态 | +| 推理创建 | `/model-inference/create` | 模型服务资源申请 | 选择模型、GPU、端口策略、并发参数 | +| 推理对话 | `/model-inference/chat/:id` | 模型对话 | 单模型流式对话、会话记录 | +| 模型对比 | `/model-compare/chat/:id`、`/model-compare/result` | 多模型对比 | 多模型加载、并行对话、对比结果 | +| 模型发布治理 | `/model-services`、`/model-services/:id` | 测试/生产服务、发布审批 | 测试服务启动、生产发布申请、调用统计、下线审批 | + +### 14.6 算力、审批、审计和运维 + +| 页面模块 | 建议路由 | 对应需求 | 主要功能 | +| --- | --- | --- | --- | +| 算力资源中心 | `/compute`、`/compute/gpus` | 单机多 GPU、资源锁定、队列 | GPU 卡片、任务占用、节点状态、队列、优先级 | +| 存储管理 | `/storage` | 本地磁盘、配额、清理 | 租户/项目占用、大文件、临时文件、checkpoint 清理 | +| 审批中心 | `/approvals`、`/approvals/pending`、`/approvals/mine`、`/approvals/:id` | 审批流 | 我的申请、待我审批、审批详情、通过/驳回/撤回 | +| 审批模板 | `/approval-settings` | 审批策略配置 | 按动作配置审批人、超时、风险级别 | +| 审计中心 | `/audit-logs`、`/login-logs`、`/download-logs` | 操作审计和留存 | 操作审计、登录审计、下载审计、筛选导出 | +| 平台性能 | `/hardware` | 系统监控 | CPU、内存、磁盘、GPU、进程 | +| 系统日志 | `/logs` | 日志查看 | 系统日志、训练日志、tail/offset 查询 | diff --git a/docs/postgres-schema.sql b/docs/postgres-schema.sql new file mode 100644 index 0000000..b9e602b --- /dev/null +++ b/docs/postgres-schema.sql @@ -0,0 +1,1386 @@ +-- PostgreSQL schema for the model fine-tuning platform. +-- Recommended PostgreSQL version: 14+. + +CREATE EXTENSION IF NOT EXISTS pgcrypto; +CREATE EXTENSION IF NOT EXISTS citext; + +CREATE SCHEMA IF NOT EXISTS ft_platform; +SET search_path TO ft_platform, public; + +-- ========================= +-- Common helpers +-- ========================= + +CREATE OR REPLACE FUNCTION ft_platform.set_updated_at() +RETURNS trigger AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION ft_platform.touch_updated_at(table_name regclass) +RETURNS void AS $$ +BEGIN + EXECUTE format('DROP TRIGGER IF EXISTS trg_set_updated_at ON %s', table_name); + EXECUTE format( + 'CREATE TRIGGER trg_set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE FUNCTION ft_platform.set_updated_at()', + table_name + ); +END; +$$ LANGUAGE plpgsql; + +-- ========================= +-- Enums +-- ========================= + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN + CREATE TYPE user_role AS ENUM ('admin', 'operator', 'viewer'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_status') THEN + CREATE TYPE user_status AS ENUM ('active', 'disabled'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'task_status') THEN + CREATE TYPE task_status AS ENUM ('pending', 'running', 'completed', 'failed', 'stopped'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'model_type') THEN + CREATE TYPE model_type AS ENUM ('LLM', 'CV', 'NLP', 'Embedding', 'Other'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'model_purpose') THEN + CREATE TYPE model_purpose AS ENUM ('training', 'inference', 'evaluation'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'model_source') THEN + CREATE TYPE model_source AS ENUM ('local', 'api'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'dataset_type') THEN + CREATE TYPE dataset_type AS ENUM ('train', 'test', 'eval', 'val', 'other'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'dataset_storage') THEN + CREATE TYPE dataset_storage AS ENUM ('local', 'minio', 'cloud'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'dataset_source') THEN + CREATE TYPE dataset_source AS ENUM ('upload', 'task'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'train_type') THEN + CREATE TYPE train_type AS ENUM ('SFT', 'DPO', 'CPT'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'train_method') THEN + CREATE TYPE train_method AS ENUM ('lora', 'qlora', 'full', 'prefix', 'adapter', 'peft', 'adalora', 'longlora'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'process_type') THEN + CREATE TYPE process_type AS ENUM ('structured', 'unstructured', 'external'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'eval_type') THEN + CREATE TYPE eval_type AS ENUM ('custom', 'baseline'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'dimension_type') THEN + CREATE TYPE dimension_type AS ENUM ('classification', 'metric', 'text_similarity'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'result_status') THEN + CREATE TYPE result_status AS ENUM ('valid', 'modified', 'invalid'); + END IF; +END $$; + +-- ========================= +-- User center and RBAC +-- ========================= + +CREATE TABLE IF NOT EXISTS users ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + username citext NOT NULL UNIQUE, + display_name varchar(100) NOT NULL, + password_hash text NOT NULL, + role user_role NOT NULL DEFAULT 'viewer', + status user_status NOT NULL DEFAULT 'active', + protected boolean NOT NULL DEFAULT false, + last_login_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('users'); + +CREATE TABLE IF NOT EXISTS permissions ( + code varchar(64) PRIMARY KEY, + name varchar(100) NOT NULL, + description text, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS role_permissions ( + role user_role NOT NULL, + permission_code varchar(64) NOT NULL REFERENCES permissions(code) ON DELETE CASCADE, + PRIMARY KEY (role, permission_code) +); + +CREATE TABLE IF NOT EXISTS user_permissions ( + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + permission_code varchar(64) NOT NULL REFERENCES permissions(code) ON DELETE CASCADE, + allowed boolean NOT NULL DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (user_id, permission_code) +); + +CREATE TABLE IF NOT EXISTS login_sessions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_jti uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(), + ip inet, + user_agent text, + expires_at timestamptz NOT NULL, + revoked_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_login_sessions_user_created ON login_sessions(user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_login_sessions_expires ON login_sessions(expires_at) WHERE revoked_at IS NULL; + +INSERT INTO permissions(code, name, description) VALUES + ('dashboard', '服务看板', '访问服务看板'), + ('fine-tune', '模型训练', '创建和管理微调任务'), + ('model-eval', '模型评测', '创建和管理评测任务'), + ('model-inference', '模型推理', '创建推理与模型对比任务'), + ('model-manage', '模型管理', '登记、编辑、删除模型'), + ('dataset', '数据集管理', '上传、编辑、下载数据集'), + ('data-process', '数据处理', '创建和管理数据处理任务'), + ('data-convert', '数据转换/工具', '使用数据转换和工具中心'), + ('hardware', '平台性能', '查看硬件和系统监控'), + ('logs', '查看日志', '查看系统与训练日志'), + ('user-settings', '用户设置', '管理用户和权限') +ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description; + +INSERT INTO role_permissions(role, permission_code) +SELECT 'admin'::user_role, code FROM permissions +ON CONFLICT DO NOTHING; + +INSERT INTO role_permissions(role, permission_code) VALUES + ('operator', 'dashboard'), + ('operator', 'fine-tune'), + ('operator', 'model-eval'), + ('operator', 'model-inference'), + ('operator', 'model-manage'), + ('operator', 'dataset'), + ('operator', 'data-process'), + ('operator', 'data-convert'), + ('operator', 'hardware'), + ('operator', 'logs'), + ('viewer', 'dashboard'), + ('viewer', 'model-eval'), + ('viewer', 'model-inference'), + ('viewer', 'dataset'), + ('viewer', 'hardware'), + ('viewer', 'logs') +ON CONFLICT DO NOTHING; + +-- Replace this password hash during deployment. +INSERT INTO users(username, display_name, password_hash, role, status, protected) +VALUES ('admin', '系统管理员', '$argon2id$replace-with-real-hash', 'admin', 'active', true) +ON CONFLICT (username) DO NOTHING; + +-- ========================= +-- Audit and operation logs +-- ========================= + +CREATE TABLE IF NOT EXISTS audit_logs ( + id bigserial PRIMARY KEY, + user_id uuid REFERENCES users(id) ON DELETE SET NULL, + username citext, + action varchar(100) NOT NULL, + resource_type varchar(80) NOT NULL, + resource_id text, + request_method varchar(12), + request_path text, + ip inet, + user_agent text, + success boolean NOT NULL DEFAULT true, + error_message text, + before_data jsonb, + after_data jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_audit_logs_user_created ON audit_logs(user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON audit_logs(resource_type, resource_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at DESC); + +CREATE TABLE IF NOT EXISTS web_logs ( + id bigserial PRIMARY KEY, + user_id uuid REFERENCES users(id) ON DELETE SET NULL, + level varchar(20) NOT NULL, + message text NOT NULL, + page text, + context jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_web_logs_created ON web_logs(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_web_logs_level_created ON web_logs(level, created_at DESC); + +-- ========================= +-- Files and object storage metadata +-- ========================= + +CREATE TABLE IF NOT EXISTS storage_objects ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + storage_type dataset_storage NOT NULL DEFAULT 'local', + bucket varchar(128), + object_key text NOT NULL, + original_name text, + mime_type varchar(200), + byte_size bigint NOT NULL DEFAULT 0 CHECK (byte_size >= 0), + checksum_sha256 char(64), + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE UNIQUE INDEX IF NOT EXISTS uq_storage_object_location + ON storage_objects(storage_type, COALESCE(bucket, ''), object_key); +CREATE INDEX IF NOT EXISTS idx_storage_objects_checksum ON storage_objects(checksum_sha256); + +-- ========================= +-- Model registry +-- ========================= + +CREATE TABLE IF NOT EXISTS models ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(150) NOT NULL, + type model_type NOT NULL DEFAULT 'LLM', + purpose model_purpose NOT NULL, + model_source model_source NOT NULL DEFAULT 'local', + description text, + path text, + api_url text, + api_key_encrypted text, + online_model_name varchar(200), + config jsonb NOT NULL DEFAULT '{}'::jsonb, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz, + CONSTRAINT ck_model_local_or_api CHECK ( + (model_source = 'local' AND path IS NOT NULL) + OR + (model_source = 'api' AND api_url IS NOT NULL AND online_model_name IS NOT NULL) + ) +); +SELECT touch_updated_at('models'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_models_name_alive ON models(name) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_models_purpose ON models(purpose) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_models_type_source ON models(type, model_source) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS trained_models ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(150) NOT NULL, + base_model_id uuid REFERENCES models(id) ON DELETE SET NULL, + fine_tune_task_id uuid, + train_method train_method, + adapter_path text, + merged boolean NOT NULL DEFAULT false, + merging boolean NOT NULL DEFAULT false, + merged_path text, + export_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL, + metrics jsonb NOT NULL DEFAULT '{}'::jsonb, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('trained_models'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_trained_models_name_alive ON trained_models(name) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_trained_models_task ON trained_models(fine_tune_task_id); +CREATE INDEX IF NOT EXISTS idx_trained_models_base ON trained_models(base_model_id); + +-- ========================= +-- Datasets and versions +-- ========================= + +CREATE TABLE IF NOT EXISTS datasets ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(150) NOT NULL, + type dataset_type NOT NULL, + storage_type dataset_storage NOT NULL DEFAULT 'local', + source dataset_source NOT NULL DEFAULT 'upload', + source_task_id uuid, + size_bytes bigint NOT NULL DEFAULT 0 CHECK (size_bytes >= 0), + record_count bigint NOT NULL DEFAULT 0 CHECK (record_count >= 0), + description text, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('datasets'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_datasets_name_alive ON datasets(name) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_datasets_type_created ON datasets(type, created_at DESC) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_datasets_source_task ON datasets(source_task_id) WHERE source = 'task'; + +CREATE TABLE IF NOT EXISTS dataset_files ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + dataset_id uuid NOT NULL REFERENCES datasets(id) ON DELETE CASCADE, + name text NOT NULL, + storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL, + current_version_id uuid, + size_bytes bigint NOT NULL DEFAULT 0 CHECK (size_bytes >= 0), + record_count bigint NOT NULL DEFAULT 0 CHECK (record_count >= 0), + file_format varchar(40), + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('dataset_files'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_dataset_files_name_alive + ON dataset_files(dataset_id, name) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_dataset_files_dataset ON dataset_files(dataset_id) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS dataset_file_versions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + dataset_file_id uuid NOT NULL REFERENCES dataset_files(id) ON DELETE CASCADE, + version_no integer NOT NULL CHECK (version_no > 0), + storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL, + content_preview text, + description text, + base_version_id uuid REFERENCES dataset_file_versions(id) ON DELETE SET NULL, + size_bytes bigint NOT NULL DEFAULT 0 CHECK (size_bytes >= 0), + record_count bigint NOT NULL DEFAULT 0 CHECK (record_count >= 0), + checksum_sha256 char(64), + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE UNIQUE INDEX IF NOT EXISTS uq_dataset_file_versions_no + ON dataset_file_versions(dataset_file_id, version_no); +CREATE INDEX IF NOT EXISTS idx_dataset_file_versions_file_created + ON dataset_file_versions(dataset_file_id, created_at DESC); + +ALTER TABLE dataset_files + DROP CONSTRAINT IF EXISTS fk_dataset_files_current_version; +ALTER TABLE dataset_files + ADD CONSTRAINT fk_dataset_files_current_version + FOREIGN KEY (current_version_id) REFERENCES dataset_file_versions(id) ON DELETE SET NULL; + +CREATE TABLE IF NOT EXISTS dataset_records ( + id bigserial PRIMARY KEY, + dataset_id uuid NOT NULL REFERENCES datasets(id) ON DELETE CASCADE, + dataset_file_id uuid REFERENCES dataset_files(id) ON DELETE CASCADE, + version_id uuid REFERENCES dataset_file_versions(id) ON DELETE CASCADE, + line_no integer, + split varchar(20), + instruction text, + input text, + output text, + raw jsonb NOT NULL DEFAULT '{}'::jsonb, + status result_status NOT NULL DEFAULT 'valid', + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_dataset_records_dataset_id ON dataset_records(dataset_id, id); +CREATE INDEX IF NOT EXISTS idx_dataset_records_file_version ON dataset_records(dataset_file_id, version_id, line_no); +CREATE INDEX IF NOT EXISTS idx_dataset_records_split ON dataset_records(dataset_id, split); +CREATE INDEX IF NOT EXISTS idx_dataset_records_raw_gin ON dataset_records USING gin(raw); + +-- ========================= +-- Data processing +-- ========================= + +CREATE TABLE IF NOT EXISTS data_process_tasks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(150) NOT NULL, + description text, + status task_status NOT NULL DEFAULT 'pending', + process_type process_type NOT NULL, + source_dataset_id uuid REFERENCES datasets(id) ON DELETE SET NULL, + output_dataset_id uuid REFERENCES datasets(id) ON DELETE SET NULL, + config jsonb NOT NULL DEFAULT '{}'::jsonb, + progress numeric(5,2) NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), + input_count bigint NOT NULL DEFAULT 0, + output_count bigint NOT NULL DEFAULT 0, + filtered_count bigint NOT NULL DEFAULT 0, + duplicate_count bigint NOT NULL DEFAULT 0, + error_count bigint NOT NULL DEFAULT 0, + failure_reason text, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + started_at timestamptz, + completed_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('data_process_tasks'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_data_process_tasks_name_alive + ON data_process_tasks(name) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_data_process_tasks_status_created + ON data_process_tasks(status, created_at DESC) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_data_process_tasks_creator_created + ON data_process_tasks(created_by, created_at DESC) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS data_process_source_files ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE, + storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL, + name text NOT NULL, + size_bytes bigint NOT NULL DEFAULT 0, + record_count bigint NOT NULL DEFAULT 0, + content_preview text, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_data_process_source_files_task ON data_process_source_files(task_id); + +CREATE TABLE IF NOT EXISTS data_process_preview_items ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE, + source_file_id uuid REFERENCES data_process_source_files(id) ON DELETE CASCADE, + original_content text NOT NULL DEFAULT '', + edited_content text NOT NULL DEFAULT '', + source_start integer, + source_end integer, + source_start_line integer, + source_end_line integer, + token_count integer NOT NULL DEFAULT 0, + status varchar(20) NOT NULL DEFAULT 'original', + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('data_process_preview_items'); +CREATE INDEX IF NOT EXISTS idx_data_process_preview_task_file + ON data_process_preview_items(task_id, source_file_id, created_at); + +CREATE TABLE IF NOT EXISTS data_process_results ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE, + preview_item_id uuid REFERENCES data_process_preview_items(id) ON DELETE SET NULL, + instruction text NOT NULL, + input text NOT NULL DEFAULT '', + output text NOT NULL, + original_instruction text, + original_input text, + original_output text, + status result_status NOT NULL DEFAULT 'valid', + error text, + split varchar(20), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('data_process_results'); +CREATE INDEX IF NOT EXISTS idx_data_process_results_task_status + ON data_process_results(task_id, status, id); +CREATE INDEX IF NOT EXISTS idx_data_process_results_task_split + ON data_process_results(task_id, split); + +-- ========================= +-- Fine-tune tasks +-- ========================= + +CREATE TABLE IF NOT EXISTS fine_tune_tasks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(150) NOT NULL, + description text, + status task_status NOT NULL DEFAULT 'pending', + train_type train_type NOT NULL, + train_method train_method NOT NULL DEFAULT 'lora', + template varchar(80) NOT NULL DEFAULT 'qwen', + base_model_id uuid NOT NULL REFERENCES models(id) ON DELETE RESTRICT, + train_dataset_id uuid REFERENCES datasets(id) ON DELETE SET NULL, + auto_merge boolean NOT NULL DEFAULT false, + output_model_name varchar(150), + gpus integer[] NOT NULL DEFAULT '{}', + params jsonb NOT NULL DEFAULT '{}'::jsonb, + progress numeric(5,2) NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), + process_id integer, + command text, + output_dir text, + log_file text, + train_duration_seconds integer, + failure_reason text, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + started_at timestamptz, + completed_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('fine_tune_tasks'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_fine_tune_tasks_name_alive + ON fine_tune_tasks(name) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_status_created + ON fine_tune_tasks(status, created_at DESC) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_dataset ON fine_tune_tasks(train_dataset_id); +CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_base_model ON fine_tune_tasks(base_model_id); + +ALTER TABLE trained_models + DROP CONSTRAINT IF EXISTS fk_trained_models_fine_tune_task; +ALTER TABLE trained_models + ADD CONSTRAINT fk_trained_models_fine_tune_task + FOREIGN KEY (fine_tune_task_id) REFERENCES fine_tune_tasks(id) ON DELETE SET NULL; + +CREATE TABLE IF NOT EXISTS fine_tune_metrics ( + id bigserial PRIMARY KEY, + task_id uuid NOT NULL REFERENCES fine_tune_tasks(id) ON DELETE CASCADE, + step integer, + epoch numeric(10,4), + loss numeric(18,8), + learning_rate numeric(18,12), + grad_norm numeric(18,8), + metrics jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_fine_tune_metrics_task_step ON fine_tune_metrics(task_id, step); +CREATE INDEX IF NOT EXISTS idx_fine_tune_metrics_task_created ON fine_tune_metrics(task_id, created_at); + +-- ========================= +-- Inference and compare +-- ========================= + +CREATE TABLE IF NOT EXISTS inference_tasks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(150) NOT NULL, + description text, + status task_status NOT NULL DEFAULT 'pending', + load_status jsonb NOT NULL DEFAULT '{"loaded_models":[]}'::jsonb, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('inference_tasks'); +CREATE INDEX IF NOT EXISTS idx_inference_tasks_status_created + ON inference_tasks(status, created_at DESC) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS inference_task_models ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES inference_tasks(id) ON DELETE CASCADE, + model_id uuid REFERENCES models(id) ON DELETE SET NULL, + trained_model_id uuid REFERENCES trained_models(id) ON DELETE SET NULL, + model_name varchar(150) NOT NULL, + model_path text, + gpu_id integer, + source varchar(40), + port integer, + pid integer, + status varchar(40) NOT NULL DEFAULT 'pending', + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('inference_task_models'); +CREATE INDEX IF NOT EXISTS idx_inference_task_models_task ON inference_task_models(task_id); +CREATE INDEX IF NOT EXISTS idx_inference_task_models_pid ON inference_task_models(pid) WHERE pid IS NOT NULL; +CREATE UNIQUE INDEX IF NOT EXISTS uq_inference_task_models_port_alive + ON inference_task_models(port) WHERE port IS NOT NULL AND status IN ('loading', 'ready'); + +CREATE TABLE IF NOT EXISTS chat_sessions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + inference_task_id uuid REFERENCES inference_tasks(id) ON DELETE SET NULL, + title varchar(200), + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('chat_sessions'); +CREATE INDEX IF NOT EXISTS idx_chat_sessions_task_created ON chat_sessions(inference_task_id, created_at DESC); + +CREATE TABLE IF NOT EXISTS chat_messages ( + id bigserial PRIMARY KEY, + session_id uuid NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE, + model_id uuid REFERENCES models(id) ON DELETE SET NULL, + trained_model_id uuid REFERENCES trained_models(id) ON DELETE SET NULL, + role varchar(20) NOT NULL, + content text NOT NULL, + latency_ms integer, + token_usage jsonb NOT NULL DEFAULT '{}'::jsonb, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_chat_messages_session_id ON chat_messages(session_id, id); + +-- ========================= +-- Evaluation +-- ========================= + +CREATE TABLE IF NOT EXISTS eval_dimensions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(150) NOT NULL, + type dimension_type NOT NULL, + description text, + eval_model_id uuid REFERENCES models(id) ON DELETE SET NULL, + eval_method jsonb NOT NULL DEFAULT '[]'::jsonb, + eval_prompt text, + is_active boolean NOT NULL DEFAULT true, + is_default boolean NOT NULL DEFAULT false, + bleu_n integer, + output_precision integer NOT NULL DEFAULT 3, + score_min numeric(12,4), + score_max numeric(12,4), + pass_threshold numeric(12,4), + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('eval_dimensions'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_eval_dimensions_name_alive + ON eval_dimensions(name) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_eval_dimensions_active ON eval_dimensions(is_active) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS eval_tasks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + eval_task_name varchar(150) NOT NULL, + eval_type eval_type NOT NULL DEFAULT 'custom', + model_id uuid REFERENCES models(id) ON DELETE SET NULL, + trained_model_id uuid REFERENCES trained_models(id) ON DELETE SET NULL, + dataset_id uuid REFERENCES datasets(id) ON DELETE SET NULL, + dimension_id uuid REFERENCES eval_dimensions(id) ON DELETE SET NULL, + gpu_id integer, + data_source varchar(40) NOT NULL DEFAULT 'dataset', + leaderboard boolean NOT NULL DEFAULT false, + basic_metrics jsonb NOT NULL DEFAULT '{}'::jsonb, + status task_status NOT NULL DEFAULT 'pending', + metric varchar(80), + score numeric(12,4), + overall_score numeric(12,4), + overall_score_max numeric(12,4), + overall_evaluation text, + improvement_suggestions jsonb NOT NULL DEFAULT '[]'::jsonb, + sample_count integer NOT NULL DEFAULT 0, + completed_count integer NOT NULL DEFAULT 0, + passed_count integer NOT NULL DEFAULT 0, + failure_reason text, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + started_at timestamptz, + completed_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('eval_tasks'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_eval_tasks_name_alive + ON eval_tasks(eval_task_name) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_eval_tasks_status_created ON eval_tasks(status, created_at DESC) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_eval_tasks_model_dataset ON eval_tasks(model_id, dataset_id); + +CREATE TABLE IF NOT EXISTS eval_dimension_summaries ( + id bigserial PRIMARY KEY, + eval_task_id uuid NOT NULL REFERENCES eval_tasks(id) ON DELETE CASCADE, + name varchar(150) NOT NULL, + score numeric(12,4), + max_score numeric(12,4), + pass_rate numeric(6,2), + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_eval_dimension_summaries_task ON eval_dimension_summaries(eval_task_id); + +CREATE TABLE IF NOT EXISTS eval_sample_results ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + eval_task_id uuid NOT NULL REFERENCES eval_tasks(id) ON DELETE CASCADE, + sample_index integer NOT NULL, + input text NOT NULL, + reference_answer text, + model_output text NOT NULL DEFAULT '', + score numeric(12,4), + max_score numeric(12,4), + passed boolean, + status task_status NOT NULL DEFAULT 'pending', + judgement varchar(40), + evaluation_reason text, + error_type varchar(80), + dimension_scores jsonb NOT NULL DEFAULT '[]'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('eval_sample_results'); +CREATE UNIQUE INDEX IF NOT EXISTS uq_eval_sample_results_task_index + ON eval_sample_results(eval_task_id, sample_index); +CREATE INDEX IF NOT EXISTS idx_eval_sample_results_task_status + ON eval_sample_results(eval_task_id, status); + +-- ========================= +-- Data convert and custom tools +-- ========================= + +CREATE TABLE IF NOT EXISTS data_convert_jobs ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + convert_type varchar(40) NOT NULL DEFAULT 'json_to_jsonl', + status task_status NOT NULL DEFAULT 'pending', + output_name varchar(200) NOT NULL, + encoding varchar(40) NOT NULL DEFAULT 'UTF-8', + source_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL, + result_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL, + error_message text, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + started_at timestamptz, + completed_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('data_convert_jobs'); +CREATE INDEX IF NOT EXISTS idx_data_convert_jobs_user_created ON data_convert_jobs(created_by, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_data_convert_jobs_status ON data_convert_jobs(status, created_at DESC); + +CREATE TABLE IF NOT EXISTS custom_tools ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(100) NOT NULL, + description text, + url text NOT NULL, + icon varchar(80) NOT NULL DEFAULT 'fa-cog', + visibility varchar(20) NOT NULL DEFAULT 'private', + owner_id uuid REFERENCES users(id) ON DELETE CASCADE, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('custom_tools'); +CREATE INDEX IF NOT EXISTS idx_custom_tools_owner ON custom_tools(owner_id) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_custom_tools_visibility ON custom_tools(visibility) WHERE deleted_at IS NULL; + +-- ========================= +-- Metrics snapshots +-- ========================= + +CREATE TABLE IF NOT EXISTS system_metric_snapshots ( + id bigserial PRIMARY KEY, + cpu jsonb NOT NULL DEFAULT '{}'::jsonb, + memory jsonb NOT NULL DEFAULT '{}'::jsonb, + disk jsonb NOT NULL DEFAULT '{}'::jsonb, + gpu jsonb NOT NULL DEFAULT '[]'::jsonb, + network jsonb NOT NULL DEFAULT '{}'::jsonb, + system jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_system_metric_snapshots_created ON system_metric_snapshots(created_at DESC); + +CREATE TABLE IF NOT EXISTS service_status_snapshots ( + id bigserial PRIMARY KEY, + service_name varchar(100) NOT NULL, + state varchar(30) NOT NULL, + instances_online integer NOT NULL DEFAULT 0, + instances_total integer NOT NULL DEFAULT 0, + detail jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_service_status_snapshots_name_created + ON service_status_snapshots(service_name, created_at DESC); + +-- ========================= +-- Useful views +-- ========================= + +CREATE OR REPLACE VIEW v_user_effective_permissions AS +SELECT + u.id AS user_id, + u.username, + p.code AS permission_code, + COALESCE(up.allowed, rp.permission_code IS NOT NULL, false) AS allowed +FROM users u +CROSS JOIN permissions p +LEFT JOIN role_permissions rp + ON rp.role = u.role AND rp.permission_code = p.code +LEFT JOIN user_permissions up + ON up.user_id = u.id AND up.permission_code = p.code +WHERE u.deleted_at IS NULL; + +CREATE OR REPLACE VIEW v_dataset_summary AS +SELECT + d.id, + d.name, + d.type, + d.storage_type, + d.source, + d.source_task_id, + d.size_bytes, + d.record_count, + d.description, + count(df.id) FILTER (WHERE df.deleted_at IS NULL) AS file_count, + d.created_at, + d.updated_at +FROM datasets d +LEFT JOIN dataset_files df ON df.dataset_id = d.id +WHERE d.deleted_at IS NULL +GROUP BY d.id; + +-- ========================= +-- Maintenance notes +-- ========================= + +-- 1. For very large installations, convert audit_logs, web_logs, +-- system_metric_snapshots, fine_tune_metrics and eval_sample_results to +-- monthly/range partitions. +-- 2. Keep large file bodies in storage_objects, not in relational rows. +-- 3. Encrypt api_key_encrypted and external source secrets at the application layer +-- with KMS or a deployment secret. +-- 4. Use soft delete for user-facing resources to preserve audit trails. + +-- ========================= +-- Enterprise governance and compute extension +-- ========================= + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'tenant_status') THEN + CREATE TYPE tenant_status AS ENUM ('active', 'disabled'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'project_status') THEN + CREATE TYPE project_status AS ENUM ('active', 'archived', 'disabled'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'project_role') THEN + CREATE TYPE project_role AS ENUM ('owner', 'maintainer', 'developer', 'reviewer', 'viewer'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'approval_status') THEN + CREATE TYPE approval_status AS ENUM ('not_required', 'pending', 'approved', 'rejected', 'cancelled', 'expired'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'compute_job_type') THEN + CREATE TYPE compute_job_type AS ENUM ('fine_tune', 'eval', 'data_process', 'inference', 'merge', 'convert', 'import'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'job_priority') THEN + CREATE TYPE job_priority AS ENUM ('low', 'normal', 'high', 'urgent'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'gpu_status') THEN + CREATE TYPE gpu_status AS ENUM ('idle', 'reserved', 'running', 'draining', 'offline', 'error'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'service_level') THEN + CREATE TYPE service_level AS ENUM ('test', 'production'); + END IF; +END $$; + +CREATE TABLE IF NOT EXISTS tenants ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code citext NOT NULL UNIQUE, + name varchar(150) NOT NULL, + status tenant_status NOT NULL DEFAULT 'active', + owner_id uuid REFERENCES users(id) ON DELETE SET NULL, + quota_config jsonb NOT NULL DEFAULT '{}'::jsonb, + retention_config jsonb NOT NULL DEFAULT '{}'::jsonb, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('tenants'); +CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS tenant_users ( + tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role user_role NOT NULL DEFAULT 'viewer', + is_tenant_admin boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (tenant_id, user_id) +); +CREATE INDEX IF NOT EXISTS idx_tenant_users_user ON tenant_users(user_id); + +CREATE TABLE IF NOT EXISTS projects ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + code citext NOT NULL, + name varchar(150) NOT NULL, + description text, + status project_status NOT NULL DEFAULT 'active', + owner_id uuid REFERENCES users(id) ON DELETE SET NULL, + quota_config jsonb NOT NULL DEFAULT '{}'::jsonb, + default_acl jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + archived_at timestamptz, + deleted_at timestamptz, + UNIQUE (tenant_id, code) +); +SELECT touch_updated_at('projects'); +CREATE INDEX IF NOT EXISTS idx_projects_tenant_status ON projects(tenant_id, status) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS project_members ( + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role project_role NOT NULL DEFAULT 'viewer', + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (project_id, user_id) +); +SELECT touch_updated_at('project_members'); +CREATE INDEX IF NOT EXISTS idx_project_members_user ON project_members(user_id); + +CREATE TABLE IF NOT EXISTS storage_nodes ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code citext NOT NULL UNIQUE, + name varchar(150) NOT NULL, + node_type varchar(40) NOT NULL DEFAULT 'compute_local', + base_path text NOT NULL, + total_bytes bigint, + used_bytes bigint NOT NULL DEFAULT 0, + status varchar(40) NOT NULL DEFAULT 'online', + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('storage_nodes'); + +CREATE TABLE IF NOT EXISTS training_engines ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code citext NOT NULL UNIQUE, + name varchar(150) NOT NULL, + version varchar(80), + engine_type varchar(60) NOT NULL DEFAULT 'llama_factory', + executable_path text, + python_env_path text, + supported_task_types jsonb NOT NULL DEFAULT '[]'::jsonb, + supported_methods jsonb NOT NULL DEFAULT '[]'::jsonb, + supported_formats jsonb NOT NULL DEFAULT '[]'::jsonb, + schema jsonb NOT NULL DEFAULT '{}'::jsonb, + status varchar(40) NOT NULL DEFAULT 'enabled', + last_health_check_at timestamptz, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('training_engines'); + +INSERT INTO training_engines( + code, name, engine_type, supported_task_types, supported_methods, supported_formats, status +) VALUES ( + 'llama_factory', + 'LLaMA-Factory', + 'llama_factory', + '["SFT", "DPO", "CPT"]'::jsonb, + '["lora", "qlora", "full"]'::jsonb, + '["alpaca", "sharegpt", "dpo_pair", "pretrain_text"]'::jsonb, + 'enabled' +) ON CONFLICT (code) DO NOTHING; + +CREATE TABLE IF NOT EXISTS quotas ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + subject_type varchar(40) NOT NULL, + subject_id uuid, + gpu_concurrency integer NOT NULL DEFAULT 0, + storage_bytes bigint NOT NULL DEFAULT 0, + max_running_jobs integer NOT NULL DEFAULT 0, + max_projects integer NOT NULL DEFAULT 0, + max_upload_file_bytes bigint NOT NULL DEFAULT 0, + config jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (subject_type, subject_id) +); +SELECT touch_updated_at('quotas'); +CREATE INDEX IF NOT EXISTS idx_quotas_tenant_project ON quotas(tenant_id, project_id); + +CREATE TABLE IF NOT EXISTS quota_usage ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + subject_type varchar(40) NOT NULL, + subject_id uuid, + gpu_running integer NOT NULL DEFAULT 0, + storage_used_bytes bigint NOT NULL DEFAULT 0, + running_jobs integer NOT NULL DEFAULT 0, + usage_detail jsonb NOT NULL DEFAULT '{}'::jsonb, + measured_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (subject_type, subject_id) +); +CREATE INDEX IF NOT EXISTS idx_quota_usage_tenant_project ON quota_usage(tenant_id, project_id); + +CREATE TABLE IF NOT EXISTS retention_policies ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + audit_days integer NOT NULL DEFAULT 180, + login_log_days integer NOT NULL DEFAULT 180, + training_log_days integer NOT NULL DEFAULT 90, + metric_raw_days integer NOT NULL DEFAULT 30, + temp_file_days integer NOT NULL DEFAULT 1, + failed_job_workspace_days integer NOT NULL DEFAULT 14, + checkpoint_policy jsonb NOT NULL DEFAULT '{"keep_last":3,"keep_best":2,"failed_job_days":14}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (tenant_id, project_id) +); +SELECT touch_updated_at('retention_policies'); + +CREATE TABLE IF NOT EXISTS approval_templates ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + action varchar(80) NOT NULL, + name varchar(150) NOT NULL, + enabled boolean NOT NULL DEFAULT true, + approver_rules jsonb NOT NULL DEFAULT '[]'::jsonb, + risk_level varchar(40) NOT NULL DEFAULT 'medium', + expire_hours integer NOT NULL DEFAULT 24, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('approval_templates'); +CREATE INDEX IF NOT EXISTS idx_approval_templates_scope_action + ON approval_templates(tenant_id, project_id, action); + +CREATE TABLE IF NOT EXISTS approval_instances ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + template_id uuid REFERENCES approval_templates(id) ON DELETE SET NULL, + action varchar(80) NOT NULL, + resource_type varchar(80) NOT NULL, + resource_id text NOT NULL, + reason text, + payload jsonb NOT NULL DEFAULT '{}'::jsonb, + status approval_status NOT NULL DEFAULT 'pending', + requested_by uuid REFERENCES users(id) ON DELETE SET NULL, + decided_by uuid REFERENCES users(id) ON DELETE SET NULL, + decided_at timestamptz, + expires_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('approval_instances'); +CREATE INDEX IF NOT EXISTS idx_approval_instances_status_created + ON approval_instances(status, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_approval_instances_scope + ON approval_instances(tenant_id, project_id, action, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_approval_instances_resource + ON approval_instances(resource_type, resource_id); + +CREATE TABLE IF NOT EXISTS approval_steps ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + approval_id uuid NOT NULL REFERENCES approval_instances(id) ON DELETE CASCADE, + step_no integer NOT NULL, + approver_user_id uuid REFERENCES users(id) ON DELETE SET NULL, + approver_role varchar(80), + status approval_status NOT NULL DEFAULT 'pending', + comment text, + decided_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (approval_id, step_no) +); +CREATE INDEX IF NOT EXISTS idx_approval_steps_approver + ON approval_steps(approver_user_id, status); + +CREATE TABLE IF NOT EXISTS compute_nodes ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code citext NOT NULL UNIQUE, + name varchar(150) NOT NULL, + host varchar(200) NOT NULL, + api_base_url text NOT NULL, + storage_node_id uuid REFERENCES storage_nodes(id) ON DELETE SET NULL, + status varchar(40) NOT NULL DEFAULT 'online', + agent_version varchar(80), + gpu_count integer NOT NULL DEFAULT 0, + last_heartbeat_at timestamptz, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('compute_nodes'); +CREATE INDEX IF NOT EXISTS idx_compute_nodes_status ON compute_nodes(status); + +CREATE TABLE IF NOT EXISTS gpu_devices ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + compute_node_id uuid NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE, + gpu_index integer NOT NULL, + uuid varchar(120) NOT NULL, + name varchar(150) NOT NULL, + status gpu_status NOT NULL DEFAULT 'idle', + memory_total_mb integer NOT NULL DEFAULT 0, + memory_used_mb integer NOT NULL DEFAULT 0, + utilization_percent numeric(5,2) NOT NULL DEFAULT 0, + temperature numeric(5,2), + power_w numeric(8,2), + driver_version varchar(80), + partition_type varchar(40) NOT NULL DEFAULT 'full', + parent_gpu_uuid varchar(120), + current_job_id uuid, + last_seen_at timestamptz, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (compute_node_id, gpu_index), + UNIQUE (uuid) +); +SELECT touch_updated_at('gpu_devices'); +CREATE INDEX IF NOT EXISTS idx_gpu_devices_node_status ON gpu_devices(compute_node_id, status); + +CREATE TABLE IF NOT EXISTS compute_jobs ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + app_task_type varchar(80) NOT NULL, + app_task_id uuid, + tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL, + project_id uuid REFERENCES projects(id) ON DELETE SET NULL, + compute_node_id uuid REFERENCES compute_nodes(id) ON DELETE SET NULL, + engine_id uuid REFERENCES training_engines(id) ON DELETE SET NULL, + job_type compute_job_type NOT NULL, + status task_status NOT NULL DEFAULT 'pending', + priority job_priority NOT NULL DEFAULT 'normal', + resource_request jsonb NOT NULL DEFAULT '{}'::jsonb, + workspace_root text, + payload jsonb NOT NULL DEFAULT '{}'::jsonb, + result jsonb NOT NULL DEFAULT '{}'::jsonb, + progress numeric(5,2) NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), + pid integer, + port integer, + failure_reason text, + requested_by uuid REFERENCES users(id) ON DELETE SET NULL, + started_at timestamptz, + completed_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('compute_jobs'); +CREATE INDEX IF NOT EXISTS idx_compute_jobs_scope_status + ON compute_jobs(tenant_id, project_id, status, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_compute_jobs_node_status + ON compute_jobs(compute_node_id, status, priority, created_at); +CREATE INDEX IF NOT EXISTS idx_compute_jobs_app_task + ON compute_jobs(app_task_type, app_task_id); + +CREATE TABLE IF NOT EXISTS gpu_allocations ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + compute_job_id uuid NOT NULL REFERENCES compute_jobs(id) ON DELETE CASCADE, + gpu_device_id uuid NOT NULL REFERENCES gpu_devices(id) ON DELETE RESTRICT, + tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL, + project_id uuid REFERENCES projects(id) ON DELETE SET NULL, + status varchar(40) NOT NULL DEFAULT 'reserved', + allocated_at timestamptz NOT NULL DEFAULT now(), + released_at timestamptz +); +CREATE INDEX IF NOT EXISTS idx_gpu_allocations_job ON gpu_allocations(compute_job_id); +CREATE INDEX IF NOT EXISTS idx_gpu_allocations_scope ON gpu_allocations(tenant_id, project_id, allocated_at DESC); +CREATE UNIQUE INDEX IF NOT EXISTS uq_gpu_allocations_active_gpu + ON gpu_allocations(gpu_device_id) WHERE released_at IS NULL; + +CREATE TABLE IF NOT EXISTS resource_acl ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + resource_type varchar(80) NOT NULL, + resource_id text NOT NULL, + subject_type varchar(40) NOT NULL, + subject_id text NOT NULL, + permissions text[] NOT NULL DEFAULT '{}', + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (resource_type, resource_id, subject_type, subject_id) +); +SELECT touch_updated_at('resource_acl'); +CREATE INDEX IF NOT EXISTS idx_resource_acl_resource + ON resource_acl(resource_type, resource_id); +CREATE INDEX IF NOT EXISTS idx_resource_acl_subject + ON resource_acl(subject_type, subject_id); + +CREATE TABLE IF NOT EXISTS file_upload_sessions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + storage_node_id uuid REFERENCES storage_nodes(id) ON DELETE SET NULL, + original_name text NOT NULL, + byte_size bigint NOT NULL DEFAULT 0, + checksum_sha256 char(64), + part_size_bytes integer NOT NULL DEFAULT 8388608, + uploaded_parts jsonb NOT NULL DEFAULT '[]'::jsonb, + status varchar(40) NOT NULL DEFAULT 'uploading', + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + expires_at timestamptz NOT NULL DEFAULT now() + interval '1 day', + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('file_upload_sessions'); +CREATE INDEX IF NOT EXISTS idx_file_upload_sessions_scope_status + ON file_upload_sessions(tenant_id, project_id, status, created_at DESC); + +CREATE TABLE IF NOT EXISTS local_import_jobs ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + compute_node_id uuid REFERENCES compute_nodes(id) ON DELETE SET NULL, + import_type varchar(40) NOT NULL, + source_path text NOT NULL, + target_resource_id uuid, + status task_status NOT NULL DEFAULT 'pending', + scan_result jsonb NOT NULL DEFAULT '{}'::jsonb, + failure_reason text, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +SELECT touch_updated_at('local_import_jobs'); +CREATE INDEX IF NOT EXISTS idx_local_import_jobs_scope_status + ON local_import_jobs(tenant_id, project_id, status, created_at DESC); + +CREATE TABLE IF NOT EXISTS fine_tune_checkpoints ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + fine_tune_task_id uuid NOT NULL REFERENCES fine_tune_tasks(id) ON DELETE CASCADE, + storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL, + checkpoint_name varchar(200) NOT NULL, + step integer, + metric_name varchar(80), + metric_value numeric(18,8), + is_best boolean NOT NULL DEFAULT false, + protected boolean NOT NULL DEFAULT false, + size_bytes bigint NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_fine_tune_checkpoints_task_step + ON fine_tune_checkpoints(fine_tune_task_id, step DESC); +CREATE INDEX IF NOT EXISTS idx_fine_tune_checkpoints_best + ON fine_tune_checkpoints(fine_tune_task_id, is_best) WHERE is_best; + +CREATE TABLE IF NOT EXISTS model_services ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + model_id uuid REFERENCES models(id) ON DELETE SET NULL, + trained_model_id uuid REFERENCES trained_models(id) ON DELETE SET NULL, + compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL, + name varchar(150) NOT NULL, + service_level service_level NOT NULL DEFAULT 'test', + status task_status NOT NULL DEFAULT 'pending', + gpu_device_id uuid REFERENCES gpu_devices(id) ON DELETE SET NULL, + port integer, + max_concurrency integer NOT NULL DEFAULT 1, + timeout_seconds integer NOT NULL DEFAULT 120, + max_context_tokens integer, + approval_id uuid REFERENCES approval_instances(id) ON DELETE SET NULL, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + started_at timestamptz, + stopped_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +SELECT touch_updated_at('model_services'); +CREATE INDEX IF NOT EXISTS idx_model_services_scope_status + ON model_services(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS cleanup_jobs ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE, + project_id uuid REFERENCES projects(id) ON DELETE CASCADE, + cleanup_type varchar(80) NOT NULL, + status task_status NOT NULL DEFAULT 'pending', + target jsonb NOT NULL DEFAULT '{}'::jsonb, + result jsonb NOT NULL DEFAULT '{}'::jsonb, + failure_reason text, + created_at timestamptz NOT NULL DEFAULT now(), + started_at timestamptz, + completed_at timestamptz +); +CREATE INDEX IF NOT EXISTS idx_cleanup_jobs_status_created ON cleanup_jobs(status, created_at DESC); + +-- User identity provider extension. First release uses local accounts; +-- OIDC/LDAP can be enabled later without changing resource ownership tables. +ALTER TABLE users ADD COLUMN IF NOT EXISTS auth_provider varchar(40) NOT NULL DEFAULT 'local'; +ALTER TABLE users ADD COLUMN IF NOT EXISTS external_id text; +ALTER TABLE users ADD COLUMN IF NOT EXISTS default_tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_users_external_provider ON users(auth_provider, external_id); +CREATE INDEX IF NOT EXISTS idx_users_default_tenant ON users(default_tenant_id); + +-- Add tenant/project/resource governance columns to existing resource tables. +ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS storage_node_id uuid REFERENCES storage_nodes(id) ON DELETE SET NULL; +ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS visibility varchar(40) NOT NULL DEFAULT 'project'; +ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS deleted_at timestamptz; +CREATE INDEX IF NOT EXISTS idx_storage_objects_scope + ON storage_objects(tenant_id, project_id, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE models ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE models ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE models ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE models ADD COLUMN IF NOT EXISTS visibility varchar(40) NOT NULL DEFAULT 'project'; +ALTER TABLE models ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required'; +CREATE INDEX IF NOT EXISTS idx_models_scope_status + ON models(tenant_id, project_id, approval_status, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS visibility varchar(40) NOT NULL DEFAULT 'project'; +ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required'; +CREATE INDEX IF NOT EXISTS idx_trained_models_scope + ON trained_models(tenant_id, project_id, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE datasets ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE datasets ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE datasets ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE datasets ADD COLUMN IF NOT EXISTS visibility varchar(40) NOT NULL DEFAULT 'project'; +ALTER TABLE datasets ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required'; +CREATE INDEX IF NOT EXISTS idx_datasets_scope_status + ON datasets(tenant_id, project_id, type, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_dataset_files_scope + ON dataset_files(tenant_id, project_id, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required'; +ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_data_process_tasks_scope_status + ON data_process_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required'; +ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL; +ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS resume_checkpoint_id uuid REFERENCES fine_tune_checkpoints(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_scope_status + ON fine_tune_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required'; +ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_inference_tasks_scope_status + ON inference_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required'; +ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_eval_tasks_scope_status + ON eval_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE data_convert_jobs ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE data_convert_jobs ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE data_convert_jobs ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE data_convert_jobs ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_data_convert_jobs_scope_status + ON data_convert_jobs(tenant_id, project_id, status, created_at DESC); + +ALTER TABLE custom_tools ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE custom_tools ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_custom_tools_scope + ON custom_tools(tenant_id, project_id, visibility, created_at DESC) WHERE deleted_at IS NULL; + +ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS approval_id uuid REFERENCES approval_instances(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_audit_logs_scope_created + ON audit_logs(tenant_id, project_id, created_at DESC); + +ALTER TABLE web_logs ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL; +ALTER TABLE web_logs ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_web_logs_scope_created + ON web_logs(tenant_id, project_id, created_at DESC); + +CREATE OR REPLACE VIEW v_project_member_permissions AS +SELECT + p.tenant_id, + pm.project_id, + pm.user_id, + pm.role, + CASE pm.role + WHEN 'owner' THEN ARRAY['read','write','execute','download','delete','manage_acl'] + WHEN 'maintainer' THEN ARRAY['read','write','execute','download','delete'] + WHEN 'developer' THEN ARRAY['read','write','execute','download'] + WHEN 'reviewer' THEN ARRAY['read','download'] + ELSE ARRAY['read'] + END AS permissions +FROM project_members pm +JOIN projects p ON p.id = pm.project_id +WHERE p.deleted_at IS NULL; diff --git a/docs/system-development-plan.md b/docs/system-development-plan.md new file mode 100644 index 0000000..27d675e --- /dev/null +++ b/docs/system-development-plan.md @@ -0,0 +1,1094 @@ +# 模型训练平台整体架构与开发计划 + +> 本文衔接 `docs/backend-api-design.md`、`docs/postgres-schema.sql`、`docs/platform-architecture-requirements.md`,用于多人并行开发。文档给出关键未决问题建议、整体架构、模块边界、开发拆分、交付计划和联调顺序。 + +## 1. 关键问题建议 + +### 1.1 文件存储位置 + +建议:训练相关文件主存储放在算力服务器本地磁盘,应用服务器只保留上传临时文件。 + +原因: + +- 算力服务器需要高频读取数据集、模型、checkpoint,本地磁盘性能和路径稳定性更好。 +- 应用平台与算力平台分离后,应用服务器不应直接持有长期训练资产。 +- 应用服务器临时文件建议保留 24 小时,上传成功转发到算力文件网关后立即标记可清理。 + +落地要求: + +- 算力平台提供文件网关 API。 +- 数据库保存 `storage_node_id`、`relative_path`、`checksum_sha256`、`byte_size`。 +- 前端下载通过应用平台申请短时链接,不暴露真实磁盘路径。 + +### 1.2 GPU 共享/MIG + +建议:第一版不支持 MIG 和 GPU 分片,一张 GPU 同一时间只分配给一个训练/推理服务。 + +原因: + +- 当前目标是单机多 GPU,先保证调度稳定、日志可追踪、资源不冲突。 +- 训练任务显存波动较大,共享会增加失败和排障成本。 + +预留: + +- GPU 表中保留 `partition_type`、`parent_gpu_uuid`、`memory_total_mb` 字段。 +- 后续如需要 MIG,可扩展为 GPU slice 资源。 + +### 1.3 任务抢占 + +建议:第一版不做自动抢占,仅支持管理员手动停止或审批后停止。 + +原因: + +- 自动抢占需要 checkpoint 恢复、优先级策略、资源补偿,复杂度较高。 +- 训练任务被抢占可能导致用户产物损坏或成本浪费。 + +落地: + +- 支持任务优先级和排队。 +- 高优先级任务可排在队列前。 +- 停止他人任务需要审批或平台管理员权限。 + +### 1.4 离线导入已有模型和数据集 + +建议:必须支持离线导入。 + +原因: + +- 训练平台通常会接入已有 `/data/models`、`/data/datasets`。 +- 只允许网页上传会影响大模型和大数据集迁移效率。 + +功能: + +- 管理员填写算力节点上的路径。 +- 算力 Agent 扫描目录、校验大小、格式、checksum。 +- 应用平台登记为模型或数据集。 +- 导入资源默认归属指定租户和项目。 + +### 1.5 测试服务与生产服务 + +建议:模型发布区分测试服务和生产服务。 + +规则: + +- 测试服务:项目内成员可创建,默认限流,允许短期运行。 +- 生产服务:必须审批,记录发布版本,支持下线审批。 + +页面: + +- 模型服务列表。 +- 测试服务启动。 +- 生产发布申请。 +- 服务调用统计。 + +### 1.6 数据脱敏与质量评分 + +建议:作为数据处理模块的一等能力纳入第一期,但可以先实现规则版。 + +第一期: + +- 手机号、身份证、邮箱、银行卡、地址等规则脱敏。 +- 空值、重复、过短、JSON 格式错误、字段缺失检测。 +- 数据质量分:完整性、重复率、格式正确率、长度分布。 + +后续: + +- LLM 敏感内容识别。 +- 行业词表。 +- 人工复核闭环。 + +### 1.7 人工评测/复核沉淀 + +建议:第二期实现,但第一期数据库和页面入口预留。 + +原因: + +- 人工评测会引入标注任务、分配、复核、一致性统计。 +- 第一版可以先完成自动评测和样本级结果展示。 + +预留: + +- `eval_sample_results` 支持人工修订字段。 +- 新增人工复核状态:`unreviewed`、`reviewed`、`disputed`。 + +### 1.8 断点续训 + +建议:第一版支持“从 checkpoint 手动恢复”,暂不做自动失败续训。 + +功能: + +- 训练详情展示 checkpoint 列表。 +- 新建训练任务可选择 checkpoint 作为恢复点。 +- 失败任务允许一键重试,重试时选择最近 checkpoint。 + +后续: + +- 自动失败检测。 +- 按错误类型决定是否自动恢复。 + +### 1.9 Checkpoint 清理 + +建议:必须支持自动清理策略。 + +默认策略: + +- 保留最近 3 个 checkpoint。 +- 保留指标最优 2 个 checkpoint。 +- 已发布模型对应 checkpoint 不自动删除。 +- 失败任务 checkpoint 保留 14 天。 + +页面: + +- 任务详情 checkpoint 管理。 +- 存储管理页查看可清理空间。 + +### 1.10 对外标准 API + +建议:第一版提供内部 API,第二期开放外部 API。 + +第一版: + +- 使用 JWT + 服务 token。 +- 面向前端和算力平台。 + +第二期: + +- API Key 管理。 +- OpenAPI 文档。 +- 请求限流。 +- Webhook 回调。 +- 外部系统发起训练、评测、推理。 + +### 1.11 企业统一身份认证 + +建议:第一版使用本地账号,预留 OIDC/LDAP;第二期接入企业统一认证。 + +落地: + +- 用户表增加 `auth_provider`、`external_id`。 +- 登录模块抽象 provider。 +- 支持本地用户和外部用户共存。 + +### 1.12 成本核算 + +建议:第一版做资源用量统计,第二期做成本核算。 + +第一版统计: + +- GPU 小时。 +- 磁盘占用。 +- 任务运行时长。 +- 推理调用次数、token 量。 + +第二期核算: + +- 租户/项目账单。 +- GPU 小时单价。 +- 存储单价。 +- 模型服务调用成本。 + +## 2. 总体目标范围 + +### 2.1 第一版目标 + +交付一个可在企业内使用的单机多 GPU 模型训练平台: + +- 支持多租户、多项目。 +- 支持项目级资源隔离和模型/数据集级授权。 +- 支持本地磁盘文件管理。 +- 支持 LLaMA-Factory 微调训练。 +- 支持数据集上传、预览、版本、处理和发布。 +- 支持训练任务、日志、指标、产物登记。 +- 支持模型评测、推理服务和模型对比。 +- 支持审批、审计、基础保留策略。 +- 支持应用平台和算力平台分离部署。 + +### 2.2 第一版不做或仅预留 + +- 自动任务抢占。 +- MIG/GPU 分片。 +- 自动断点续训。 +- 完整人工评测生产线。 +- 外部开放 API 市场化管理。 +- 精细成本计费。 + +## 3. 系统整体架构 + +### 3.1 架构分层 + +```mermaid +flowchart TB + FE["前端 Vue 应用"] + API["应用平台 FastAPI"] + DB["PostgreSQL 元数据"] + REDIS["Redis 队列/缓存/锁"] + WORKER["应用平台 Worker"] + COMPUTE["算力平台 Compute API"] + AGENT["Compute Agent"] + FILE["本地文件网关"] + ENGINE["LLaMA-Factory Adapter"] + GPU["单机多 GPU"] + DISK["算力节点本地磁盘"] + + FE --> API + API --> DB + API --> REDIS + WORKER --> DB + WORKER --> REDIS + API --> COMPUTE + COMPUTE --> AGENT + AGENT --> FILE + AGENT --> ENGINE + ENGINE --> GPU + FILE --> DISK + ENGINE --> DISK +``` + +### 3.2 仓库建议结构 + +```text +YG_FT/ + frontend/ + backend/ + app/ + api/ + core/ + modules/ + auth/ + tenant/ + project/ + model/ + dataset/ + data_process/ + fine_tune/ + eval/ + inference/ + approval/ + audit/ + compute_gateway/ + db/ + schemas/ + services/ + workers/ + migrations/ + tests/ + compute/ + api/ + agent/ + engines/ + llama_factory/ + file_gateway/ + tests/ + docs/ + deploy/ + app/ + compute/ + nginx/ + systemd/ +``` + +## 4. 开发内容总览 + +### 4.1 前端开发 + +现有前端已有基础页面: + +- 服务看板。 +- 模型训练列表/创建。 +- 评测列表/创建/详情。 +- 推理列表/创建/对话。 +- 模型对比。 +- 模型管理。 +- 数据处理列表/创建/详情。 +- 数据集管理/上传/预览。 +- 工具和数据转换原型。 +- 硬件和日志。 + +需要补全: + +| 模块 | 页面 | 优先级 | +| --- | --- | --- | +| 用户中心 | 用户列表、创建用户、权限设置、无权限页 | P0 | +| 租户项目 | 租户管理、项目列表、项目详情、成员管理 | P0 | +| 项目上下文 | 顶部项目切换器、项目权限提示 | P0 | +| 审批中心 | 我的申请、待我审批、审批详情、模板配置 | P0 | +| 算力资源 | GPU 状态、任务队列、节点 Agent 状态、资源配额 | P0 | +| 存储管理 | 磁盘占用、大文件、checkpoint、清理策略 | P1 | +| 训练增强 | checkpoint、恢复训练、产物管理、训练引擎选择 | P1 | +| 模型服务 | 测试服务、生产发布、服务实例、调用统计 | P1 | +| 数据治理 | 脱敏规则、质量评分、数据处理结果发布 | P1 | +| 审计中心 | 操作审计、登录审计、下载审计 | P1 | +| 引擎管理 | LLaMA-Factory 健康、参数 schema、接入标准 | P2 | + +### 4.2 后端接口开发 + +第一批必须完成: + +- Auth / RBAC / 项目 ACL。 +- Tenant / Project。 +- Model / Dataset。 +- Data Process。 +- Fine Tune。 +- Compute Gateway。 +- Approval / Audit。 +- System Monitor / Logs。 + +第二批完成: + +- Eval。 +- Inference / Compare。 +- File cleanup / retention。 +- Checkpoint management。 +- Model service governance。 + +第三批完成: + +- External API。 +- OIDC/LDAP。 +- Cost accounting。 +- Human review。 + +### 4.3 DB 开发 + +在 `postgres-schema.sql` 基础上新增企业治理和隔离模型: + +- 租户、项目、成员。 +- 资源 ACL。 +- 审批。 +- 配额和用量。 +- 算力节点、GPU、分配记录。 +- 训练引擎。 +- 保留策略。 + +同时调整已有资源表: + +- 增加 `tenant_id`、`project_id`、`owner_id`。 +- 增加 `approval_status`。 +- 增加 `storage_node_id`。 +- 增加资源可见性字段。 + +### 4.4 系统部署开发 + +需要拆成应用平台部署包和算力平台部署包: + +应用平台: + +- Nginx。 +- Frontend 静态资源。 +- FastAPI。 +- PostgreSQL。 +- Redis。 +- App Worker。 + +算力平台: + +- Compute API。 +- Compute Agent。 +- File Gateway。 +- LLaMA-Factory 环境。 +- GPU 监控采集。 +- 本地磁盘目录初始化。 + +## 5. 前端开发计划 + +### 5.1 P0 页面 + +#### 用户中心 + +路由: + +- `/user-settings` +- `/user-settings/create` +- `/user-settings/:id/permission` +- `/permission-denied` + +功能: + +- 用户列表、搜索、角色筛选、状态筛选。 +- 创建用户、禁用用户、重置密码。 +- 页面权限分配。 +- 租户和项目关联展示。 +- 无权限页友好提示。 + +联调接口: + +- `GET /api/users` +- `POST /api/users` +- `PUT /api/users/{id}` +- `DELETE /api/users/{id}` +- `PUT /api/users/{id}/password` + +#### 项目空间 + +路由: + +- `/projects` +- `/projects/create` +- `/projects/:id` +- `/projects/:id/members` +- `/projects/:id/permissions` + +功能: + +- 项目列表。 +- 创建项目。 +- 项目概览。 +- 成员管理。 +- 项目配额查看。 +- 项目切换器。 + +联调接口: + +- `GET /api/projects` +- `POST /api/projects` +- `GET /api/projects/{id}` +- `PUT /api/projects/{id}` +- `GET /api/projects/{id}/members` +- `POST /api/projects/{id}/members` + +#### 审批中心 + +路由: + +- `/approvals` +- `/approvals/pending` +- `/approvals/mine` +- `/approvals/:id` +- `/approval-settings` + +功能: + +- 待审批。 +- 我的申请。 +- 审批详情。 +- 通过、驳回、撤回。 +- 审批模板配置。 + +联调接口: + +- `GET /api/approvals` +- `POST /api/approvals/{id}/approve` +- `POST /api/approvals/{id}/reject` +- `POST /api/approvals/{id}/cancel` + +#### 算力资源中心 + +路由: + +- `/compute` +- `/compute/gpus` +- `/compute/queue` +- `/compute/nodes` + +功能: + +- GPU 卡片状态。 +- GPU 进程和任务占用。 +- 任务队列。 +- Agent 健康状态。 +- 租户/项目配额。 + +联调接口: + +- `GET /api/compute/gpus` +- `GET /api/compute/queue` +- `GET /api/compute/nodes` +- `GET /api/quotas/usage` + +### 5.2 现有页面改造 + +所有资源列表页需要增加项目上下文: + +- 数据集列表按项目过滤。 +- 模型列表按项目过滤。 +- 训练任务按项目过滤。 +- 评测任务按项目过滤。 +- 推理任务按项目过滤。 + +通用改造: + +- 请求自动带 `project_id`。 +- 顶部项目切换后刷新数据。 +- 无项目权限时进入无权限页。 +- 删除、导出、发布等危险动作走审批。 + +## 6. 后端开发计划 + +### 6.1 P0 模块 + +#### Auth 模块 + +开发内容: + +- 登录、登出、当前用户。 +- JWT 生成和校验。 +- 密码 hash。 +- 页面权限。 +- 项目权限。 +- 服务间 token。 + +交付接口: + +- `POST /api/login` +- `POST /api/logout` +- `GET /api/me` + +#### Tenant / Project 模块 + +开发内容: + +- 租户 CRUD。 +- 项目 CRUD。 +- 项目成员。 +- 项目角色。 +- 项目配额读取。 + +交付接口: + +- `/api/tenants` +- `/api/projects` +- `/api/projects/{id}/members` + +#### Resource ACL 模块 + +开发内容: + +- 资源授权。 +- 权限校验依赖。 +- 数据查询作用域过滤。 + +交付接口: + +- `GET /api/resources/{resource_type}/{resource_id}/acl` +- `PUT /api/resources/{resource_type}/{resource_id}/acl` + +#### Compute Gateway 模块 + +开发内容: + +- 算力平台客户端。 +- 创建 compute job。 +- 查询状态和日志。 +- 状态回调验签。 +- 任务状态映射。 + +交付接口: + +- `GET /api/compute/gpus` +- `GET /api/compute/queue` +- `POST /api/internal/compute-callbacks/jobs` + +#### Fine Tune 模块 + +开发内容: + +- 训练任务创建。 +- 参数校验。 +- 审批校验。 +- GPU 配额校验。 +- 下发算力任务。 +- 进度和日志。 +- 产物登记。 + +交付接口: + +- 沿用 `docs/backend-api-design.md` 的 `/api/fine-tune` 系列。 + +#### Approval 模块 + +开发内容: + +- 审批模板。 +- 审批实例。 +- 审批动作。 +- 审批通过后触发原业务。 + +交付接口: + +- `/api/approvals` +- `/api/approval-templates` + +#### Audit 模块 + +开发内容: + +- 请求审计中间件。 +- 操作前后数据记录。 +- 登录审计。 +- 下载审计。 +- 审计查询。 + +交付接口: + +- `GET /api/audit-logs` +- `GET /api/login-logs` + +### 6.2 P1 模块 + +- 数据处理后端化。 +- 数据脱敏和质量评分。 +- Checkpoint 管理。 +- 模型服务治理。 +- 存储清理策略。 +- 评测详情增强。 + +### 6.3 P2 模块 + +- 外部 API Key。 +- OIDC/LDAP。 +- 成本核算。 +- 人工评测。 +- 多训练引擎接入。 + +## 7. 算力平台开发计划 + +### 7.1 Compute API + +接口: + +- `POST /compute/jobs` +- `GET /compute/jobs/{id}` +- `POST /compute/jobs/{id}/stop` +- `GET /compute/jobs/{id}/logs` +- `GET /compute/resources/gpus` +- `POST /compute/files/upload` +- `GET /compute/files/{id}/download` + +职责: + +- 接收应用平台任务。 +- 校验服务 token。 +- 调用 Agent。 +- 聚合状态。 +- 回调应用平台。 + +### 7.2 Compute Agent + +职责: + +- 发现 GPU。 +- 采集 GPU 指标。 +- 锁定/释放 GPU。 +- 管理任务进程。 +- 管理端口。 +- 管理工作区。 +- 上传日志和任务事件。 + +关键要求: + +- Agent 重启后能恢复正在运行的任务状态。 +- 任务异常退出要释放 GPU 锁。 +- 停止任务要先 graceful stop,再强制 kill。 + +### 7.3 LLaMA-Factory Adapter + +职责: + +- 生成训练 YAML 或 CLI。 +- 生成 dataset_info。 +- 设置 `CUDA_VISIBLE_DEVICES`。 +- 启动训练。 +- 解析日志。 +- 收集产物。 +- 合并 LoRA。 +- 导出量化模型。 + +### 7.4 File Gateway + +职责: + +- 分片上传。 +- 文件校验。 +- 文件预览。 +- 文件下载。 +- 目录扫描导入。 +- 短时下载令牌。 + +## 8. 数据库开发计划 + +### 8.1 迁移顺序 + +1. 基础扩展和枚举。 +2. 用户、角色、权限。 +3. 租户、项目、成员。 +4. 资源 ACL。 +5. 文件对象和存储节点。 +6. 模型、数据集。 +7. 数据处理、训练、评测、推理。 +8. 审批、审计。 +9. 算力节点、GPU、队列、配额。 +10. 保留策略和清理记录。 + +### 8.2 新增表清单 + +| 表 | 说明 | +| --- | --- | +| `tenants` | 租户 | +| `tenant_users` | 租户用户 | +| `projects` | 项目 | +| `project_members` | 项目成员 | +| `resource_acl` | 资源级授权 | +| `approval_templates` | 审批模板 | +| `approval_instances` | 审批单 | +| `approval_steps` | 审批步骤 | +| `quotas` | 配额 | +| `quota_usage` | 配额使用 | +| `storage_nodes` | 存储节点 | +| `compute_nodes` | 算力节点 | +| `gpu_devices` | GPU 设备 | +| `gpu_allocations` | GPU 分配记录 | +| `compute_jobs` | 算力任务 | +| `training_engines` | 训练引擎 | +| `retention_policies` | 保留策略 | +| `cleanup_jobs` | 清理任务 | + +### 8.3 索引原则 + +- 所有租户资源表建立 `(tenant_id, project_id, created_at desc)` 索引。 +- 列表页常用状态字段建立 `(tenant_id, project_id, status, created_at desc)` 索引。 +- 审计日志按时间分区。 +- 指标数据按时间分区。 +- 数据集样本大表按 `dataset_id` 和 `version_id` 建索引。 +- JSONB 配置只对高频查询字段做表达式索引。 + +## 9. 部署计划 + +### 9.1 应用平台部署 + +组件: + +- `frontend-nginx` +- `backend-api` +- `backend-worker` +- `postgres` +- `redis` + +配置: + +- `DATABASE_URL` +- `REDIS_URL` +- `JWT_SECRET` +- `SERVICE_TOKEN` +- `COMPUTE_API_BASE_URL` +- `FILE_UPLOAD_TEMP_DIR` +- `AUDIT_RETENTION_DAYS` + +### 9.2 算力平台部署 + +组件: + +- `compute-api` +- `compute-agent` +- `file-gateway` +- `llama-factory-env` + +配置: + +- `COMPUTE_NODE_ID` +- `SERVICE_TOKEN` +- `APP_CALLBACK_URL` +- `DATA_ROOT=/data/ft-platform` +- `LLAMA_FACTORY_PATH` +- `PYTHON_ENV_PATH` +- `GPU_VISIBLE_DEVICES` +- `PORT_RANGE_START` +- `PORT_RANGE_END` + +### 9.3 网络要求 + +- 前端只访问应用平台。 +- 应用平台可访问算力平台内部 API。 +- 算力平台可回调应用平台 internal callback。 +- 算力平台不直接暴露给公网。 +- 文件下载通过应用平台签发令牌。 + +### 9.4 运维脚本 + +需要提供: + +- 应用平台启动/停止。 +- 算力平台启动/停止。 +- 数据库初始化。 +- 默认管理员初始化。 +- 本地磁盘目录初始化。 +- LLaMA-Factory 健康检查。 +- GPU 诊断脚本。 +- 日志清理脚本。 + +## 10. 联调顺序 + +### 阶段 1:基础框架 + +目标:前后端登录、项目上下文、权限校验跑通。 + +交付: + +- 登录。 +- 用户列表。 +- 项目列表。 +- 页面权限。 +- 当前项目切换。 + +参与: + +- 前端 1 人。 +- 后端 1 人。 +- DB 1 人。 + +### 阶段 2:资源管理 + +目标:模型、数据集、文件上传、离线导入跑通。 + +交付: + +- 模型管理。 +- 数据集管理。 +- 文件版本。 +- 算力文件网关。 + +参与: + +- 前端 1 人。 +- 后端 2 人。 +- 算力 1 人。 + +### 阶段 3:训练链路 + +目标:创建训练任务并在单机多 GPU 上执行 LLaMA-Factory。 + +交付: + +- GPU 状态。 +- 训练创建。 +- 训练启动。 +- 训练日志。 +- 指标曲线。 +- 产物登记。 + +参与: + +- 前端 1 人。 +- 后端 2 人。 +- 算力 2 人。 + +### 阶段 4:数据处理与评测 + +目标:训练前数据处理、训练后评测闭环。 + +交付: + +- 数据处理任务。 +- 脱敏和质量评分。 +- 发布数据集。 +- 自动评测。 +- 评测详情。 + +参与: + +- 前端 1 人。 +- 后端 2 人。 + +### 阶段 5:推理服务与发布治理 + +目标:训练产物可发布为测试/生产服务。 + +交付: + +- 推理服务启动。 +- 对话和对比。 +- 发布审批。 +- 服务监控。 + +参与: + +- 前端 1 人。 +- 后端 1 人。 +- 算力 1 人。 + +### 阶段 6:企业治理 + +目标:审批、审计、保留、清理、配额完善。 + +交付: + +- 审批中心。 +- 审计中心。 +- 配额管理。 +- 存储清理。 +- 保留策略。 + +参与: + +- 前端 1 人。 +- 后端 2 人。 +- DB 1 人。 + +## 11. 人员分工建议 + +### 前端组 + +- FE-A:基础布局、用户中心、项目空间。 +- FE-B:模型、数据集、数据处理。 +- FE-C:训练、评测、推理、算力监控。 + +### 后端应用组 + +- BE-A:认证、租户、项目、权限、审计。 +- BE-B:模型、数据集、数据处理、文件元数据。 +- BE-C:训练、评测、推理、审批、任务编排。 + +### 算力组 + +- CE-A:Compute API、Agent、GPU 调度。 +- CE-B:LLaMA-Factory Adapter、日志解析、产物管理。 +- CE-C:File Gateway、本地磁盘、离线导入。 + +### 数据库/部署组 + +- DB-A:迁移脚本、索引、分区、种子数据。 +- OPS-A:Docker/systemd、Nginx、环境变量、健康检查。 + +## 12. 里程碑计划 + +| 里程碑 | 时间建议 | 目标 | +| --- | --- | --- | +| M1 基础治理 | 第 1-2 周 | 登录、用户、租户、项目、权限 | +| M2 资源管理 | 第 3-4 周 | 模型、数据集、文件网关、离线导入 | +| M3 训练主链路 | 第 5-7 周 | GPU 调度、LLaMA-Factory 训练、日志指标、产物 | +| M4 数据处理和评测 | 第 8-9 周 | 数据处理、质量评分、自动评测 | +| M5 推理发布 | 第 10-11 周 | 推理服务、模型对比、生产发布审批 | +| M6 企业治理收口 | 第 12 周 | 审批、审计、配额、清理、部署文档 | + +## 13. 验收标准 + +### 13.1 功能验收 + +- 不同租户用户不能看到彼此资源。 +- 项目成员只能访问授权项目资源。 +- 模型和数据集支持资源级授权。 +- 上传数据集后可预览、编辑版本、用于训练。 +- 训练任务可指定 GPU 并成功运行。 +- 训练日志和指标实时可见。 +- 训练产物可登记、合并、发布测试服务。 +- 评测任务可生成样本级结果。 +- 删除数据集、模型发布等动作可触发审批。 +- 审计日志能追踪关键操作。 + +### 13.2 性能验收 + +- 常规列表接口 P95 小于 500ms。 +- GPU 状态刷新周期 5-10 秒。 +- 训练日志拉取支持 tail,不一次加载全文件。 +- 大文件上传支持分片。 +- 数据集样本超过百万行时列表和预览仍可用。 + +### 13.3 安全验收 + +- API Key 和外部数据源密码不明文存储。 +- 文件下载链接短时有效。 +- 服务间接口不能被前端直接调用。 +- 用户越权访问返回 403。 +- 审计日志不可由普通用户删除。 + +## 14. 开发风险和应对 + +| 风险 | 影响 | 应对 | +| --- | --- | --- | +| 应用/算力分离导致文件路径复杂 | 上传、下载、训练失败 | 统一文件网关,不暴露真实路径 | +| GPU 锁释放异常 | 资源被占用 | Agent 心跳和任务恢复扫描 | +| LLaMA-Factory 参数变化 | 训练失败 | 训练引擎 schema 和版本管理 | +| 权限模型过复杂 | 开发慢、容易越权 | RBAC + 项目角色 + ACL 三层固定规则 | +| 审批流影响体验 | 用户觉得操作慢 | 只对高风险动作审批,低风险动作直通 | +| 本地磁盘爆满 | 训练失败 | 配额、清理策略、checkpoint 保留策略 | + +## 15. 文档交付关系 + +- `docs/backend-api-design.md`:接口定义基础版本。 +- `docs/postgres-schema.sql`:初始数据库模型。 +- `docs/platform-architecture-requirements.md`:架构与功能需求补充。 +- `docs/system-development-plan.md`:多人开发拆分和实施计划。 + +后续建议再补两份执行文档: + +- `docs/compute-platform-api.md`:算力平台内部 API 细节。 +- `docs/db-migration-v2.sql`:多租户、项目、审批、算力调度的数据库增量脚本。 + +## 16. 页面模块开发工作包 + +本节用于多人并行开发时认领任务。每个工作包都标明对应页面、前端内容、后端接口、DB 表和部署/算力依赖。 + +### 16.1 基础入口与用户权限 + +| 工作包 | 页面/路由 | 前端开发 | 后端开发 | DB 关联 | 依赖 | +| --- | --- | --- | --- | --- | --- | +| 登录和会话 | `/login` | 登录表单、错误提示、登录后跳转、会话过期处理 | `POST /api/login`、`GET /api/me`、JWT 中间件 | `users`、`login_sessions`、`v_user_effective_permissions` | 无 | +| 主布局和权限菜单 | `/` | 菜单按权限过滤、用户信息、项目切换器 | 当前用户权限、当前项目上下文 | `permissions`、`role_permissions`、`project_members` | 登录 | +| 用户中心 | `/user-settings`、`/user-settings/create`、`/user-settings/:id/permission` | 用户列表、创建、禁用、重置密码、页面权限 | `/api/users` 系列 | `users`、`user_permissions`、`tenant_users` | 租户/项目 | +| 无权限页 | `/permission-denied` | 无权限说明、返回入口 | 权限异常返回统一错误码 | 无新增 | 主布局 | + +### 16.2 租户和项目空间 + +| 工作包 | 页面/路由 | 前端开发 | 后端开发 | DB 关联 | 依赖 | +| --- | --- | --- | --- | --- | --- | +| 租户管理 | `/tenants`、`/tenants/:id` | 租户列表、创建/禁用、配额、留存策略 | `/api/tenants` 系列 | `tenants`、`tenant_users`、`quotas`、`retention_policies` | 用户中心 | +| 项目列表和详情 | `/projects`、`/projects/:id` | 项目列表、创建、归档、资源概览 | `/api/projects` 系列 | `projects`、`quota_usage` | 租户 | +| 项目成员 | `/projects/:id/members` | 成员列表、添加成员、角色选择 | `/api/projects/{id}/members` 系列 | `project_members` | 项目 | +| 资源授权 | `/projects/:id/permissions`、资源详情弹窗 | ACL 表格、用户/角色授权 | `/api/resources/{type}/{id}/acl` | `resource_acl` | 项目、资源表 | + +### 16.3 数据集和数据处理 + +| 工作包 | 页面/路由 | 前端开发 | 后端开发 | DB 关联 | 依赖 | +| --- | --- | --- | --- | --- | --- | +| 数据集列表 | `/dataset` | 列表、搜索、项目过滤、下载、删除审批入口 | `GET/DELETE /api/dataset-manage` | `datasets`、`resource_acl`、`approval_instances` | 项目上下文 | +| 数据集创建/上传 | `/dataset/create`、`/dataset/:id/edit` | 表单、分片上传、离线导入弹窗 | `POST /api/dataset-manage`、`POST /api/files/upload-session`、`POST /api/import/local-dataset` | `datasets`、`dataset_files`、`storage_objects`、`file_upload_sessions`、`local_import_jobs` | 文件网关 | +| 数据集预览/版本 | `/dataset/:id/preview` | 文件列表、内容预览、版本栏、在线编辑 | `/api/dataset-manage/preview`、`/versions` 系列 | `dataset_file_versions`、`dataset_records` | 数据集上传 | +| 数据处理列表 | `/data-process` | 任务列表、状态、输出数据集跳转 | `/api/data-process` 系列 | `data_process_tasks` | 数据集 | +| 数据处理创建向导 | `/data-process/create` | 任务配置、模型选择、源文件、预览切片、生成、结果编辑、发布 | `/api/data-process/{id}/source-files`、`preview/build`、`generate`、`publish` | `data_process_source_files`、`data_process_preview_items`、`data_process_results` | 数据集、模型、文件网关 | +| 数据处理详情 | `/data-process/:id` | 运行统计、失败原因、结果表格 | `GET /api/data-process/{id}`、`results`、`events` | `data_process_tasks`、`data_process_results` | 数据处理任务 | +| 数据转换 | `/data-convert` | 文件上传、转换状态、下载 | `/api/data-convert/jobs` 系列 | `data_convert_jobs`、`storage_objects` | 文件网关 | + +### 16.4 模型管理和训练 + +| 工作包 | 页面/路由 | 前端开发 | 后端开发 | DB 关联 | 依赖 | +| --- | --- | --- | --- | --- | --- | +| 模型列表 | `/model-manage` | 列表、用途标签、授权、导出、删除审批 | `/api/model-manage` 系列 | `models`、`trained_models`、`resource_acl` | 项目上下文 | +| 模型创建/离线导入 | `/model-manage/create`、`/model-manage/:id/edit` | 本地/API 模型表单、离线导入 | `POST/PUT /api/model-manage`、`POST /api/import/local-model` | `models`、`storage_objects`、`local_import_jobs` | 文件网关 | +| 权重合并 | `/model-manage/merge` | 选择训练产物、合并状态 | `POST /api/model-manage/merge`、`GET /api/compute/jobs/{id}` | `trained_models`、`compute_jobs` | 算力平台 | +| 微调列表 | `/fine-tune` | 任务列表、停止、删除、日志入口 | `/api/fine-tune` 系列 | `fine_tune_tasks` | 模型、数据集 | +| 微调创建 | `/fine-tune/create` | 训练参数、GPU 选择、命令预览、审批提示 | `POST /api/fine-tune`、`POST /api/fine-tune/start`、`GET /api/compute/gpus` | `fine_tune_tasks`、`compute_jobs`、`gpu_allocations` | 算力平台、审批 | +| 训练日志和 checkpoint | `/training-log/:id` | 日志 tail、指标曲线、checkpoint、恢复/重试 | `GET /api/fine-tune/{id}/overview`、`checkpoints`、`resume`、`retry` | `fine_tune_metrics`、`fine_tune_checkpoints` | 训练任务 | +| 训练引擎管理 | `/training-engines` | 引擎列表、schema、健康检查 | `/api/training-engines` 系列 | `training_engines` | 算力 Agent | + +### 16.5 评测、推理和发布 + +| 工作包 | 页面/路由 | 前端开发 | 后端开发 | DB 关联 | 依赖 | +| --- | --- | --- | --- | --- | --- | +| 评测列表 | `/model-eval` | 列表、分数、状态、删除 | `/api/model-eval` 系列 | `eval_tasks` | 模型、数据集 | +| 评测创建 | `/model-eval/create` | 模型/数据集/维度/GPU 选择、指标配置 | `POST /api/model-eval/start`、`GET /api/dimension` | `eval_tasks`、`eval_dimensions`、`compute_jobs` | 算力平台 | +| 评测详情 | `/model-eval/:id` | 综合评价、维度汇总、样本结果、人工复核预留 | `GET /api/model-eval/{id}`、`events` | `eval_sample_results`、`eval_dimension_summaries` | 评测任务 | +| 评测维度 | `/model-eval/dimension/:id/edit` | 维度表单、Prompt 编辑 | `/api/dimension` 系列 | `eval_dimensions` | 模型管理 | +| 推理列表/创建 | `/model-inference`、`/model-inference/create` | 任务列表、模型选择、GPU 选择、加载状态 | `/api/model-compare`、`load/unload` | `inference_tasks`、`inference_task_models` | 算力平台 | +| 推理对话 | `/model-inference/chat/:id` | 单模型流式对话、历史消息 | `stream-chat`、`chat-with-port` | `chat_sessions`、`chat_messages` | 推理服务 | +| 模型对比 | `/model-compare/chat/:id`、`/model-compare/result` | 多模型对话、结果对比 | `/api/model-chat/*` | `chat_sessions`、`chat_messages` | 推理任务 | +| 模型服务治理 | `/model-services`、`/model-services/:id` | 测试/生产服务、发布申请、下线、调用统计 | `POST /api/approvals`、`GET /api/usage/summary` | `model_services`、`approval_instances` | 审批、算力平台 | + +### 16.6 审批、审计、算力和存储运维 + +| 工作包 | 页面/路由 | 前端开发 | 后端开发 | DB 关联 | 依赖 | +| --- | --- | --- | --- | --- | --- | +| 审批中心 | `/approvals`、`/approvals/pending`、`/approvals/mine`、`/approvals/:id` | 审批列表、详情、通过/驳回/撤回 | `/api/approvals` 系列 | `approval_instances`、`approval_steps` | 用户、项目 | +| 审批模板 | `/approval-settings` | 动作策略、审批人规则、超时配置 | `/api/approval-templates` 系列 | `approval_templates` | 租户/项目 | +| 算力资源中心 | `/compute`、`/compute/gpus`、`/compute/queue`、`/compute/nodes` | GPU 卡片、节点状态、队列、优先级 | `/api/compute/*`、`/compute/*` 内部接口 | `compute_nodes`、`gpu_devices`、`compute_jobs`、`gpu_allocations` | Compute API/Agent | +| 存储管理 | `/storage` | 磁盘占用、大文件、临时文件、checkpoint 清理 | `GET /api/quotas/usage`、`GET/PUT /api/retention-policies` | `storage_nodes`、`storage_objects`、`cleanup_jobs`、`retention_policies` | 文件网关 | +| 审计中心 | `/audit-logs`、`/login-logs`、`/download-logs` | 筛选、详情、导出 | `GET /api/audit-logs`、`GET /api/login-logs`、`GET /api/download-logs` | `audit_logs`、`login_sessions` | 审计中间件 | + +### 16.7 页面开发优先级建议 + +| 优先级 | 页面模块 | 原因 | +| --- | --- | --- | +| P0 | 登录、主布局、用户中心、租户、项目、项目成员、资源授权 | 所有资源隔离和接口过滤依赖这些基础能力 | +| P0 | 模型、数据集、文件上传、算力资源、微调创建、训练日志 | 训练主链路必须先跑通 | +| P0 | 审批中心基础能力 | 删除、发布、导出、停止任务等高风险动作依赖审批 | +| P1 | 数据处理、评测、推理、模型服务治理 | 形成训练前后闭环 | +| P1 | 存储管理、checkpoint、审计中心 | 企业治理和运维收口 | +| P2 | 训练引擎管理、外部 API、OIDC/LDAP、成本核算、人工评测 | 扩展能力,第一版可预留 | From ba4059fe3ba2a61c9469010b08f7b73d63970f28 Mon Sep 17 00:00:00 2001 From: wuyongtao Date: Thu, 16 Jul 2026 13:47:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=9E=B6=E6=9E=84=E3=80=81=E8=AE=A1=E7=AE=97=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=8F=8A=E9=83=A8=E7=BD=B2=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 184 +++++------ backend/.env.example | 9 + backend/README.md | 65 ++++ backend/app/__init__.py | 1 + backend/app/api/__init__.py | 1 + backend/app/api/v1/__init__.py | 1 + backend/app/api/v1/endpoints/__init__.py | 1 + backend/app/api/v1/endpoints/health.py | 12 + backend/app/api/v1/router.py | 6 + backend/app/core/__init__.py | 1 + backend/app/core/config.py | 28 ++ backend/app/core/logging.py | 253 +++++++++++++++ backend/app/db/__init__.py | 1 + backend/app/db/session.py | 4 + backend/app/main.py | 18 ++ backend/app/modules/README.md | 15 + backend/app/modules/approval/__init__.py | 1 + backend/app/modules/audit/__init__.py | 1 + backend/app/modules/auth/__init__.py | 1 + .../app/modules/compute_gateway/__init__.py | 1 + backend/app/modules/data_process/__init__.py | 1 + backend/app/modules/dataset/__init__.py | 1 + .../app/modules/engine_registry/__init__.py | 1 + backend/app/modules/eval/__init__.py | 1 + backend/app/modules/file_gateway/__init__.py | 1 + backend/app/modules/fine_tune/__init__.py | 1 + backend/app/modules/inference/__init__.py | 1 + backend/app/modules/model/__init__.py | 1 + backend/app/modules/project/__init__.py | 1 + backend/app/modules/retention/__init__.py | 1 + backend/app/modules/system/__init__.py | 1 + backend/app/modules/tenant/__init__.py | 1 + backend/app/schemas/__init__.py | 1 + backend/app/services/__init__.py | 1 + backend/app/workers/__init__.py | 1 + backend/pyproject.toml | 32 ++ backend/requirements.txt | 12 + compute/README.md | 24 ++ compute/agent/__init__.py | 1 + compute/api/__init__.py | 1 + compute/engines/__init__.py | 1 + compute/engines/llama_factory/__init__.py | 1 + compute/file_gateway/__init__.py | 1 + compute/tests/__init__.py | 1 + docs/backend-logging.md | 109 +++++++ docs/deployment-plan.md | 305 ++++++++++++++++++ 46 files changed, 1002 insertions(+), 105 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/README.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/v1/__init__.py create mode 100644 backend/app/api/v1/endpoints/__init__.py create mode 100644 backend/app/api/v1/endpoints/health.py create mode 100644 backend/app/api/v1/router.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/logging.py create mode 100644 backend/app/db/__init__.py create mode 100644 backend/app/db/session.py create mode 100644 backend/app/main.py create mode 100644 backend/app/modules/README.md create mode 100644 backend/app/modules/approval/__init__.py create mode 100644 backend/app/modules/audit/__init__.py create mode 100644 backend/app/modules/auth/__init__.py create mode 100644 backend/app/modules/compute_gateway/__init__.py create mode 100644 backend/app/modules/data_process/__init__.py create mode 100644 backend/app/modules/dataset/__init__.py create mode 100644 backend/app/modules/engine_registry/__init__.py create mode 100644 backend/app/modules/eval/__init__.py create mode 100644 backend/app/modules/file_gateway/__init__.py create mode 100644 backend/app/modules/fine_tune/__init__.py create mode 100644 backend/app/modules/inference/__init__.py create mode 100644 backend/app/modules/model/__init__.py create mode 100644 backend/app/modules/project/__init__.py create mode 100644 backend/app/modules/retention/__init__.py create mode 100644 backend/app/modules/system/__init__.py create mode 100644 backend/app/modules/tenant/__init__.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/workers/__init__.py create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements.txt create mode 100644 compute/README.md create mode 100644 compute/agent/__init__.py create mode 100644 compute/api/__init__.py create mode 100644 compute/engines/__init__.py create mode 100644 compute/engines/llama_factory/__init__.py create mode 100644 compute/file_gateway/__init__.py create mode 100644 compute/tests/__init__.py create mode 100644 docs/backend-logging.md create mode 100644 docs/deployment-plan.md diff --git a/README.md b/README.md index 31084b3..f27a031 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,107 @@ -# YG_FT +# YG_FT 模型微调平台 -远光微调平台 - 面向大语言模型的微调、评测、推理与对比一体化前端。 +YG_FT 是一个面向企业治理场景的完整模型微调平台,覆盖用户中心、多租户、项目隔离、数据集管理、模型管理、训练任务、评测、推理、审批流、审计留存、算力调度和训练引擎适配。当前前端已存在基础页面,后端与算力平台已按多人协作开发方式建立工程骨架。 -## 技术栈 +## 总体架构 -| 类别 | 技术 | 版本 | -|------|------|------| -| 框架 | Vue 3 | ^3.5.13 | -| 语言 | TypeScript | ~5.7.2 | -| 构建工具 | Vite | ^6.0.7 | -| 路由 | Vue Router | ^4.5.0 | -| 状态管理 | Pinia | ^2.3.0 | -| UI 组件库 | Element Plus | ^2.9.1 | -| HTTP 客户端 | axios | ^1.7.9 | -| 图表 | ECharts / vue-echarts | ^6.1.0 / ^8.0.1 | -| Markdown | marked + DOMPurify | ^15.0.5 / ^3.2.3 | -| 编辑器 | md-editor-v3 | ^5.1.4 | -| 工具集 | @vueuse/core | ^11.3.0 | -| 样式 | Sass | ^1.83.0 | +```text +YG_FT/ + frontend/ # 前端应用,承载训练平台控制台页面 + backend/ # FastAPI 应用平台后端 + app/ + api/v1/ # 对前端暴露的 REST API + core/ # 配置、日志、中间件、权限等基础能力 + db/ # 数据库连接、迁移、事务工具 + modules/ # 业务模块目录 + schemas/ # Pydantic 入参/出参模型 + services/ # 跨模块应用服务 + workers/ # 后台任务入口 + requirements.txt # 后端 Python 第三方依赖 + compute/ # 算力平台与训练框架适配层 + api/ # 内部 Compute API + agent/ # 单机多 GPU 调度与进程管理 + engines/llama_factory/ # LLaMA-Factory 适配器 + file_gateway/ # 本地文件上传、下载、导入、产物管理 + docs/ # 需求、接口、数据库、开发计划和部署文档 + docker/ # Nginx 等容器化配置 +``` -**项目版本**:1.0.0 +## 平台分层 -## 环境要求 +| 层级 | 职责 | 主要目录 | +| --- | --- | --- | +| 前端控制台 | 用户操作入口、任务看板、项目/模型/数据集/训练/审批/审计页面 | `frontend/` | +| 应用平台后端 | 用户中心、多租户、RBAC/ABAC、项目隔离、元数据、审批流、审计、API 编排 | `backend/` | +| 算力平台 | GPU 发现、资源锁定、训练进程管理、日志采集、产物归档、任务状态回传 | `compute/` | +| 训练引擎 | 当前固定接入 LLaMA-Factory,预留其他训练平台适配标准 | `compute/engines/` | +| 数据层 | PostgreSQL、Redis、本地文件存储、日志归档 | `docs/postgres-schema.sql` | -- **Node.js** >= 18(推荐 20 LTS) -- **npm** >= 9 -- 后端服务运行于 `http://localhost:7861`(前端通过代理转发,见下文) +## 关键能力 -## 快速开始 +- 多租户:租户级数据隔离、租户配置、租户成员和角色。 +- 权限控制:支持项目、模型、数据集级隔离,后续可扩展到字段级和操作级策略。 +- 审批流:覆盖数据集发布、模型发布、训练资源申请、推理服务上线等企业流程。 +- 审计留存:操作审计、安全审计、审批审计、任务审计,支持留存周期策略。 +- 训练任务:训练参数管理、单机多 GPU 调度、任务状态同步、训练日志、产物管理。 +- 引擎适配:默认 LLaMA-Factory,预留统一 Engine Adapter 接口接入其他微调框架。 +- 文件存储:当前使用本地磁盘,按租户/项目/数据集/任务分区。 +- 日志采集:后端 JSON Lines 日志,主日志和错误日志拆分,便于 ELK/日志平台采集。 -### 1. 安装依赖 +## 后端启动 ```bash -cd frontend -npm install +cd backend +python -m venv .venv +.venv\Scripts\activate +pip install -r requirements.txt +uvicorn app.main:app --reload ``` -### 2. 启动开发服务器 +默认健康检查: -```bash -npm run dev +```text +GET /api/v1/health ``` -开发服务器默认运行在 `http://localhost:6801`。 +## 日志 -### 3. 构建生产包 +后端日志模块位于 `backend/app/core/logging.py`,说明文档见: -```bash -npm run build # 类型检查 + 生产构建,产物输出到 dist/ -npm run preview # 本地预览构建产物 +- `docs/backend-logging.md` + +默认输出: + +```text +logs/backend-YYYY-MM-DD.log +logs/error-YYYY-MM-DD.log ``` -### 4. 类型检查 +日志格式为 JSON Lines,单个文件不超过 20MB,只保留最近 10 天。 -```bash -npm run type-check -``` +## 主要文档 -## 测试 +- `docs/platform-architecture-requirements.md`:平台需求、功能模块、页面补全建议。 +- `docs/backend-api-design.md`:FastAPI 接口分组、参数定义、权限说明。 +- `docs/postgres-schema.sql`:PostgreSQL 数据库脚本,包含权限、用户中心、多租户、审批、审计等模型。 +- `docs/system-development-plan.md`:多人协作开发计划,按前端、后端、DB、部署拆分。 +- `docs/backend-logging.md`:后端日志模块使用说明。 +- `docs/deployment-plan.md`:后期部署方案,覆盖单机算力服务器部署与应用/算力分离部署。 -内置基于 Playwright 的 UI 回归脚本,首次运行前需安装浏览器: +## 部署模式 -```bash -npx playwright install chromium -``` +平台支持两种主要部署模式: -执行已注册的回归脚本: +1. 所有服务部署在算力服务器:适合 PoC、内网试点、小团队单机多 GPU 使用。 +2. 应用服务和算力/训练服务独立部署:适合企业生产环境,应用平台部署在业务服务区,算力平台和 LLaMA-Factory 部署在 GPU 服务器。 -```bash -npm run test:data-process-wizard # 数据处理向导 -npm run test:model-manage # 模型管理 -npm run test:training-log-layout # 训练日志布局 -npm run test:page-surface # 页面表层级 -``` +生产环境建议采用第二种模式。算力平台与训练框架应部署在 GPU 算力服务器上,应用平台不直接控制 GPU 进程,而是通过内部 Compute API 调度训练任务。 -其余脚本可直接运行: +详细方案见 `docs/deployment-plan.md`。 -```bash -node scripts/regression-back-navigation.mjs # 返回导航 -node scripts/regression-fine-tune-create-ui.mjs # 调优创建 UI -``` +## 后续开发原则 -> 回归脚本默认连接 `http://localhost:6801`,需先启动开发服务器。 - -## 目录结构 - -``` -YG-FT/ -├── frontend/ # 前端工程(Vue 3 SPA) -│ ├── src/ -│ │ ├── api/ # axios 封装 + 各业务模块 API -│ │ ├── components/ # 公共组件 -│ │ ├── composables/ # 组合式函数 -│ │ ├── constants/ # 常量与映射表 -│ │ ├── layouts/ # 主布局 -│ │ ├── mock/ # Mock 数据与适配器 -│ │ ├── plugins/ # 第三方插件注册 -│ │ ├── router/ # 路由配置 + 登录守卫 -│ │ ├── stores/ # Pinia 状态 -│ │ ├── styles/ # 全局样式 -│ │ ├── types/ # TypeScript 类型定义 -│ │ └── views/ # 业务页面 -│ ├── scripts/ # UI 回归测试脚本 -│ ├── public/ # 静态资源 -│ └── vite.config.ts # Vite 构建与代理配置 -├── docs/ # 设计文档与视觉走查记录 -└── design-qa.md # 视觉走查汇总 -``` - -## 端口与代理 - -| 服务 | 地址 | -|------|------| -| 前端开发服务器 | `http://localhost:6801` | -| 后端 API | `http://localhost:7861` | - -前端统一使用 `/api` 相对路径发请求,由 Vite 开发代理转发到后端 `http://localhost:7861`(配置见 `frontend/vite.config.ts`)。 - -## 业务模块 - -| 模块 | 说明 | -|------|------| -| 登录 | 用户登录鉴权 | -| 模型调优 | 微调任务创建与管理 | -| 模型评测 | 评测任务与评测维度配置 | -| 模型推理 | 在线推理对话 | -| 模型对比 | 多模型对话与结果对比 | -| 模型管理 | 模型 CRUD 与权重合并 | -| 数据集 | 数据集管理与预览 | -| 数据处理 | 数据处理任务向导 | -| 工具 | 辅助工具集 | -| 系统 | 硬件监控、日志、训练日志 | +- 接口实现优先遵循 `docs/backend-api-design.md`。 +- 数据库实现优先遵循 `docs/postgres-schema.sql`,后续通过 Alembic 迁移管理变更。 +- 前端页面与后端接口、数据库表之间的映射以文档中的“对应页面/功能模块”为准。 +- 训练引擎适配必须通过 `compute/engines/` 下的标准接口,不在应用平台后端直接拼接训练命令。 +- 敏感信息不得写入日志,生产环境密钥通过环境变量或密钥管理系统注入。 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..436987d --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,9 @@ +APP_NAME=YG Fine-Tune Platform API +APP_ENV=local +API_PREFIX=/api +LOG_LEVEL=INFO +LOG_DIR=./logs +LOG_FILE_PREFIX=backend +LOG_ERROR_FILE_PREFIX=error +LOG_MAX_BYTES=20971520 +LOG_RETENTION_DAYS=10 diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..0b10132 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,65 @@ +# Backend Service + +后端工程使用 FastAPI,定位为模型微调平台的应用平台服务,负责用户中心、多租户、权限隔离、项目、数据集、模型、训练任务、审批、审计和算力平台编排。 + +## 目录结构 + +```text +backend/ + app/ + main.py # FastAPI 应用入口 + api/v1/ # 对前端暴露的 API 路由 + core/ # 配置、日志、中间件、权限等基础能力 + db/ # 数据库连接、迁移集成、事务工具 + modules/ # 业务模块 + auth/ + tenant/ + project/ + model/ + dataset/ + data_process/ + fine_tune/ + eval/ + inference/ + approval/ + audit/ + compute_gateway/ + file_gateway/ + engine_registry/ + retention/ + system/ + schemas/ # Pydantic 入参/出参模型 + services/ # 跨模块应用服务 + workers/ # 后台任务入口 + requirements.txt # 后端第三方依赖 + logs/ # 本地开发日志目录,生产环境建议挂载到独立日志盘 +``` + +## 本地启动 + +```bash +cd backend +python -m venv .venv +.venv\Scripts\activate +pip install -r requirements.txt +uvicorn app.main:app --reload +``` + +健康检查: + +```text +GET /api/v1/health +``` + +## 日志 + +日志模块位于 `app/core/logging.py`,使用说明见 `../docs/backend-logging.md`。 + +默认日志文件: + +```text +logs/backend-YYYY-MM-DD.log +logs/error-YYYY-MM-DD.log +``` + +文件日志为 JSON Lines 格式,单个文件不超过 20MB,只保存最近 10 天,错误日志按 `ERROR` 级别独立拆分,便于 ELK/日志平台采集。 diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..18b665e --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +"""Application package.""" diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..dff53e5 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ +"""API package.""" diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py new file mode 100644 index 0000000..6d0f325 --- /dev/null +++ b/backend/app/api/v1/__init__.py @@ -0,0 +1 @@ +"""Versioned API package.""" diff --git a/backend/app/api/v1/endpoints/__init__.py b/backend/app/api/v1/endpoints/__init__.py new file mode 100644 index 0000000..1bdb261 --- /dev/null +++ b/backend/app/api/v1/endpoints/__init__.py @@ -0,0 +1 @@ +"""API endpoint modules.""" diff --git a/backend/app/api/v1/endpoints/health.py b/backend/app/api/v1/endpoints/health.py new file mode 100644 index 0000000..72b27fe --- /dev/null +++ b/backend/app/api/v1/endpoints/health.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +from app.core.logging import get_logger + +router = APIRouter() +logger = get_logger(__name__) + + +@router.get("/health") +async def health_check() -> dict[str, str]: + logger.info("health check requested") + return {"status": "ok"} diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py new file mode 100644 index 0000000..7d5f9d2 --- /dev/null +++ b/backend/app/api/v1/router.py @@ -0,0 +1,6 @@ +from fastapi import APIRouter + +from app.api.v1.endpoints.health import router as health_router + +api_router = APIRouter() +api_router.include_router(health_router, tags=["health"]) diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..5fd6e27 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1 @@ +"""Core infrastructure modules.""" diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..f5457ba --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from functools import lru_cache +import os + + +def _int_env(name: str, default: int) -> int: + raw = os.getenv(name) + if raw is None or raw == "": + return default + return int(raw) + + +@dataclass(frozen=True) +class Settings: + app_name: str = os.getenv("APP_NAME", "YG Fine-Tune Platform API") + app_env: str = os.getenv("APP_ENV", "local") + api_prefix: str = os.getenv("API_PREFIX", "/api") + log_level: str = os.getenv("LOG_LEVEL", "INFO") + log_dir: str = os.getenv("LOG_DIR", "./logs") + log_file_prefix: str = os.getenv("LOG_FILE_PREFIX", "backend") + log_error_file_prefix: str = os.getenv("LOG_ERROR_FILE_PREFIX", "error") + log_max_bytes: int = _int_env("LOG_MAX_BYTES", 20 * 1024 * 1024) + log_retention_days: int = _int_env("LOG_RETENTION_DAYS", 10) + + +@lru_cache +def get_settings() -> Settings: + return Settings() diff --git a/backend/app/core/logging.py b/backend/app/core/logging.py new file mode 100644 index 0000000..6332907 --- /dev/null +++ b/backend/app/core/logging.py @@ -0,0 +1,253 @@ +from __future__ import annotations + +from contextvars import ContextVar +from datetime import date, datetime, timedelta +import json +import logging +from logging import Handler, LogRecord +from pathlib import Path +import re +import time +from typing import Any +from uuid import uuid4 + +from fastapi import FastAPI, Request + +from app.core.config import Settings, get_settings + +request_id_var: ContextVar[str] = ContextVar("request_id", default="-") + + +class RequestIdFilter(logging.Filter): + def filter(self, record: LogRecord) -> bool: + record.request_id = request_id_var.get() + return True + + +class JsonLogFormatter(logging.Formatter): + """Format one JSON object per line for ELK/Filebeat collection.""" + + def format(self, record: LogRecord) -> str: + payload: dict[str, Any] = { + "@timestamp": datetime.fromtimestamp(record.created).astimezone().isoformat( + timespec="milliseconds" + ), + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + "module": record.module, + "function": record.funcName, + "file": record.pathname, + "line": record.lineno, + "process": record.process, + "thread": record.thread, + "thread_name": record.threadName, + "request_id": getattr(record, "request_id", "-"), + } + if record.exc_info: + payload["exception"] = self.formatException(record.exc_info) + if record.stack_info: + payload["stack"] = self.formatStack(record.stack_info) + return json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + + +class DateSizeRotatingFileHandler(Handler): + """Rotate log files by date and size while keeping date in every file name.""" + + def __init__( + self, + log_dir: str | Path, + file_prefix: str, + max_bytes: int, + retention_days: int, + encoding: str = "utf-8", + ) -> None: + super().__init__() + self.log_dir = Path(log_dir) + self.file_prefix = file_prefix + self.max_bytes = max_bytes + self.retention_days = retention_days + self.encoding = encoding + self._current_date: date | None = None + self._stream: Any | None = None + self._current_path: Path | None = None + self.log_dir.mkdir(parents=True, exist_ok=True) + + def emit(self, record: LogRecord) -> None: + try: + message = self.format(record) + self.terminator + encoded_size = len(message.encode(self.encoding)) + self._ensure_stream() + if self._should_rotate(encoded_size): + self._rotate_by_size() + self._ensure_stream(force=True) + self._stream.write(message) + self.flush() + self._cleanup_expired_files() + except Exception: + self.handleError(record) + + @property + def terminator(self) -> str: + return "\n" + + def flush(self) -> None: + if self._stream and not self._stream.closed: + self._stream.flush() + + def close(self) -> None: + try: + if self._stream and not self._stream.closed: + self._stream.close() + finally: + self._stream = None + super().close() + + def _dated_path(self, target_date: date) -> Path: + return self.log_dir / f"{self.file_prefix}-{target_date.isoformat()}.log" + + def _ensure_stream(self, force: bool = False) -> None: + today = date.today() + if not force and self._stream and self._current_date == today: + return + + if self._stream and not self._stream.closed: + self._stream.close() + + self._current_date = today + self._current_path = self._dated_path(today) + self._stream = self._current_path.open("a", encoding=self.encoding) + + def _should_rotate(self, incoming_size: int) -> bool: + if not self._current_path or self.max_bytes <= 0: + return False + if not self._current_path.exists(): + return False + return self._current_path.stat().st_size + incoming_size > self.max_bytes + + def _rotate_by_size(self) -> None: + if not self._current_path or not self._current_path.exists(): + return + + if self._stream and not self._stream.closed: + self._stream.close() + self._stream = None + + stem = self._current_path.stem + suffix = self._current_path.suffix + index = 1 + while True: + rotated_path = self.log_dir / f"{stem}.{index}{suffix}" + if not rotated_path.exists(): + self._current_path.rename(rotated_path) + return + index += 1 + + def _cleanup_expired_files(self) -> None: + if self.retention_days <= 0: + return + + cutoff = date.today() - timedelta(days=self.retention_days - 1) + pattern = re.compile( + rf"^{re.escape(self.file_prefix)}-(\d{{4}}-\d{{2}}-\d{{2}})(?:\.\d+)?\.log$" + ) + for path in self.log_dir.glob(f"{self.file_prefix}-*.log"): + match = pattern.match(path.name) + if not match: + continue + file_date = datetime.strptime(match.group(1), "%Y-%m-%d").date() + if file_date < cutoff: + path.unlink(missing_ok=True) + + +def configure_logging(settings: Settings | None = None) -> None: + settings = settings or get_settings() + + root_logger = logging.getLogger() + root_logger.handlers.clear() + root_logger.setLevel(settings.log_level.upper()) + + console_formatter = logging.Formatter( + fmt=( + "%(asctime)s | %(levelname)s | pid=%(process)d | %(threadName)s | " + "request_id=%(request_id)s | %(name)s | %(pathname)s:%(lineno)d | %(message)s" + ), + datefmt="%Y-%m-%d %H:%M:%S", + ) + json_formatter = JsonLogFormatter() + request_filter = RequestIdFilter() + + console_handler = logging.StreamHandler() + console_handler.setFormatter(console_formatter) + console_handler.addFilter(request_filter) + + file_handler = DateSizeRotatingFileHandler( + log_dir=settings.log_dir, + file_prefix=settings.log_file_prefix, + max_bytes=settings.log_max_bytes, + retention_days=settings.log_retention_days, + ) + file_handler.setFormatter(json_formatter) + file_handler.addFilter(request_filter) + + error_file_handler = DateSizeRotatingFileHandler( + log_dir=settings.log_dir, + file_prefix=settings.log_error_file_prefix, + max_bytes=settings.log_max_bytes, + retention_days=settings.log_retention_days, + ) + error_file_handler.setLevel(logging.ERROR) + error_file_handler.setFormatter(json_formatter) + error_file_handler.addFilter(request_filter) + + root_logger.addHandler(console_handler) + root_logger.addHandler(file_handler) + root_logger.addHandler(error_file_handler) + + for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"): + logger = logging.getLogger(logger_name) + logger.handlers.clear() + logger.propagate = True + + +def get_logger(name: str) -> logging.Logger: + return logging.getLogger(name) + + +def set_request_id(request_id: str) -> None: + request_id_var.set(request_id) + + +def setup_request_logging(app: FastAPI) -> None: + logger = get_logger("app.access") + + @app.middleware("http") + async def request_logging_middleware(request: Request, call_next): # type: ignore[no-untyped-def] + request_id = request.headers.get("X-Request-ID") or str(uuid4()) + token = request_id_var.set(request_id) + started_at = time.perf_counter() + try: + response = await call_next(request) + elapsed_ms = (time.perf_counter() - started_at) * 1000 + logger.info( + "request completed method=%s path=%s status_code=%s duration_ms=%.2f client=%s", + request.method, + request.url.path, + response.status_code, + elapsed_ms, + request.client.host if request.client else "-", + ) + response.headers["X-Request-ID"] = request_id + return response + except Exception: + elapsed_ms = (time.perf_counter() - started_at) * 1000 + logger.exception( + "request failed method=%s path=%s duration_ms=%.2f client=%s", + request.method, + request.url.path, + elapsed_ms, + request.client.host if request.client else "-", + ) + raise + finally: + request_id_var.reset(token) diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py new file mode 100644 index 0000000..2a29512 --- /dev/null +++ b/backend/app/db/__init__.py @@ -0,0 +1 @@ +"""Database infrastructure package.""" diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 0000000..5542d81 --- /dev/null +++ b/backend/app/db/session.py @@ -0,0 +1,4 @@ +"""Database session factory placeholder. + +Implement SQLAlchemy/SQLModel session management here when database development starts. +""" diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..a9978cf --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI + +from app.api.v1.router import api_router +from app.core.config import get_settings +from app.core.logging import configure_logging, setup_request_logging + + +def create_app() -> FastAPI: + settings = get_settings() + configure_logging(settings) + + app = FastAPI(title=settings.app_name) + setup_request_logging(app) + app.include_router(api_router, prefix=settings.api_prefix) + return app + + +app = create_app() diff --git a/backend/app/modules/README.md b/backend/app/modules/README.md new file mode 100644 index 0000000..880ff37 --- /dev/null +++ b/backend/app/modules/README.md @@ -0,0 +1,15 @@ +# Backend Module Convention + +每个业务模块建议保持一致结构: + +```text +module_name/ + __init__.py + router.py # FastAPI router + schemas.py # Pydantic request/response models + service.py # Business orchestration + repository.py # Database access + permissions.py # Optional resource permission checks +``` + +模块边界以 `docs/system-development-plan.md` 的页面模块开发工作包为准。 diff --git a/backend/app/modules/approval/__init__.py b/backend/app/modules/approval/__init__.py new file mode 100644 index 0000000..17daab2 --- /dev/null +++ b/backend/app/modules/approval/__init__.py @@ -0,0 +1 @@ +"""Approval workflow module.""" diff --git a/backend/app/modules/audit/__init__.py b/backend/app/modules/audit/__init__.py new file mode 100644 index 0000000..3202b1e --- /dev/null +++ b/backend/app/modules/audit/__init__.py @@ -0,0 +1 @@ +"""Audit log module.""" diff --git a/backend/app/modules/auth/__init__.py b/backend/app/modules/auth/__init__.py new file mode 100644 index 0000000..53ede25 --- /dev/null +++ b/backend/app/modules/auth/__init__.py @@ -0,0 +1 @@ +"""Authentication and user session module.""" diff --git a/backend/app/modules/compute_gateway/__init__.py b/backend/app/modules/compute_gateway/__init__.py new file mode 100644 index 0000000..70436ed --- /dev/null +++ b/backend/app/modules/compute_gateway/__init__.py @@ -0,0 +1 @@ +"""Application-side compute platform gateway module.""" diff --git a/backend/app/modules/data_process/__init__.py b/backend/app/modules/data_process/__init__.py new file mode 100644 index 0000000..3da002b --- /dev/null +++ b/backend/app/modules/data_process/__init__.py @@ -0,0 +1 @@ +"""Data processing module.""" diff --git a/backend/app/modules/dataset/__init__.py b/backend/app/modules/dataset/__init__.py new file mode 100644 index 0000000..9fbd064 --- /dev/null +++ b/backend/app/modules/dataset/__init__.py @@ -0,0 +1 @@ +"""Dataset management module.""" diff --git a/backend/app/modules/engine_registry/__init__.py b/backend/app/modules/engine_registry/__init__.py new file mode 100644 index 0000000..e5237fe --- /dev/null +++ b/backend/app/modules/engine_registry/__init__.py @@ -0,0 +1 @@ +"""Training engine registry module.""" diff --git a/backend/app/modules/eval/__init__.py b/backend/app/modules/eval/__init__.py new file mode 100644 index 0000000..c7f7436 --- /dev/null +++ b/backend/app/modules/eval/__init__.py @@ -0,0 +1 @@ +"""Evaluation module.""" diff --git a/backend/app/modules/file_gateway/__init__.py b/backend/app/modules/file_gateway/__init__.py new file mode 100644 index 0000000..e5e295c --- /dev/null +++ b/backend/app/modules/file_gateway/__init__.py @@ -0,0 +1 @@ +"""Application-side file gateway module.""" diff --git a/backend/app/modules/fine_tune/__init__.py b/backend/app/modules/fine_tune/__init__.py new file mode 100644 index 0000000..3675f07 --- /dev/null +++ b/backend/app/modules/fine_tune/__init__.py @@ -0,0 +1 @@ +"""Fine-tuning task module.""" diff --git a/backend/app/modules/inference/__init__.py b/backend/app/modules/inference/__init__.py new file mode 100644 index 0000000..be36430 --- /dev/null +++ b/backend/app/modules/inference/__init__.py @@ -0,0 +1 @@ +"""Inference and compare module.""" diff --git a/backend/app/modules/model/__init__.py b/backend/app/modules/model/__init__.py new file mode 100644 index 0000000..bcace02 --- /dev/null +++ b/backend/app/modules/model/__init__.py @@ -0,0 +1 @@ +"""Model registry module.""" diff --git a/backend/app/modules/project/__init__.py b/backend/app/modules/project/__init__.py new file mode 100644 index 0000000..ddcbb9a --- /dev/null +++ b/backend/app/modules/project/__init__.py @@ -0,0 +1 @@ +"""Project workspace and member module.""" diff --git a/backend/app/modules/retention/__init__.py b/backend/app/modules/retention/__init__.py new file mode 100644 index 0000000..22eeffc --- /dev/null +++ b/backend/app/modules/retention/__init__.py @@ -0,0 +1 @@ +"""Retention policy and cleanup module.""" diff --git a/backend/app/modules/system/__init__.py b/backend/app/modules/system/__init__.py new file mode 100644 index 0000000..3f76f43 --- /dev/null +++ b/backend/app/modules/system/__init__.py @@ -0,0 +1 @@ +"""System health, metrics and logs module.""" diff --git a/backend/app/modules/tenant/__init__.py b/backend/app/modules/tenant/__init__.py new file mode 100644 index 0000000..06c6143 --- /dev/null +++ b/backend/app/modules/tenant/__init__.py @@ -0,0 +1 @@ +"""Tenant management module.""" diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..8bd66a3 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1 @@ +"""Shared schemas package.""" diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..84e9714 --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1 @@ +"""Cross-module services package.""" diff --git a/backend/app/workers/__init__.py b/backend/app/workers/__init__.py new file mode 100644 index 0000000..d4cb435 --- /dev/null +++ b/backend/app/workers/__init__.py @@ -0,0 +1 @@ +"""Background workers package.""" diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..14ee2de --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "yg-ft-backend" +version = "0.1.0" +description = "Backend service for the model fine-tuning platform" +requires-python = ">=3.11" +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", + "alembic>=1.13.1", + "redis>=5.0.4", + "httpx>=0.27.0", + "PyJWT>=2.8.0", + "passlib[bcrypt]>=1.7.4", + "python-dotenv>=1.0.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.2.0", + "ruff>=0.5.0", +] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..036a07c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,12 @@ +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 +alembic>=1.13.1 +redis>=5.0.4 +httpx>=0.27.0 +PyJWT>=2.8.0 +passlib[bcrypt]>=1.7.4 +python-dotenv>=1.0.1 diff --git a/compute/README.md b/compute/README.md new file mode 100644 index 0000000..f0091f8 --- /dev/null +++ b/compute/README.md @@ -0,0 +1,24 @@ +# Compute Platform + +算力平台与应用平台分开部署,本目录用于后续实现单机多 GPU 调度、文件网关和训练引擎适配。 + +## 目录结构 + +```text +compute/ + api/ # 只允许应用平台访问的内部 Compute API + agent/ # 单机 Agent,负责 GPU、进程、工作区管理 + engines/ + llama_factory/ # LLaMA-Factory 训练引擎适配器 + file_gateway/ # 本地磁盘上传、下载、预览、离线导入 + tests/ +``` + +## 第一版职责 + +- GPU 发现、状态上报、锁定和释放。 +- 本地磁盘工作区管理。 +- 创建、停止、查询训练/评测/推理/合并任务。 +- LLaMA-Factory 命令生成、日志解析、产物收集。 +- 分片上传、短时下载、离线导入。 +- 通过服务间 token 接受应用平台调用。 diff --git a/compute/agent/__init__.py b/compute/agent/__init__.py new file mode 100644 index 0000000..3f8fd74 --- /dev/null +++ b/compute/agent/__init__.py @@ -0,0 +1 @@ +"""Compute agent package.""" diff --git a/compute/api/__init__.py b/compute/api/__init__.py new file mode 100644 index 0000000..4a6735a --- /dev/null +++ b/compute/api/__init__.py @@ -0,0 +1 @@ +"""Compute API package.""" diff --git a/compute/engines/__init__.py b/compute/engines/__init__.py new file mode 100644 index 0000000..adb1576 --- /dev/null +++ b/compute/engines/__init__.py @@ -0,0 +1 @@ +"""Training engine adapters package.""" diff --git a/compute/engines/llama_factory/__init__.py b/compute/engines/llama_factory/__init__.py new file mode 100644 index 0000000..eac03ba --- /dev/null +++ b/compute/engines/llama_factory/__init__.py @@ -0,0 +1 @@ +"""LLaMA-Factory engine adapter package.""" diff --git a/compute/file_gateway/__init__.py b/compute/file_gateway/__init__.py new file mode 100644 index 0000000..e781d5e --- /dev/null +++ b/compute/file_gateway/__init__.py @@ -0,0 +1 @@ +"""Local file gateway package.""" diff --git a/compute/tests/__init__.py b/compute/tests/__init__.py new file mode 100644 index 0000000..a95c0bf --- /dev/null +++ b/compute/tests/__init__.py @@ -0,0 +1 @@ +"""Compute platform tests package.""" diff --git a/docs/backend-logging.md b/docs/backend-logging.md new file mode 100644 index 0000000..babc044 --- /dev/null +++ b/docs/backend-logging.md @@ -0,0 +1,109 @@ +# 后端日志模块说明 + +本文档对应页面/功能模块:全平台通用能力、系统设置、审计中心、任务详情、训练任务日志、运维监控。 + +## 设计目标 + +- 后端服务统一使用 `backend/app/core/logging.py` 初始化日志。 +- 日志文件按日期命名,单个文件超过 20MB 自动滚动。 +- 日志只保留最近 10 天,过期文件自动清理。 +- 业务日志使用 JSON Lines 格式,便于 Filebeat、Vector、Logstash、ELK、OpenSearch 等日志平台采集。 +- `ERROR` 及以上日志独立写入错误日志文件,便于告警与问题定位。 +- 日志字段必须包含代码文件、行号、函数、日志内容、请求 ID、进程和线程信息。 + +## 文件命名 + +默认日志目录由 `LOG_DIR` 控制,本地默认是 `./logs`。 + +```text +logs/ + backend-2026-07-16.log # INFO/ERROR 等全部应用日志,JSON Lines + backend-2026-07-16.1.log # 当天主日志超过 20MB 后滚动产生 + error-2026-07-16.log # ERROR/CRITICAL 错误日志,JSON Lines + error-2026-07-16.1.log # 当天错误日志超过 20MB 后滚动产生 +``` + +## 环境变量 + +```env +LOG_LEVEL=INFO +LOG_DIR=./logs +LOG_FILE_PREFIX=backend +LOG_ERROR_FILE_PREFIX=error +LOG_MAX_BYTES=20971520 +LOG_RETENTION_DAYS=10 +``` + +## JSON 字段 + +每一行都是一个完整 JSON 对象。 + +```json +{ + "@timestamp": "2026-07-16T13:20:10.123", + "level": "INFO", + "logger": "app.access", + "message": "request completed method=GET path=/api/v1/health status_code=200 duration_ms=3.12 client=127.0.0.1", + "module": "logging", + "function": "request_logging_middleware", + "file": "D:\\AI\\codex-code\\YG_FT\\backend\\app\\core\\logging.py", + "line": 169, + "process": 1234, + "thread": 5678, + "thread_name": "MainThread", + "request_id": "6f9d1c3c-8be0-4c8d-a5b2-18f9d41f9a0c" +} +``` + +异常日志会额外包含: + +```json +{ + "exception": "Traceback ..." +} +``` + +## 使用方式 + +业务代码中不要直接 `print`,统一使用: + +```python +from app.core.logging import get_logger + +logger = get_logger(__name__) + +logger.info("dataset uploaded dataset_id=%s", dataset_id) +logger.warning("gpu queue is busy project_id=%s", project_id) +logger.exception("training job failed job_id=%s", job_id) +``` + +`logger.exception(...)` 只能在 `except` 代码块中使用,它会自动写入堆栈信息,并同时进入主日志和错误日志。 + +## FastAPI 接入 + +应用入口 `backend/app/main.py` 已完成接入: + +```python +settings = get_settings() +configure_logging(settings) +setup_request_logging(app) +``` + +请求日志会自动生成或透传 `X-Request-ID`,并在响应头中返回同一个请求 ID,方便前端、后端、算力服务、日志平台串联排障。 + +## ELK/日志平台采集建议 + +- 采集路径:`/app/logs/*.log` 或生产环境挂载后的日志目录。 +- 解析方式:按行读取,每行作为 JSON 文档解析。 +- 索引建议: + - 主日志:`yg-ft-backend-*` + - 错误日志:`yg-ft-backend-error-*` +- 推荐保留字段:`@timestamp`、`level`、`logger`、`message`、`file`、`line`、`function`、`request_id`、`tenant_id`、`project_id`、`job_id`。 +- 业务开发后续应在关键模块日志中补充 `tenant_id`、`project_id`、`job_id` 等上下文字段,便于企业审计和问题定位。 + +## 注意事项 + +- 当前日志落本地磁盘,生产环境建议把日志目录挂载到独立数据盘。 +- 日志文件保留 10 天是应用侧兜底策略,企业侧长期留存应由 ELK、对象存储或归档服务承担。 +- 敏感字段如 token、密码、密钥、原始用户数据内容不得写入日志。 +- 算力节点和应用节点分开部署时,建议两侧都采用 JSON Lines 格式,并使用统一 `request_id/job_id` 贯穿链路。 diff --git a/docs/deployment-plan.md b/docs/deployment-plan.md new file mode 100644 index 0000000..1c69bfe --- /dev/null +++ b/docs/deployment-plan.md @@ -0,0 +1,305 @@ +# 模型微调平台后期部署方案 + +本文档对应页面/功能模块:系统设置、算力资源、训练任务、任务详情、模型管理、数据集管理、审批中心、审计中心、运维监控。 + +## 1. 部署目标 + +平台需要支持单机多 GPU 训练、本地磁盘文件存储、LLaMA-Factory 训练框架,并预留未来接入其他训练平台的能力。部署设计需要把“应用平台”和“算力平台”边界明确拆开: + +- 应用平台:面向用户、权限、项目、模型、数据集、审批、审计、任务编排和 API。 +- 算力平台:面向 GPU、训练进程、训练框架、本地工作目录、训练日志和产物。 +- 训练框架:当前固定 LLaMA-Factory,后续通过 Engine Adapter 标准接入其他框架。 + +结论:算力平台和训练框架应该部署在 GPU 算力服务器上。原因是训练框架需要直接访问 GPU、CUDA、驱动、模型权重、本地数据集切片、训练工作目录和训练进程。应用平台可以与算力平台同机部署,也可以独立部署,但不建议在无 GPU 的应用服务器上直接运行 LLaMA-Factory。 + +## 2. 服务清单 + +| 服务 | 部署位置 | 职责 | +| --- | --- | --- | +| Nginx | 应用服务器或算力服务器 | 前端静态资源、反向代理、TLS 终止 | +| Frontend | Nginx 静态目录 | 平台控制台 | +| Backend API | 应用服务器 | FastAPI 接口、鉴权、元数据、审批、审计、任务编排 | +| Backend Worker | 应用服务器 | 异步任务、状态同步、通知、审计归档 | +| PostgreSQL | 应用服务器或独立数据库服务器 | 业务元数据、权限、审批、审计 | +| Redis | 应用服务器或独立缓存服务器 | 队列、锁、短期状态、幂等控制 | +| Compute API | GPU 算力服务器 | 只对应用平台开放的内部算力接口 | +| Compute Agent | GPU 算力服务器 | GPU 发现、资源锁定、训练进程管理 | +| File Gateway | GPU 算力服务器 | 本地文件上传、下载、离线导入、产物访问 | +| LLaMA-Factory | GPU 算力服务器 | 实际训练、评测、合并、导出 | +| 日志采集 Agent | 两侧服务器 | 采集应用日志、训练日志、系统日志 | + +## 3. 目录与存储规划 + +建议生产环境把文件、日志、数据库数据分盘挂载: + +```text +/opt/yg-ft/ + app/ # 应用服务代码 + compute/ # 算力服务代码 + config/ # 环境配置和服务配置 + logs/ + backend/ # 后端 JSON Lines 日志 + compute/ # 算力服务日志 + training/ # 训练过程日志 + data/ + datasets/ # 数据集文件 + models/ # 基座模型、微调模型、导出模型 + jobs/ # 训练任务工作目录 + artifacts/ # 评测报告、adapter、checkpoint、导出包 +``` + +本地文件存储建议按租户、项目、资源类型分区: + +```text +/data/yg-ft/ + tenants/{tenant_id}/ + projects/{project_id}/ + datasets/{dataset_id}/ + models/{model_id}/ + jobs/{job_id}/ +``` + +## 4. 方案一:所有服务部署在算力服务器 + +### 4.1 适用场景 + +- PoC、试点环境、演示环境。 +- 小团队共用一台单机多 GPU 服务器。 +- 网络隔离要求不高,部署资源有限。 + +### 4.2 拓扑 + +```mermaid +flowchart LR + U["用户浏览器"] --> N["Nginx/Frontend"] + N --> B["Backend API"] + B --> DB["PostgreSQL"] + B --> R["Redis"] + B --> C["Compute API"] + C --> A["Compute Agent"] + A --> L["LLaMA-Factory"] + A --> G["GPU/CUDA"] + A --> FS["本地磁盘文件存储"] +``` + +### 4.3 部署方式 + +同一台 GPU 服务器部署: + +- `frontend` 构建后由 Nginx 托管。 +- `backend-api` 使用 Uvicorn/Gunicorn 或容器运行。 +- `backend-worker` 独立进程运行。 +- `postgres` 和 `redis` 可使用 Docker Compose 或系统服务。 +- `compute-api`、`compute-agent`、`file-gateway` 与 LLaMA-Factory 在同机运行。 +- 训练产物、数据集、模型和日志都放在本地数据盘。 + +### 4.4 优点 + +- 部署简单,路径共享容易。 +- 上传数据、训练读取、产物归档都在本机完成,I/O 链路短。 +- 适合快速验证平台功能。 + +### 4.5 风险 + +- 应用服务、数据库、训练任务抢占同一台服务器资源。 +- GPU 训练高负载可能影响 API 响应。 +- 数据库与文件存储容灾能力弱。 +- 安全边界不清晰,企业生产不推荐长期使用。 + +### 4.6 端口建议 + +| 服务 | 端口 | 暴露范围 | +| --- | --- | --- | +| Nginx | 80/443 | 用户网段 | +| Backend API | 8000 | 仅 Nginx、本机 | +| Compute API | 9100 | 仅 Backend API、本机 | +| File Gateway | 9101 | 仅 Backend API、本机 | +| PostgreSQL | 5432 | 本机或内网 | +| Redis | 6379 | 本机或内网 | + +## 5. 方案二:应用服务与算力/训练服务独立部署 + +### 5.1 适用场景 + +- 企业生产环境。 +- 有独立应用服务器、数据库服务器和 GPU 算力服务器。 +- 需要清晰网络边界、权限边界和运维职责。 +- 未来可能扩展多台 GPU 服务器或多种训练框架。 + +### 5.2 拓扑 + +```mermaid +flowchart LR + U["用户浏览器"] --> N["应用区 Nginx/Frontend"] + N --> B["应用区 Backend API"] + B --> DB["PostgreSQL"] + B --> R["Redis"] + B -- "内部 HTTPS/mTLS + 服务 Token" --> C["算力区 Compute API"] + C --> A["Compute Agent"] + A --> L["LLaMA-Factory"] + A --> G["GPU/CUDA"] + A --> FS["算力服务器本地磁盘"] + A -- "状态回调/日志摘要" --> B +``` + +### 5.3 部署边界 + +应用服务器部署: + +- Nginx。 +- Frontend。 +- Backend API。 +- Backend Worker。 +- PostgreSQL 或数据库连接。 +- Redis 或队列连接。 +- 审批、审计、系统配置、用户中心等应用能力。 + +GPU 算力服务器部署: + +- Compute API。 +- Compute Agent。 +- File Gateway。 +- LLaMA-Factory。 +- CUDA、NVIDIA Driver、NCCL、PyTorch、训练依赖。 +- 本地训练工作目录、模型目录、数据集缓存、产物目录。 + +### 5.4 互通方式 + +应用平台调用算力平台: + +- 协议:内部 HTTPS REST,后续可扩展 gRPC。 +- 鉴权:服务间 Token,生产建议 mTLS + IP 白名单。 +- 幂等:训练任务提交使用 `Idempotency-Key` 或 `job_id`。 +- 回调:算力平台向应用平台回调任务状态、指标摘要、产物索引。 +- 拉取:应用平台也可以定时轮询 Compute API,避免回调失败导致状态丢失。 + +文件互通: + +- 小文件:前端上传到 Backend API,再由 Backend API 转发或同步到 File Gateway。 +- 大文件:Backend API 创建上传会话,前端通过受控地址分片上传到 File Gateway。 +- 离线数据:管理员把数据放到算力服务器指定目录,应用平台登记离线导入任务。 +- 产物下载:应用平台校验权限后,向 File Gateway 申请短期下载地址。 + +状态互通: + +- Backend API 是业务状态的最终来源。 +- Compute Agent 是训练进程状态的事实来源。 +- Worker 定时对账,把 `queued/running/succeeded/failed/cancelled` 等状态同步回业务库。 + +### 5.5 优点 + +- 应用服务稳定性不受 GPU 训练高负载直接影响。 +- 数据库和审计能力更适合纳入企业基础设施。 +- 算力节点可以逐步扩展,不影响前端和应用后端。 +- 安全边界更清晰,便于设置防火墙、堡垒机、服务账号和审计策略。 + +### 5.6 风险 + +- 文件传输链路比单机部署复杂。 +- 需要处理跨服务器网络失败、回调失败、任务状态对账。 +- 需要明确模型、数据集、产物在应用侧和算力侧的索引关系。 + +## 6. Compute API 接入标准 + +为预留其他训练平台,应用平台只依赖统一算力接口,不直接依赖 LLaMA-Factory 命令。 + +训练引擎适配器应提供: + +- `validate_config(config)`:校验训练参数和模板。 +- `build_command(job)`:生成训练命令或执行计划。 +- `start(job)`:启动训练进程。 +- `stop(job_id)`:停止训练进程。 +- `status(job_id)`:查询训练状态。 +- `collect_metrics(job_id)`:采集 loss、learning rate、epoch、step 等指标。 +- `collect_artifacts(job_id)`:登记 checkpoint、adapter、导出模型、评测报告。 +- `parse_log(line)`:解析训练日志。 + +第一版适配器: + +```text +compute/engines/llama_factory/ +``` + +后续其他框架: + +```text +compute/engines/xtuner/ +compute/engines/deepspeed_custom/ +compute/engines/openrlhf/ +``` + +## 7. 环境变量建议 + +应用平台: + +```env +APP_ENV=prod +API_PREFIX=/api +DATABASE_URL=postgresql+asyncpg://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:9100 +COMPUTE_SERVICE_TOKEN=*** +FILE_GATEWAY_BASE_URL=https://compute.internal:9101 +``` + +算力平台: + +```env +COMPUTE_ENV=prod +COMPUTE_HOST_ID=gpu-node-01 +COMPUTE_API_PORT=9100 +FILE_GATEWAY_PORT=9101 +APP_CALLBACK_BASE_URL=https://app.internal/api/v1/compute/callbacks +APP_SERVICE_TOKEN=*** +LLAMA_FACTORY_HOME=/opt/LLaMA-Factory +YG_FT_DATA_ROOT=/data/yg-ft +LOG_DIR=/opt/yg-ft/logs/compute +CUDA_VISIBLE_DEVICES=0,1,2,3 +``` + +## 8. 日志与监控 + +应用平台: + +- 采集 `backend-YYYY-MM-DD.log` 和 `error-YYYY-MM-DD.log`。 +- 按 `request_id`、`tenant_id`、`project_id`、`job_id` 检索。 +- ERROR 日志触发告警。 + +算力平台: + +- 采集 Compute API 日志、Agent 日志、训练原始日志。 +- 训练日志需要按 `job_id` 独立归档。 +- 关键指标包括 GPU 利用率、显存、磁盘容量、训练队列长度、失败率。 + +## 9. 安全要求 + +- Compute API 不对公网开放。 +- 应用平台和算力平台之间使用服务账号鉴权,生产建议 mTLS。 +- File Gateway 下载地址必须短期有效,并绑定租户、项目、资源权限。 +- 日志不得输出密码、Token、密钥、数据集原文敏感内容。 +- 审计日志留存周期按租户或企业配置执行,应用日志短期留存,长期归档交给日志平台。 + +## 10. 部署检查清单 + +- PostgreSQL 已初始化 `docs/postgres-schema.sql`。 +- Redis 可连通。 +- 后端 `GET /api/v1/health` 正常。 +- Compute API 健康检查正常。 +- Compute Agent 能识别 GPU、显存、CUDA 版本。 +- LLaMA-Factory 能在命令行完成最小训练样例。 +- 应用平台能提交训练任务到 Compute API。 +- 任务状态能从算力平台同步回应用平台。 +- 数据集上传、离线导入、产物下载路径权限正确。 +- 后端 JSON 日志可被日志平台解析。 +- ERROR 日志能触发告警。 +- 日志、数据集、模型、产物所在磁盘容量有监控和告警。 + +## 11. 仍需确认的问题 + +- 生产环境是否已有统一 ELK/OpenSearch、Filebeat/Vector 标准配置。 +- 数据库和 Redis 是否由企业基础设施统一提供,还是由项目自行部署。 +- 是否需要 PostgreSQL 主备、备份恢复、审计日志长期归档的明确 SLA。 +- 大文件上传是否需要断点续传、限速、病毒扫描或 DLP 检测。 +- 应用服务器与算力服务器之间是否允许双向访问,还是只能应用侧主动访问算力侧。 +- 是否需要未来支持多台 GPU 节点调度;如果需要,Compute API 需要提前设计节点注册和调度策略。