Spaces:
No application file
No application file
| import torch | |
| import time | |
| import uuid | |
| from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import List, Optional, Union, Dict, Any | |
| import uvicorn | |
| app = FastAPI() | |
| # CORS設定 | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # INT4量子化設定 | |
| quantization_config = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_compute_dtype=torch.float16, | |
| bnb_4bit_use_double_quant=True, | |
| bnb_4bit_quant_type="nf4" | |
| ) | |
| # モデルとトークナイザーの初期化 | |
| model_name = "Qwen/Qwen3-235B-A22B" | |
| model_id = "qwen3-235b-a22b-int4" # OpenAI API互換のモデルID | |
| print(f"Loading model: {model_name}") | |
| tokenizer = AutoTokenizer.from_pretrained(model_name) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| model_name, | |
| quantization_config=quantization_config, | |
| device_map="auto", | |
| torch_dtype=torch.float16, | |
| trust_remote_code=True | |
| ) | |
| print("Model loaded successfully") | |
| # OpenAI API互換のデータモデル | |
| class ChatMessage(BaseModel): | |
| role: str | |
| content: str | |
| class ChatCompletionRequest(BaseModel): | |
| model: str | |
| messages: List[ChatMessage] | |
| temperature: Optional[float] = 0.7 | |
| top_p: Optional[float] = 0.9 | |
| max_tokens: Optional[int] = 512 | |
| stream: Optional[bool] = False | |
| class ChatCompletionChoice(BaseModel): | |
| index: int | |
| message: ChatMessage | |
| finish_reason: str | |
| class ChatCompletionUsage(BaseModel): | |
| prompt_tokens: int | |
| completion_tokens: int | |
| total_tokens: int | |
| class ChatCompletionResponse(BaseModel): | |
| id: str | |
| object: str = "chat.completion" | |
| created: int | |
| model: str | |
| choices: List[ChatCompletionChoice] | |
| usage: ChatCompletionUsage | |
| class ModelInfo(BaseModel): | |
| id: str | |
| object: str = "model" | |
| created: int | |
| owned_by: str = "huggingface" | |
| class ModelsResponse(BaseModel): | |
| object: str = "list" | |
| data: List[ModelInfo] | |
| # チャット形式のプロンプト構築 | |
| def build_chat_prompt(messages: List[ChatMessage]) -> str: | |
| """Qwen形式のチャットプロンプトを構築""" | |
| prompt = "" | |
| for msg in messages: | |
| if msg.role == "system": | |
| prompt += f"System: {msg.content}\n\n" | |
| elif msg.role == "user": | |
| prompt += f"Human: {msg.content}\n\n" | |
| elif msg.role == "assistant": | |
| prompt += f"Assistant: {msg.content}\n\n" | |
| # 最後にAssistantのプロンプトを追加 | |
| if messages[-1].role != "assistant": | |
| prompt += "Assistant: " | |
| return prompt | |
| async def chat_completions(request: ChatCompletionRequest): | |
| """OpenAI互換のチャット補完エンドポイント""" | |
| try: | |
| # プロンプト構築 | |
| prompt = build_chat_prompt(request.messages) | |
| # トークナイズ | |
| inputs = tokenizer(prompt, return_tensors="pt").to(model.device) | |
| input_length = inputs["input_ids"].shape[1] | |
| # 生成 | |
| with torch.no_grad(): | |
| outputs = model.generate( | |
| **inputs, | |
| max_new_tokens=request.max_tokens, | |
| temperature=request.temperature, | |
| top_p=request.top_p, | |
| do_sample=True, | |
| pad_token_id=tokenizer.eos_token_id | |
| ) | |
| # デコード | |
| generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| # プロンプトを除去 | |
| response_text = generated_text[len(prompt):].strip() | |
| # トークン数計算 | |
| output_length = outputs[0].shape[0] - input_length | |
| # レスポンス構築 | |
| response = ChatCompletionResponse( | |
| id=f"chatcmpl-{uuid.uuid4().hex[:8]}", | |
| created=int(time.time()), | |
| model=request.model, | |
| choices=[ | |
| ChatCompletionChoice( | |
| index=0, | |
| message=ChatMessage(role="assistant", content=response_text), | |
| finish_reason="stop" | |
| ) | |
| ], | |
| usage=ChatCompletionUsage( | |
| prompt_tokens=input_length, | |
| completion_tokens=output_length, | |
| total_tokens=input_length + output_length | |
| ) | |
| ) | |
| return response | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def list_models(): | |
| """利用可能なモデルのリストを返す""" | |
| return ModelsResponse( | |
| data=[ | |
| ModelInfo( | |
| id=model_id, | |
| created=int(time.time()) | |
| ) | |
| ] | |
| ) | |
| async def health(): | |
| return { | |
| "status": "healthy", | |
| "model": model_name, | |
| "model_id": model_id, | |
| "quantization": "INT4" | |
| } | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |