fomext commited on
Commit
bcf5e24
·
verified ·
1 Parent(s): 5f2a604

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +55 -27
app.py CHANGED
@@ -10,6 +10,7 @@ import os
10
  import time
11
  import uuid
12
  import json
 
13
  import asyncio
14
  import logging
15
  import threading
@@ -49,6 +50,12 @@ _llm_lock = threading.Lock()
49
  _llm_ready = threading.Event() # set once the model is loaded
50
  _llm_error: Optional[str] = None # set if loading failed
51
 
 
 
 
 
 
 
52
 
53
  def _download_model() -> None:
54
  """Download the GGUF file from MODEL_URL if MODEL_PATH doesn't exist."""
@@ -196,27 +203,46 @@ def _make_chunk(delta_content: str, finish_reason: Optional[str], request_id: st
196
  return f"data: {json.dumps(chunk)}\n\n"
197
 
198
 
199
- async def _stream_response(request: ChatCompletionRequest, request_id: str) -> AsyncIterator[str]:
200
- llm = _get_llm()
201
- messages = [{"role": m.role, "content": m.content} for m in request.messages]
202
  loop = asyncio.get_event_loop()
203
-
204
- def _run():
205
- return llm.create_chat_completion(
206
- messages=messages,
207
- max_tokens=request.max_tokens,
208
- temperature=request.temperature,
209
- top_p=request.top_p,
210
- stop=request.stop or [],
211
- stream=True,
212
- )
213
-
214
- gen = await loop.run_in_executor(None, _run)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  yield _make_chunk("", None, request_id) # opening delta
217
 
218
- for chunk in gen:
219
- choice = chunk["choices"][0]
 
 
 
 
 
220
  delta = choice.get("delta", {})
221
  content = delta.get("content", "")
222
  finish = choice.get("finish_reason")
@@ -265,14 +291,15 @@ async def list_models():
265
 
266
  @app.post("/v1/chat/completions")
267
  async def chat_completions(request: ChatCompletionRequest):
268
- llm = _get_llm() # raises 503 if not ready
 
269
 
270
  messages = [{"role": m.role, "content": m.content} for m in request.messages]
271
 
272
  if request.stream:
273
  request_id = f"chatcmpl-{uuid.uuid4().hex}"
274
  return StreamingResponse(
275
- _stream_response(request, request_id),
276
  media_type="text/event-stream",
277
  headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
278
  )
@@ -281,14 +308,15 @@ async def chat_completions(request: ChatCompletionRequest):
281
  loop = asyncio.get_event_loop()
282
 
283
  def _run():
284
- return llm.create_chat_completion(
285
- messages=messages,
286
- max_tokens=request.max_tokens,
287
- temperature=request.temperature,
288
- top_p=request.top_p,
289
- stop=request.stop or [],
290
- stream=False,
291
- )
 
292
 
293
  result = await loop.run_in_executor(None, _run)
294
  choice = result["choices"][0]
 
10
  import time
11
  import uuid
12
  import json
13
+ import queue
14
  import asyncio
15
  import logging
16
  import threading
 
50
  _llm_ready = threading.Event() # set once the model is loaded
51
  _llm_error: Optional[str] = None # set if loading failed
52
 
53
+ # llama.cpp contexts are NOT safe to call concurrently -- there's a single
54
+ # KV cache / sampling state shared by every call into the same Llama
55
+ # instance. This lock serializes all generation calls (streaming and
56
+ # non-streaming) so two requests can never run inference at the same time.
57
+ _inference_lock = threading.Lock()
58
+
59
 
60
  def _download_model() -> None:
61
  """Download the GGUF file from MODEL_URL if MODEL_PATH doesn't exist."""
 
203
  return f"data: {json.dumps(chunk)}\n\n"
204
 
205
 
206
+ async def _stream_response(llm: Llama, messages: list, request: ChatCompletionRequest, request_id: str) -> AsyncIterator[str]:
 
 
207
  loop = asyncio.get_event_loop()
208
+ q: "queue.Queue" = queue.Queue()
209
+ _SENTINEL = object()
210
+
211
+ def _produce():
212
+ # Holds the lock for the *entire* generation, not just creation --
213
+ # this is the only place token generation actually happens, and it
214
+ # must never overlap with another request's call into the same
215
+ # Llama instance.
216
+ with _inference_lock:
217
+ try:
218
+ gen = llm.create_chat_completion(
219
+ messages=messages,
220
+ max_tokens=request.max_tokens,
221
+ temperature=request.temperature,
222
+ top_p=request.top_p,
223
+ stop=request.stop or [],
224
+ stream=True,
225
+ )
226
+ for chunk in gen:
227
+ q.put(chunk)
228
+ except Exception as exc: # surfaced to the consumer below
229
+ q.put(exc)
230
+ finally:
231
+ q.put(_SENTINEL)
232
+
233
+ # Fire-and-forget: runs on a worker thread, the consumer below just
234
+ # drains the queue without ever blocking the asyncio event loop.
235
+ loop.run_in_executor(None, _produce)
236
 
237
  yield _make_chunk("", None, request_id) # opening delta
238
 
239
+ while True:
240
+ item = await loop.run_in_executor(None, q.get)
241
+ if item is _SENTINEL:
242
+ break
243
+ if isinstance(item, Exception):
244
+ raise item
245
+ choice = item["choices"][0]
246
  delta = choice.get("delta", {})
247
  content = delta.get("content", "")
248
  finish = choice.get("finish_reason")
 
291
 
292
  @app.post("/v1/chat/completions")
293
  async def chat_completions(request: ChatCompletionRequest):
294
+ llm = _get_llm() # raises 503/500 *before* we commit to a response,
295
+ # including for the streaming branch below
296
 
297
  messages = [{"role": m.role, "content": m.content} for m in request.messages]
298
 
299
  if request.stream:
300
  request_id = f"chatcmpl-{uuid.uuid4().hex}"
301
  return StreamingResponse(
302
+ _stream_response(llm, messages, request, request_id),
303
  media_type="text/event-stream",
304
  headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
305
  )
 
308
  loop = asyncio.get_event_loop()
309
 
310
  def _run():
311
+ with _inference_lock: # never overlap with another generation call
312
+ return llm.create_chat_completion(
313
+ messages=messages,
314
+ max_tokens=request.max_tokens,
315
+ temperature=request.temperature,
316
+ top_p=request.top_p,
317
+ stop=request.stop or [],
318
+ stream=False,
319
+ )
320
 
321
  result = await loop.run_in_executor(None, _run)
322
  choice = result["choices"][0]