ft_wyt #5
184
README.md
184
README.md
@@ -1,133 +1,107 @@
|
|||||||
# YG_FT
|
# YG_FT 模型微调平台
|
||||||
|
|
||||||
远光微调平台 - 面向大语言模型的微调、评测、推理与对比一体化前端。
|
YG_FT 是一个面向企业治理场景的完整模型微调平台,覆盖用户中心、多租户、项目隔离、数据集管理、模型管理、训练任务、评测、推理、审批流、审计留存、算力调度和训练引擎适配。当前前端已存在基础页面,后端与算力平台已按多人协作开发方式建立工程骨架。
|
||||||
|
|
||||||
## 技术栈
|
## 总体架构
|
||||||
|
|
||||||
| 类别 | 技术 | 版本 |
|
```text
|
||||||
|------|------|------|
|
YG_FT/
|
||||||
| 框架 | Vue 3 | ^3.5.13 |
|
frontend/ # 前端应用,承载训练平台控制台页面
|
||||||
| 语言 | TypeScript | ~5.7.2 |
|
backend/ # FastAPI 应用平台后端
|
||||||
| 构建工具 | Vite | ^6.0.7 |
|
app/
|
||||||
| 路由 | Vue Router | ^4.5.0 |
|
api/v1/ # 对前端暴露的 REST API
|
||||||
| 状态管理 | Pinia | ^2.3.0 |
|
core/ # 配置、日志、中间件、权限等基础能力
|
||||||
| UI 组件库 | Element Plus | ^2.9.1 |
|
db/ # 数据库连接、迁移、事务工具
|
||||||
| HTTP 客户端 | axios | ^1.7.9 |
|
modules/ # 业务模块目录
|
||||||
| 图表 | ECharts / vue-echarts | ^6.1.0 / ^8.0.1 |
|
schemas/ # Pydantic 入参/出参模型
|
||||||
| Markdown | marked + DOMPurify | ^15.0.5 / ^3.2.3 |
|
services/ # 跨模块应用服务
|
||||||
| 编辑器 | md-editor-v3 | ^5.1.4 |
|
workers/ # 后台任务入口
|
||||||
| 工具集 | @vueuse/core | ^11.3.0 |
|
requirements.txt # 后端 Python 第三方依赖
|
||||||
| 样式 | Sass | ^1.83.0 |
|
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
|
```bash
|
||||||
cd frontend
|
cd backend
|
||||||
npm install
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 启动开发服务器
|
默认健康检查:
|
||||||
|
|
||||||
```bash
|
```text
|
||||||
npm run dev
|
GET /api/v1/health
|
||||||
```
|
```
|
||||||
|
|
||||||
开发服务器默认运行在 `http://localhost:6801`。
|
## 日志
|
||||||
|
|
||||||
### 3. 构建生产包
|
后端日志模块位于 `backend/app/core/logging.py`,说明文档见:
|
||||||
|
|
||||||
```bash
|
- `docs/backend-logging.md`
|
||||||
npm run build # 类型检查 + 生产构建,产物输出到 dist/
|
|
||||||
npm run preview # 本地预览构建产物
|
默认输出:
|
||||||
|
|
||||||
|
```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
|
生产环境建议采用第二种模式。算力平台与训练框架应部署在 GPU 算力服务器上,应用平台不直接控制 GPU 进程,而是通过内部 Compute API 调度训练任务。
|
||||||
npm run test:data-process-wizard # 数据处理向导
|
|
||||||
npm run test:model-manage # 模型管理
|
|
||||||
npm run test:training-log-layout # 训练日志布局
|
|
||||||
npm run test:page-surface # 页面表层级
|
|
||||||
```
|
|
||||||
|
|
||||||
其余脚本可直接运行:
|
详细方案见 `docs/deployment-plan.md`。
|
||||||
|
|
||||||
```bash
|
## 后续开发原则
|
||||||
node scripts/regression-back-navigation.mjs # 返回导航
|
|
||||||
node scripts/regression-fine-tune-create-ui.mjs # 调优创建 UI
|
|
||||||
```
|
|
||||||
|
|
||||||
> 回归脚本默认连接 `http://localhost:6801`,需先启动开发服务器。
|
- 接口实现优先遵循 `docs/backend-api-design.md`。
|
||||||
|
- 数据库实现优先遵循 `docs/postgres-schema.sql`,后续通过 Alembic 迁移管理变更。
|
||||||
## 目录结构
|
- 前端页面与后端接口、数据库表之间的映射以文档中的“对应页面/功能模块”为准。
|
||||||
|
- 训练引擎适配必须通过 `compute/engines/` 下的标准接口,不在应用平台后端直接拼接训练命令。
|
||||||
```
|
- 敏感信息不得写入日志,生产环境密钥通过环境变量或密钥管理系统注入。
|
||||||
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 与权重合并 |
|
|
||||||
| 数据集 | 数据集管理与预览 |
|
|
||||||
| 数据处理 | 数据处理任务向导 |
|
|
||||||
| 工具 | 辅助工具集 |
|
|
||||||
| 系统 | 硬件监控、日志、训练日志 |
|
|
||||||
|
|||||||
9
backend/.env.example
Normal file
9
backend/.env.example
Normal file
@@ -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
|
||||||
65
backend/README.md
Normal file
65
backend/README.md
Normal file
@@ -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/日志平台采集。
|
||||||
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Application package."""
|
||||||
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""API package."""
|
||||||
1
backend/app/api/v1/__init__.py
Normal file
1
backend/app/api/v1/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Versioned API package."""
|
||||||
1
backend/app/api/v1/endpoints/__init__.py
Normal file
1
backend/app/api/v1/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""API endpoint modules."""
|
||||||
12
backend/app/api/v1/endpoints/health.py
Normal file
12
backend/app/api/v1/endpoints/health.py
Normal file
@@ -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"}
|
||||||
6
backend/app/api/v1/router.py
Normal file
6
backend/app/api/v1/router.py
Normal file
@@ -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"])
|
||||||
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Core infrastructure modules."""
|
||||||
28
backend/app/core/config.py
Normal file
28
backend/app/core/config.py
Normal file
@@ -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()
|
||||||
253
backend/app/core/logging.py
Normal file
253
backend/app/core/logging.py
Normal file
@@ -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)
|
||||||
1
backend/app/db/__init__.py
Normal file
1
backend/app/db/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Database infrastructure package."""
|
||||||
4
backend/app/db/session.py
Normal file
4
backend/app/db/session.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""Database session factory placeholder.
|
||||||
|
|
||||||
|
Implement SQLAlchemy/SQLModel session management here when database development starts.
|
||||||
|
"""
|
||||||
18
backend/app/main.py
Normal file
18
backend/app/main.py
Normal file
@@ -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()
|
||||||
15
backend/app/modules/README.md
Normal file
15
backend/app/modules/README.md
Normal file
@@ -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` 的页面模块开发工作包为准。
|
||||||
1
backend/app/modules/approval/__init__.py
Normal file
1
backend/app/modules/approval/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Approval workflow module."""
|
||||||
1
backend/app/modules/audit/__init__.py
Normal file
1
backend/app/modules/audit/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Audit log module."""
|
||||||
1
backend/app/modules/auth/__init__.py
Normal file
1
backend/app/modules/auth/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Authentication and user session module."""
|
||||||
1
backend/app/modules/compute_gateway/__init__.py
Normal file
1
backend/app/modules/compute_gateway/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Application-side compute platform gateway module."""
|
||||||
1
backend/app/modules/data_process/__init__.py
Normal file
1
backend/app/modules/data_process/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Data processing module."""
|
||||||
1
backend/app/modules/dataset/__init__.py
Normal file
1
backend/app/modules/dataset/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Dataset management module."""
|
||||||
1
backend/app/modules/engine_registry/__init__.py
Normal file
1
backend/app/modules/engine_registry/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Training engine registry module."""
|
||||||
1
backend/app/modules/eval/__init__.py
Normal file
1
backend/app/modules/eval/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Evaluation module."""
|
||||||
1
backend/app/modules/file_gateway/__init__.py
Normal file
1
backend/app/modules/file_gateway/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Application-side file gateway module."""
|
||||||
1
backend/app/modules/fine_tune/__init__.py
Normal file
1
backend/app/modules/fine_tune/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Fine-tuning task module."""
|
||||||
1
backend/app/modules/inference/__init__.py
Normal file
1
backend/app/modules/inference/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Inference and compare module."""
|
||||||
1
backend/app/modules/model/__init__.py
Normal file
1
backend/app/modules/model/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Model registry module."""
|
||||||
1
backend/app/modules/project/__init__.py
Normal file
1
backend/app/modules/project/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Project workspace and member module."""
|
||||||
1
backend/app/modules/retention/__init__.py
Normal file
1
backend/app/modules/retention/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Retention policy and cleanup module."""
|
||||||
1
backend/app/modules/system/__init__.py
Normal file
1
backend/app/modules/system/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""System health, metrics and logs module."""
|
||||||
1
backend/app/modules/tenant/__init__.py
Normal file
1
backend/app/modules/tenant/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Tenant management module."""
|
||||||
1
backend/app/schemas/__init__.py
Normal file
1
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Shared schemas package."""
|
||||||
1
backend/app/services/__init__.py
Normal file
1
backend/app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Cross-module services package."""
|
||||||
1
backend/app/workers/__init__.py
Normal file
1
backend/app/workers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Background workers package."""
|
||||||
32
backend/pyproject.toml
Normal file
32
backend/pyproject.toml
Normal file
@@ -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"]
|
||||||
12
backend/requirements.txt
Normal file
12
backend/requirements.txt
Normal file
@@ -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
|
||||||
24
compute/README.md
Normal file
24
compute/README.md
Normal file
@@ -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 接受应用平台调用。
|
||||||
1
compute/agent/__init__.py
Normal file
1
compute/agent/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Compute agent package."""
|
||||||
1
compute/api/__init__.py
Normal file
1
compute/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Compute API package."""
|
||||||
1
compute/engines/__init__.py
Normal file
1
compute/engines/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Training engine adapters package."""
|
||||||
1
compute/engines/llama_factory/__init__.py
Normal file
1
compute/engines/llama_factory/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""LLaMA-Factory engine adapter package."""
|
||||||
1
compute/file_gateway/__init__.py
Normal file
1
compute/file_gateway/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Local file gateway package."""
|
||||||
1
compute/tests/__init__.py
Normal file
1
compute/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Compute platform tests package."""
|
||||||
109
docs/backend-logging.md
Normal file
109
docs/backend-logging.md
Normal file
@@ -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` 贯穿链路。
|
||||||
305
docs/deployment-plan.md
Normal file
305
docs/deployment-plan.md
Normal file
@@ -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 需要提前设计节点注册和调度策略。
|
||||||
Reference in New Issue
Block a user