From ba4059fe3ba2a61c9469010b08f7b73d63970f28 Mon Sep 17 00:00:00 2001 From: wuyongtao Date: Thu, 16 Jul 2026 13:47:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E3=80=81=E8=AE=A1=E7=AE=97=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=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 需要提前设计节点注册和调度策略。