Arena Agent commited on
Commit
32935ed
·
0 Parent(s):

Initial deploy

Browse files
Files changed (3) hide show
  1. Dockerfile +17 -0
  2. app.py +172 -0
  3. requirements.txt +15 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ PORT=7860 \
7
+ MODEL_NAME=gemma-4-E4B-it \
8
+ MODEL_SIZE=5 \
9
+ MODEL_CONTEXT=128000
10
+
11
+ WORKDIR /app
12
+ RUN apt-get update && apt-get install -y --no-install-recommends git curl build-essential && rm -rf /var/lib/apt/lists/*
13
+ COPY requirements.txt /app/requirements.txt
14
+ RUN pip install --upgrade pip && pip install -r /app/requirements.txt
15
+ COPY app.py /app/app.py
16
+ EXPOSE 7860
17
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import time
4
+ from typing import Optional
5
+ from datetime import datetime
6
+ from functools import lru_cache
7
+
8
+ from fastapi import FastAPI, HTTPException
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from pydantic import BaseModel
11
+ import torch
12
+ from transformers import AutoTokenizer, AutoModelForCausalLM
13
+
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ MODEL_NAME = os.getenv("MODEL_NAME", "gemma-4-E4B-it")
18
+ MODEL_SIZE = int(os.getenv("MODEL_SIZE", "5"))
19
+ MODEL_CONTEXT = int(os.getenv("MODEL_CONTEXT", "128000"))
20
+ HF_REPO_ID = os.getenv("HF_REPO_ID", f"google/{MODEL_NAME}")
21
+ QUANTIZATION = os.getenv("QUANTIZATION", "Q4_K_M")
22
+
23
+ DEVICE = "cpu"
24
+ if torch.cuda.is_available():
25
+ DEVICE = "cuda"
26
+ elif getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
27
+ DEVICE = "mps"
28
+
29
+ app = FastAPI(title=f"Gemma Inference API - {MODEL_NAME}")
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ class ChatMessage(BaseModel):
39
+ role: str
40
+ content: str
41
+
42
+ class InferenceRequest(BaseModel):
43
+ messages: list[ChatMessage]
44
+ model: str = MODEL_NAME
45
+ temperature: float = 0.7
46
+ max_tokens: int = 512
47
+ top_p: float = 0.9
48
+ top_k: int = 50
49
+ thinking: bool = False
50
+ system_prompt: Optional[str] = None
51
+
52
+ class InferenceResponse(BaseModel):
53
+ model: str
54
+ response: str
55
+ tokens_used: int
56
+ latency_ms: float
57
+ thinking: Optional[str] = None
58
+ timestamp: str
59
+
60
+ class HealthResponse(BaseModel):
61
+ status: str
62
+ model: str
63
+ device: str
64
+ model_size_gb: int
65
+ context_window: int
66
+
67
+ @lru_cache(maxsize=1)
68
+ def get_tokenizer():
69
+ logger.info(f"Loading tokenizer for {HF_REPO_ID}...")
70
+ return AutoTokenizer.from_pretrained(HF_REPO_ID)
71
+
72
+ @lru_cache(maxsize=1)
73
+ def get_model():
74
+ logger.info(f"Loading model {HF_REPO_ID} on {DEVICE}...")
75
+ kwargs = dict(low_cpu_mem_usage=True)
76
+ if DEVICE == "cuda":
77
+ kwargs["torch_dtype"] = torch.float16
78
+ model = AutoModelForCausalLM.from_pretrained(HF_REPO_ID, device_map="auto", **kwargs)
79
+ elif DEVICE == "cpu" and MODEL_SIZE > 10:
80
+ kwargs["torch_dtype"] = torch.float32
81
+ try:
82
+ model = AutoModelForCausalLM.from_pretrained(HF_REPO_ID, device_map="auto", load_in_4bit=True, **kwargs)
83
+ except Exception:
84
+ model = AutoModelForCausalLM.from_pretrained(HF_REPO_ID, **kwargs)
85
+ else:
86
+ kwargs["torch_dtype"] = torch.float32
87
+ model = AutoModelForCausalLM.from_pretrained(HF_REPO_ID, **kwargs)
88
+ return model
89
+
90
+ def build_chat_prompt(messages: list[ChatMessage], system_prompt: Optional[str] = None) -> str:
91
+ parts = []
92
+ if system_prompt:
93
+ parts.append(f"<|system|>\n{system_prompt}<|end_of_turn|>\n")
94
+ for msg in messages:
95
+ parts.append(f"<|{msg.role}|>\n{msg.content}<|end_of_turn|>\n")
96
+ parts.append("<|assistant|>\n")
97
+ return "".join(parts)
98
+
99
+ @app.get("/health", response_model=HealthResponse)
100
+ async def health_check():
101
+ return HealthResponse(status="healthy", model=MODEL_NAME, device=DEVICE, model_size_gb=MODEL_SIZE, context_window=MODEL_CONTEXT)
102
+
103
+ @app.get("/info")
104
+ async def model_info():
105
+ return {"model": MODEL_NAME, "repo_id": HF_REPO_ID, "device": DEVICE, "model_size_gb": MODEL_SIZE, "context_window": MODEL_CONTEXT, "quantization": QUANTIZATION}
106
+
107
+ @app.post("/infer", response_model=InferenceResponse)
108
+ async def infer(request: InferenceRequest):
109
+ start_time = time.time()
110
+ try:
111
+ tokenizer = get_tokenizer()
112
+ model = get_model()
113
+ prompt = build_chat_prompt(request.messages, system_prompt=request.system_prompt)
114
+ if request.thinking:
115
+ prompt = "<|think|>\n" + prompt
116
+ inputs = tokenizer(prompt, return_tensors="pt")
117
+ model_device = getattr(model, 'device', None)
118
+ if model_device is not None:
119
+ inputs = {k: v.to(model_device) for k, v in inputs.items()}
120
+ input_length = inputs['input_ids'].shape[1]
121
+ max_new = max(1, min(request.max_tokens, MODEL_CONTEXT - input_length))
122
+ outputs = model.generate(
123
+ **inputs,
124
+ max_new_tokens=max_new,
125
+ temperature=request.temperature,
126
+ top_p=request.top_p,
127
+ top_k=request.top_k,
128
+ do_sample=request.temperature > 0,
129
+ pad_token_id=tokenizer.eos_token_id,
130
+ )
131
+ generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
132
+ response_text = generated_text.split("<|assistant|>")[-1].strip() if "<|assistant|>" in generated_text else generated_text
133
+ latency_ms = (time.time() - start_time) * 1000
134
+ tokens_generated = outputs.shape[1] - input_length
135
+ return InferenceResponse(model=MODEL_NAME, response=response_text, tokens_used=int(tokens_generated), latency_ms=latency_ms, timestamp=datetime.utcnow().isoformat())
136
+ except Exception as e:
137
+ logger.exception("Inference error")
138
+ raise HTTPException(status_code=500, detail=str(e))
139
+
140
+ @app.post("/chat")
141
+ async def chat(request: InferenceRequest):
142
+ return await infer(request)
143
+
144
+ @app.post("/complete")
145
+ async def complete(request: InferenceRequest):
146
+ if not request.messages:
147
+ raise HTTPException(status_code=400, detail="No messages provided")
148
+ simple_request = InferenceRequest(
149
+ messages=[ChatMessage(role="user", content=request.messages[-1].content)],
150
+ temperature=request.temperature,
151
+ max_tokens=request.max_tokens,
152
+ top_p=request.top_p,
153
+ top_k=request.top_k,
154
+ thinking=request.thinking,
155
+ system_prompt=request.system_prompt,
156
+ )
157
+ return await infer(simple_request)
158
+
159
+ @app.get("/")
160
+ async def root():
161
+ return {
162
+ "name": "Gemma Inference API",
163
+ "model": MODEL_NAME,
164
+ "version": "1.0",
165
+ "endpoints": {
166
+ "/health": "Health check",
167
+ "/info": "Model information",
168
+ "/infer": "Run inference (POST)",
169
+ "/chat": "Chat interface (POST)",
170
+ "/complete": "Text completion (POST)"
171
+ }
172
+ }
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ python-multipart==0.0.6
4
+ pydantic==2.5.0
5
+ requests==2.31.0
6
+ httpx==0.25.2
7
+ torch==2.1.2
8
+ transformers==4.36.2
9
+ huggingface-hub==0.20.1
10
+ numpy==1.26.3
11
+ python-dotenv==1.0.0
12
+ sentencepiece==0.1.99
13
+ accelerate==0.25.0
14
+ bitsandbytes==0.42.0
15
+ openai==1.3.7