tommytracx commited on
Commit
45f3b9d
·
verified ·
1 Parent(s): 4dda59b

Keep interactive streams live during inference

Browse files

Exact source: ttracx/thoxroute@f56b047f0c269ed6559d728ad42e06cd8bb54201

Files changed (3) hide show
  1. Dockerfile +2 -0
  2. README.md +3 -2
  3. app.py +65 -29
Dockerfile CHANGED
@@ -25,6 +25,8 @@ ENV HF_HOME=/home/mambauser/.cache/huggingface \
25
  THOX_FAST_MODEL_FILE=qwen2.5-0.5b-instruct-q4_k_m.gguf \
26
  THOX_FAST_POOL_SIZE=1 \
27
  THOX_FAST_QUEUE_TIMEOUT_S=6 \
 
 
28
  THOX_MAX_OUTPUT_TOKENS=128
29
 
30
  # Bake the immutable public interactive model into the image. A RUNNING Space
 
25
  THOX_FAST_MODEL_FILE=qwen2.5-0.5b-instruct-q4_k_m.gguf \
26
  THOX_FAST_POOL_SIZE=1 \
27
  THOX_FAST_QUEUE_TIMEOUT_S=6 \
28
+ THOX_FAST_MAX_OUTPUT_TOKENS=16 \
29
+ THOX_STREAM_HEARTBEAT_S=2 \
30
  THOX_MAX_OUTPUT_TOKENS=128
31
 
32
  # Bake the immutable public interactive model into the image. A RUNNING Space
README.md CHANGED
@@ -19,7 +19,8 @@ This Space exposes one OpenAI-compatible endpoint:
19
  `GET /healthz` is ready only after the interactive context is loaded and has
20
  completed a one-token startup warmup. Streaming sends a role chunk before model
21
  evaluation so upstream time-to-first-byte watchdogs do not abandon healthy CPU
22
- work. First-party 15-second requests are capped at 16 output tokens by
23
- ThoxRoute.
 
24
  The interactive model is pinned to immutable model revision
25
  `9217f5db79a29953eb74d5343926648285ec7e67` and baked into the image.
 
19
  `GET /healthz` is ready only after the interactive context is loaded and has
20
  completed a one-token startup warmup. Streaming sends a role chunk before model
21
  evaluation so upstream time-to-first-byte watchdogs do not abandon healthy CPU
22
+ work; two-second SSE comments keep the stream live during synchronous prompt
23
+ evaluation. The interactive provider itself caps every request at 16 output
24
+ tokens, so malformed or older callers cannot leave minutes of abandoned work.
25
  The interactive model is pinned to immutable model revision
26
  `9217f5db79a29953eb74d5343926648285ec7e67` and baked into the image.
app.py CHANGED
@@ -50,6 +50,12 @@ FAST_QUEUE_TIMEOUT_S = max(
50
  MAX_OUTPUT_TOKENS = max(
51
  1, min(int(os.environ.get("THOX_MAX_OUTPUT_TOKENS", "128")), 512)
52
  )
 
 
 
 
 
 
53
  MAX_MESSAGES = 64
54
  MAX_MESSAGE_CHARS = 65_536
55
  MAX_REQUEST_CHARS = 131_072
@@ -219,7 +225,8 @@ def _messages(req: ChatRequest) -> list[dict[str, str]]:
219
 
220
 
221
  def _max_tokens(req: ChatRequest) -> int:
222
- return min(req.max_tokens, MAX_OUTPUT_TOKENS)
 
223
 
224
 
225
  def _runtime() -> Runtime:
@@ -240,6 +247,8 @@ def healthz() -> dict[str, Any]:
240
  "interactive_pool_size": FAST_POOL_SIZE,
241
  "interactive_available": active.interactive_available,
242
  "specialist_model": CODER_MODEL_ID,
 
 
243
  "max_output_tokens": MAX_OUTPUT_TOKENS,
244
  "n_ctx": N_CTX,
245
  "threads": _usable_cpus(),
@@ -253,31 +262,61 @@ def _completion_id() -> str:
253
  def _stream_completion(req: ChatRequest, lease: Lease) -> Iterator[bytes]:
254
  completion_id = _completion_id()
255
  created = int(time.time())
256
- try:
257
- # Send a valid role chunk before llama prompt evaluation. This proves
258
- # the stream is alive inside ThoxRoute's short time-to-first-byte budget
259
- # and prevents a healthy CPU inference from being abandoned at 12 s.
260
- initial = {
261
- "id": completion_id,
262
- "object": "chat.completion.chunk",
263
- "created": created,
264
- "model": req.model,
265
- "choices": [
266
- {
267
- "index": 0,
268
- "delta": {"role": "assistant", "content": ""},
269
- "finish_reason": None,
270
- }
271
- ],
272
- }
273
- yield f"data: {json.dumps(initial, separators=(',', ':'))}\n\n".encode()
274
- chunks = lease.model.create_chat_completion(
275
- messages=_messages(req),
276
- max_tokens=_max_tokens(req),
277
- temperature=req.temperature,
278
- stream=True,
279
- )
280
- for chunk in chunks:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  choice = chunk.get("choices", [{}])[0]
282
  payload = {
283
  "id": completion_id,
@@ -293,9 +332,6 @@ def _stream_completion(req: ChatRequest, lease: Lease) -> Iterator[bytes]:
293
  ],
294
  }
295
  yield f"data: {json.dumps(payload, separators=(',', ':'))}\n\n".encode()
296
- yield b"data: [DONE]\n\n"
297
- finally:
298
- lease.release()
299
 
300
 
301
  @api.post("/v1/chat/completions")
 
50
  MAX_OUTPUT_TOKENS = max(
51
  1, min(int(os.environ.get("THOX_MAX_OUTPUT_TOKENS", "128")), 512)
52
  )
53
+ FAST_MAX_OUTPUT_TOKENS = max(
54
+ 1, min(int(os.environ.get("THOX_FAST_MAX_OUTPUT_TOKENS", "16")), 32)
55
+ )
56
+ STREAM_HEARTBEAT_S = max(
57
+ 0.25, min(float(os.environ.get("THOX_STREAM_HEARTBEAT_S", "2")), 5.0)
58
+ )
59
  MAX_MESSAGES = 64
60
  MAX_MESSAGE_CHARS = 65_536
61
  MAX_REQUEST_CHARS = 131_072
 
225
 
226
 
227
  def _max_tokens(req: ChatRequest) -> int:
228
+ ceiling = FAST_MAX_OUTPUT_TOKENS if req.model == FAST_MODEL_ID else MAX_OUTPUT_TOKENS
229
+ return min(req.max_tokens, ceiling)
230
 
231
 
232
  def _runtime() -> Runtime:
 
247
  "interactive_pool_size": FAST_POOL_SIZE,
248
  "interactive_available": active.interactive_available,
249
  "specialist_model": CODER_MODEL_ID,
250
+ "interactive_max_output_tokens": FAST_MAX_OUTPUT_TOKENS,
251
+ "stream_heartbeat_s": STREAM_HEARTBEAT_S,
252
  "max_output_tokens": MAX_OUTPUT_TOKENS,
253
  "n_ctx": N_CTX,
254
  "threads": _usable_cpus(),
 
262
  def _stream_completion(req: ChatRequest, lease: Lease) -> Iterator[bytes]:
263
  completion_id = _completion_id()
264
  created = int(time.time())
265
+ items: queue.Queue[dict[str, Any] | object] = queue.Queue()
266
+ complete = object()
267
+ failed = object()
268
+
269
+ def generate() -> None:
270
+ try:
271
+ chunks = lease.model.create_chat_completion(
272
+ messages=_messages(req),
273
+ max_tokens=_max_tokens(req),
274
+ temperature=req.temperature,
275
+ stream=True,
276
+ )
277
+ for chunk in chunks:
278
+ items.put(chunk)
279
+ items.put(complete)
280
+ except Exception:
281
+ # Provider-controlled exception text must never cross the API boundary.
282
+ items.put(failed)
283
+ finally:
284
+ # A disconnected client closes the response generator, but llama.cpp
285
+ # can still be using the context. The worker therefore owns release.
286
+ lease.release()
287
+
288
+ threading.Thread(target=generate, daemon=True, name="thox-fast-generation").start()
289
+
290
+ # Prove liveness before prompt evaluation, then emit SSE comments while the
291
+ # synchronous llama context is busy. Upstream read timers reset on every
292
+ # heartbeat and comments are ignored by OpenAI-compatible parsers.
293
+ initial = {
294
+ "id": completion_id,
295
+ "object": "chat.completion.chunk",
296
+ "created": created,
297
+ "model": req.model,
298
+ "choices": [
299
+ {
300
+ "index": 0,
301
+ "delta": {"role": "assistant", "content": ""},
302
+ "finish_reason": None,
303
+ }
304
+ ],
305
+ }
306
+ yield f"data: {json.dumps(initial, separators=(',', ':'))}\n\n".encode()
307
+ while True:
308
+ try:
309
+ item = items.get(timeout=STREAM_HEARTBEAT_S)
310
+ except queue.Empty:
311
+ yield b": thox-fast-heartbeat\n\n"
312
+ continue
313
+ if item is complete:
314
+ yield b"data: [DONE]\n\n"
315
+ return
316
+ if item is failed:
317
+ raise RuntimeError("interactive generation failed")
318
+ if isinstance(item, dict):
319
+ chunk = item
320
  choice = chunk.get("choices", [{}])[0]
321
  payload = {
322
  "id": completion_id,
 
332
  ],
333
  }
334
  yield f"data: {json.dumps(payload, separators=(',', ':'))}\n\n".encode()
 
 
 
335
 
336
 
337
  @api.post("/v1/chat/completions")