gpt-oss-20b / app.py
doropiza's picture
commit
095a19e
"""
ChatGPT Clone - 日本語対応チャットボット
Hugging Face Spaces (ZeroGPU) 対応版
使用モデル:
- elyza/Llama-3-ELYZA-JP-8B
- Fugaku-LLM/Fugaku-LLM-13B
- openai/gpt-oss-20b
"""
import os
from typing import List, Tuple
# Check if running on ZeroGPU FIRST (before any CUDA initialization)
try:
import spaces
IS_ZEROGPU = True
print("ZeroGPU環境を検出しました。")
except ImportError:
IS_ZEROGPU = False
print("通常のGPU/CPU環境で実行しています。")
# Import after spaces check
import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
# Hugging Face token from environment variable
HF_TOKEN = os.getenv("HF_TOKEN")
# トークンのチェック
if not HF_TOKEN:
print("警告: HF_TOKENが設定されていません。プライベートモデルへのアクセスが制限される場合があります。")
class ChatBot:
def __init__(self):
self.model = None
self.tokenizer = None
self.pipeline = None
self.current_model = None
def is_gpt_oss_model(self, model_name: str) -> bool:
"""gpt-ossモデルかどうかを判定"""
return "gpt-oss" in model_name.lower()
def load_model(self, model_name: str):
"""モデルとトークナイザーをロード"""
if self.current_model == model_name and (self.model is not None or self.pipeline is not None):
return
try:
# メモリクリア
if self.model is not None:
del self.model
del self.tokenizer
if self.pipeline is not None:
del self.pipeline
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
if self.is_gpt_oss_model(model_name):
# gpt-ossモデルの場合はpipelineを使用
print(f"gpt-ossモデル {model_name} をpipelineでロードします...")
self.pipeline = pipeline(
"text-generation",
model=model_name,
torch_dtype="auto", # gpt-ossモデルに適した精度を自動選択
trust_remote_code=True,
token=HF_TOKEN,
device_map="auto" if not IS_ZEROGPU else None # ZeroGPU以外では自動マッピング
)
self.model = None
self.tokenizer = None
else:
# 通常のモデルの場合
print(f"通常のモデル {model_name} をロードします...")
# トークナイザーロード
self.tokenizer = AutoTokenizer.from_pretrained(
model_name,
token=HF_TOKEN,
trust_remote_code=True,
padding_side="left"
)
# パッドトークンの設定
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
# モデルロード(ZeroGPU対応)
self.model = AutoModelForCausalLM.from_pretrained(
model_name,
token=HF_TOKEN,
torch_dtype="auto", # 自動精度選択
low_cpu_mem_usage=True,
trust_remote_code=True,
load_in_8bit=False, # ZeroGPU環境では8bit量子化は使わない
device_map="auto" if not IS_ZEROGPU else None # ZeroGPU対応
)
self.pipeline = None
self.current_model = model_name
print(f"モデル {model_name} のロードが完了しました。")
except Exception as e:
print(f"モデルのロード中にエラーが発生しました: {str(e)}")
# gpt-ossモデルでエラーが出た場合、使用不可と表示
if self.is_gpt_oss_model(model_name):
raise Exception(f"gpt-ossモデルのロードに失敗しました。このモデルは現在の環境では使用できません: {str(e)}")
else:
raise
def _generate_response_gpu(self, message: str, history: List[Tuple[str, str]], model_name: str,
temperature: float = 0.7, max_tokens: int = 512) -> str:
"""GPU上で応答を生成する実際の処理"""
try:
# モデルロード
self.load_model(model_name)
if self.is_gpt_oss_model(model_name):
# gpt-ossモデルの場合
return self._generate_with_pipeline(message, history, temperature, max_tokens)
else:
# 通常のモデルの場合
return self._generate_with_model(message, history, temperature, max_tokens)
except Exception as e:
return f"エラー: {str(e)}"
def _generate_with_pipeline(self, message: str, history: List[Tuple[str, str]],
temperature: float, max_tokens: int) -> str:
"""pipelineを使用した生成(gpt-oss用)"""
# device_mapが設定されていない場合のみ手動でGPU移動
if IS_ZEROGPU and hasattr(self.pipeline, 'model') and hasattr(self.pipeline.model, 'to'):
self.pipeline.model.to('cuda')
# gpt-ossはchat format用のmessages形式を使用
messages = []
# 履歴を追加(最新3件のみ)
for user_msg, assistant_msg in history[-3:]:
messages.append({"role": "user", "content": user_msg})
messages.append({"role": "assistant", "content": assistant_msg})
# 現在のメッセージを追加
messages.append({"role": "user", "content": message})
# pipeline経由で生成
outputs = self.pipeline(
messages,
max_new_tokens=max_tokens,
temperature=temperature,
do_sample=True,
top_p=0.95,
return_full_text=False
)
# ZeroGPU環境でのみCPUに戻す(メモリ節約)
if IS_ZEROGPU and hasattr(self.pipeline, 'model') and hasattr(self.pipeline.model, 'to'):
self.pipeline.model.to('cpu')
torch.cuda.empty_cache()
torch.cuda.synchronize()
# レスポンスの処理
response_text = outputs[0]["generated_text"].strip()
# gpt-ossモデルの場合、"assistantfinal"以降のみを抽出
if "assistantfinal" in response_text:
response_text = response_text.split("assistantfinal", 1)[1].strip()
return response_text
def _generate_with_model(self, message: str, history: List[Tuple[str, str]],
temperature: float, max_tokens: int) -> str:
"""通常のモデルを使用した生成"""
# GPUに移動
self.model.to('cuda')
# プロンプト構築
prompt = self._build_prompt(message, history)
# トークナイズ
inputs = self.tokenizer.encode(prompt, return_tensors="pt").to('cuda')
# 生成
with torch.no_grad():
outputs = self.model.generate(
inputs,
max_new_tokens=max_tokens,
temperature=temperature,
do_sample=True,
top_p=0.95,
top_k=50,
repetition_penalty=1.1,
pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=self.tokenizer.eos_token_id
)
# デコード
response = self.tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True)
# CPUに戻す(メモリ節約)
self.model.to('cpu')
torch.cuda.empty_cache()
torch.cuda.synchronize()
return response.strip()
def generate_response(self, message: str, history: List[Tuple[str, str]], model_name: str,
temperature: float = 0.7, max_tokens: int = 512) -> str:
"""メッセージに対する応答を生成"""
if IS_ZEROGPU:
# ZeroGPU環境の場合
return self._generate_response_gpu(message, history, model_name, temperature, max_tokens)
else:
# 通常環境の場合
try:
self.load_model(model_name)
if self.is_gpt_oss_model(model_name):
# gpt-ossモデルの場合
# device_mapが設定されていない場合のみ手動でGPU移動
device = 'cuda' if torch.cuda.is_available() else 'cpu'
if device == 'cuda' and hasattr(self.pipeline, 'model') and hasattr(self.pipeline.model, 'to'):
# device_mapが"auto"でない場合のみ手動移動
self.pipeline.model.to(device)
messages = []
for user_msg, assistant_msg in history[-3:]:
messages.append({"role": "user", "content": user_msg})
messages.append({"role": "assistant", "content": assistant_msg})
messages.append({"role": "user", "content": message})
outputs = self.pipeline(
messages,
max_new_tokens=max_tokens,
temperature=temperature,
do_sample=True,
top_p=0.95,
return_full_text=False
)
response_text = outputs[0]["generated_text"].strip()
# gpt-ossモデルの場合、"assistantfinal"以降のみを抽出
if "assistantfinal" in response_text:
response_text = response_text.split("assistantfinal", 1)[1].strip()
return response_text
# 通常のモデルの場合
device = 'cuda' if torch.cuda.is_available() else 'cpu'
if device == 'cuda':
self.model.to(device)
prompt = self._build_prompt(message, history)
inputs = self.tokenizer.encode(prompt, return_tensors="pt").to(device)
with torch.no_grad():
outputs = self.model.generate(
inputs,
max_new_tokens=max_tokens,
temperature=temperature,
do_sample=True,
top_p=0.95,
top_k=50,
repetition_penalty=1.1,
pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=self.tokenizer.eos_token_id
)
response = self.tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True)
return response.strip()
except Exception as e:
return f"エラー: {str(e)}"
def _build_prompt(self, message: str, history: List[Tuple[str, str]]) -> str:
"""会話履歴からプロンプトを構築(通常のモデル用)"""
prompt = ""
# 履歴を追加(最新3件のみ使用 - メモリ効率のため)
for user_msg, assistant_msg in history[-3:]:
prompt += f"User: {user_msg}\nAssistant: {assistant_msg}\n\n"
# 現在のメッセージを追加
prompt += f"User: {message}\nAssistant: "
return prompt
# ChatBotインスタンス
chatbot = ChatBot()
# ZeroGPU環境の場合、GPUデコレータを適用
if IS_ZEROGPU:
chatbot._generate_response_gpu = spaces.GPU(duration=120)(chatbot._generate_response_gpu)
def respond(message: str, history: List[Tuple[str, str]], model_name: str,
temperature: float, max_tokens: int) -> Tuple[List[Tuple[str, str]], str]:
"""Gradioのコールバック関数"""
if not message:
return history, ""
try:
# 応答生成
response = chatbot.generate_response(message, history, model_name, temperature, max_tokens)
# 履歴に追加
history.append((message, response))
return history, ""
except RuntimeError as e:
if "out of memory" in str(e).lower():
error_msg = "メモリ不足エラー: より小さいモデルを使用するか、最大トークン数を減らしてください。"
else:
error_msg = f"実行時エラー: {str(e)}"
history.append((message, error_msg))
return history, ""
except Exception as e:
error_msg = f"エラーが発生しました: {str(e)}"
history.append((message, error_msg))
return history, ""
def clear_chat() -> Tuple[List, str]:
"""チャット履歴をクリア"""
return [], ""
# Gradio UI
with gr.Blocks(title="ChatGPT Clone", theme=gr.themes.Soft()) as app:
gr.Markdown("# 🤖 ChatGPT Clone")
gr.Markdown("""
日本語対応のLLMを使用したチャットボットです。
**使用可能モデル:**
- [elyza/Llama-3-ELYZA-JP-8B](https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B)
- [Fugaku-LLM/Fugaku-LLM-13B](https://huggingface.co/Fugaku-LLM/Fugaku-LLM-13B)
- [openai/gpt-oss-20b](https://huggingface.co/openai/gpt-oss-20b) - OpenAIの最新オープンウェイト推論モデル
""")
with gr.Row():
with gr.Column(scale=3):
chatbot_ui = gr.Chatbot(
label="Chat",
height=500,
show_label=False,
container=True
)
with gr.Row():
msg_input = gr.Textbox(
label="メッセージを入力",
placeholder="ここにメッセージを入力してください...",
lines=2,
scale=4,
show_label=False
)
send_btn = gr.Button("送信", variant="primary", scale=1)
with gr.Row():
clear_btn = gr.Button("🗑️ 新しい会話", variant="secondary")
with gr.Column(scale=1):
model_select = gr.Dropdown(
choices=[
"elyza/Llama-3-ELYZA-JP-8B",
"Fugaku-LLM/Fugaku-LLM-13B",
"openai/gpt-oss-20b",
],
value="elyza/Llama-3-ELYZA-JP-8B",
label="モデル選択",
interactive=True
)
temperature = gr.Slider(
minimum=0.1,
maximum=1.0,
value=0.7,
step=0.1,
label="Temperature",
info="生成の創造性を調整"
)
max_tokens = gr.Slider(
minimum=64,
maximum=512,
value=256,
step=64,
label="最大トークン数",
info="生成する最大トークン数"
)
gr.Markdown("""
### 使い方
1. モデルを選択
2. メッセージを入力
3. 送信ボタンをクリック
### 注意事項
- 初回のモデル読み込みには時間がかかります
- ZeroGPU使用により高速推論が可能
- 1回の生成は120秒以内に完了します
- 大きなモデル使用時は、短めの応答になる場合があります
- gpt-oss-20bは推論専用モデルで、harmony formatを使用します
""")
# イベントハンドラ
msg_input.submit(
fn=respond,
inputs=[msg_input, chatbot_ui, model_select, temperature, max_tokens],
outputs=[chatbot_ui, msg_input]
)
send_btn.click(
fn=respond,
inputs=[msg_input, chatbot_ui, model_select, temperature, max_tokens],
outputs=[chatbot_ui, msg_input]
)
clear_btn.click(
fn=clear_chat,
outputs=[chatbot_ui, msg_input]
)
if __name__ == "__main__":
# Hugging Face Spaces環境かどうかを確認
is_hf_spaces = os.getenv("SPACE_ID") is not None
app.launch(
share=False,
show_error=True,
server_name="0.0.0.0" if is_hf_spaces else "127.0.0.1",
server_port=7860
)