feat: 基于 LLaMA-Factory 实现模型推理引擎
利用容器内已有的 LLaMA-Factory ChatModel (huggingface 后端) 实现真实 模型推理,无需额外安装 vLLM。 Compute 端新增: - compute/engines/llama_factory/inference.py InferenceSession: 模型加载/卸载/对话/流式输出 支持 base model 和 LoRA adapter,线程安全 - compute/api/main.py 新增 5 个推理端点: POST /inference/load - 加载模型 POST /inference/unload - 卸载释放 GPU 显存 GET /inference/status - 查询会话状态 POST /inference/chat - 非流式对话 POST /inference/chat/stream - SSE 流式对话 后端新增: - platform.py 推理代理端点(local/chat, local/chat/stream, local/preload, local/unload, local/status, trained/preload) - ComputeNodeClient._request 通用请求方法 - _select_first_online_node 自动选择在线节点 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,11 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, File, Form, HTTPException, Query, Request, UploadFile
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
||||
|
||||
from compute.agent.process_manager import ProcessManager
|
||||
from compute.engines.llama_factory.adapter import build_command, parse_log_line, prepare_runtime_files
|
||||
from compute.engines.llama_factory.inference import get_inference_session
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
@@ -666,6 +667,85 @@ def create_app() -> FastAPI:
|
||||
metrics = [parse_log_line(line) for line in window["content"].splitlines()]
|
||||
return {"job_id": job_id, **window, "metrics": [m for m in metrics if m]}
|
||||
|
||||
# ── Inference Endpoints ───────────────────────────────────────────
|
||||
|
||||
@app.post(f"{route_prefix}/inference/load")
|
||||
async def inference_load(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Load a model for inference using LLaMA-Factory ChatModel.
|
||||
|
||||
Expected payload:
|
||||
model_name_or_path: str (required)
|
||||
adapter_name_or_path: str (optional, for LoRA adapters)
|
||||
template: str (default: "qwen")
|
||||
infer_backend: str (default: "huggingface")
|
||||
infer_dtype: str (default: "auto")
|
||||
"""
|
||||
session = get_inference_session()
|
||||
result = session.load(
|
||||
model_name_or_path=payload.get("model_name_or_path", ""),
|
||||
adapter_name_or_path=payload.get("adapter_name_or_path", ""),
|
||||
template=payload.get("template", "qwen"),
|
||||
infer_backend=payload.get("infer_backend", "huggingface"),
|
||||
infer_dtype=payload.get("infer_dtype", "auto"),
|
||||
)
|
||||
if not result.get("loaded"):
|
||||
raise HTTPException(status_code=500, detail=result.get("error", "model load failed"))
|
||||
return result
|
||||
|
||||
@app.post(f"{route_prefix}/inference/unload")
|
||||
async def inference_unload() -> dict[str, Any]:
|
||||
"""Unload the currently loaded model and free GPU memory."""
|
||||
return get_inference_session().unload()
|
||||
|
||||
@app.get(f"{route_prefix}/inference/status")
|
||||
async def inference_status() -> dict[str, Any]:
|
||||
"""Get the current inference session status."""
|
||||
return get_inference_session().info()
|
||||
|
||||
@app.post(f"{route_prefix}/inference/chat")
|
||||
async def inference_chat(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Chat with the loaded model (non-streaming).
|
||||
|
||||
Expected payload:
|
||||
messages: list[dict] (OpenAI format)
|
||||
temperature: float (default 0.95)
|
||||
top_p: float (default 0.7)
|
||||
max_new_tokens: int (default 1024)
|
||||
"""
|
||||
messages = payload.get("messages") or []
|
||||
if not messages:
|
||||
raise HTTPException(status_code=400, detail="messages is required")
|
||||
result = get_inference_session().chat(
|
||||
messages=messages,
|
||||
temperature=float(payload.get("temperature", 0.95)),
|
||||
top_p=float(payload.get("top_p", 0.7)),
|
||||
max_new_tokens=int(payload.get("max_new_tokens", 1024)),
|
||||
do_sample=bool(payload.get("do_sample", True)),
|
||||
)
|
||||
if result.get("error"):
|
||||
raise HTTPException(status_code=500, detail=result["error"])
|
||||
return {"response": result["response"]}
|
||||
|
||||
@app.post(f"{route_prefix}/inference/chat/stream")
|
||||
async def inference_chat_stream(payload: dict[str, Any]) -> StreamingResponse:
|
||||
"""Chat with streaming response (Server-Sent Events)."""
|
||||
messages = payload.get("messages") or []
|
||||
if not messages:
|
||||
raise HTTPException(status_code=400, detail="messages is required")
|
||||
|
||||
def generate():
|
||||
session = get_inference_session()
|
||||
for chunk in session.chat_stream(
|
||||
messages=messages,
|
||||
temperature=float(payload.get("temperature", 0.95)),
|
||||
top_p=float(payload.get("top_p", 0.7)),
|
||||
max_new_tokens=int(payload.get("max_new_tokens", 1024)),
|
||||
do_sample=bool(payload.get("do_sample", True)),
|
||||
):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||||
|
||||
@app.post(f"{route_prefix}/compute/files/upload")
|
||||
async def upload_file(
|
||||
file: UploadFile | None = File(default=None),
|
||||
|
||||
Reference in New Issue
Block a user