#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ VLM 图生文推理服务 — AndesVL-1B (千问体系) 为 RDK X5 提供 OpenAI 兼容 API(纯 CPU 推理) 启动方式: python vlm_server.py API 地址: http://192.168.10.210:8000 """ import base64 import io import time import uuid import logging import sys from contextlib import asynccontextmanager import torch import uvicorn from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from PIL import Image from transformers import AutoModel, AutoTokenizer, AutoImageProcessor # ========== 配置 ========== MODEL_PATH = "/home/root/models/AndesVL-1B-Instruct" # RDK X5 上模型路径 HOST = "0.0.0.0" PORT = 8000 # ========== 日志 ========== logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler(sys.stdout)], ) logger = logging.getLogger("vlm_server") # ========== 全局变量 ========== model = None tokenizer = None image_processor = None def load_model(): """加载 AndesVL-1B 模型""" global model, tokenizer, image_processor logger.info(f"模型路径: {MODEL_PATH}") logger.info("加载 tokenizer & image processor...") tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) image_processor = AutoImageProcessor.from_pretrained( MODEL_PATH, trust_remote_code=True ) logger.info("加载 AndesVL-1B 模型 (fp32 CPU, 约 18 秒)...") t0 = time.time() model = AutoModel.from_pretrained( MODEL_PATH, trust_remote_code=True, torch_dtype=torch.float32, low_cpu_mem_usage=True, ) model.eval() logger.info(f"模型加载完成,耗时 {time.time() - t0:.1f}s") logger.info("AndesVL-1B 推理服务就绪!") @asynccontextmanager async def lifespan(app: FastAPI): load_model() yield # ========== FastAPI 应用 ========== app = FastAPI(title="VLM Server (AndesVL-1B)", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ========== Pydantic 模型 (OpenAI 格式) ========== class ImageUrl(BaseModel): url: str class ContentPart(BaseModel): type: str text: str | None = None image_url: ImageUrl | None = None class Message(BaseModel): role: str content: str | list[ContentPart] class ChatCompletionRequest(BaseModel): model: str messages: list[Message] max_tokens: int = 100 temperature: float | None = None # ========== 辅助函数 ========== def decode_base64_image(data_url: str) -> Image.Image: """从 data URL 解码图像""" if "," in data_url: base64_str = data_url.split(",", 1)[1] else: base64_str = data_url image_bytes = base64.b64decode(base64_str) return Image.open(io.BytesIO(image_bytes)).convert("RGB") def build_messages(messages: list[Message]) -> list[dict]: """OpenAI 格式 → AndesVL chat 格式""" result = [] for msg in messages: if isinstance(msg.content, str): result.append({"role": msg.role, "content": msg.content}) else: content_list = [] for part in msg.content: if part.type == "text" and part.text: content_list.append({"type": "text", "text": part.text}) elif part.type == "image_url" and part.image_url: image = decode_base64_image(part.image_url.url) content_list.append({"type": "image", "image": image}) result.append({"role": msg.role, "content": content_list}) return result # ========== API 路由 ========== @app.get("/v1/models") async def list_models(): return { "object": "list", "data": [ { "id": "./OpenGVLab/InternVL3-1B/", "object": "model", "created": 1700000000, "owned_by": "local", } ], } @app.post("/v1/chat/completions") async def chat_completions(request: ChatCompletionRequest): try: messages = build_messages(request.messages) logger.info(f"收到请求, model={request.model}, max_tokens={request.max_tokens}") start_time = time.time() response_text = model.chat( messages, tokenizer, image_processor, max_new_tokens=request.max_tokens, ) elapsed = time.time() - start_time logger.info(f"推理完成,耗时 {elapsed:.1f}s, 结果: {response_text[:50]}...") return { "id": f"chatcmpl-{uuid.uuid4().hex[:12]}", "object": "chat.completion", "created": int(time.time()), "model": request.model, "choices": [ { "index": 0, "message": {"role": "assistant", "content": response_text}, "finish_reason": "stop", } ], "usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, }, } except Exception as e: logger.error(f"推理出错: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @app.get("/health") async def health(): return {"status": "ok", "model_loaded": model is not None} # ========== 入口 ========== if __name__ == "__main__": logger.info("=" * 50) logger.info("VLM 图生文推理服务启动中...") logger.info(f"监听: http://{HOST}:{PORT}") logger.info("=" * 50) uvicorn.run(app, host=HOST, port=PORT, log_level="info")