贡献 / #1808

#1808 serve: a queue cap (max_queue) answers 429 + Retry-After instead of queueing without end

open · @streber42 · 0 评论 · 去 GitHub 看

Server & APINVIDIA / CUDADocumentation

说明

Issue: Resolves #1809

## Summary
`"max_queue": N` in `strata-<model>.json` (or `--max-queue N`) lets at most N generation requests wait beyond the running slots. One more gets an immediate **429** with a whole-second `Retry-After` (1-60 s), sent before any byte of the answer, so a streamed request never starts a stream it cannot finish. If it is unset, the queue has no limit, as before; `0` rejects whatever cannot start at once.

The status is 429 and not 503 because litellm and the OpenAI and Anthropic SDKs retry a 429 and read it as "busy". litellm cools the deployment down for `Retry-After` and moves to another deployment in the group, or waits and retries when Strata is the only one. A 503 counts as a server failure there.

## What changed
- `serve/server.py`
  - **Admission:** `Service.admit()` / `release()` count admitted requests (running and waiting) under `status_lock`. They use a new counter instead of `status["queued"]`, which reads about 0 while `--batch` requests wait on the slots. The counter lives on the Service, so an engine restart (#1012) keeps it.
  - **Where the check runs:** `do_POST` checks right after `begin_request` for `/v1/chat/completions`, `/v1/messages` and `/v1/responses` only, and releases in its `finally`, so every way out (disconnect, `EngineDied`, a 400) frees the place.
  - **Retry-After:** `Service.retry_after()` is the mean `duration_s` of the last 8 finished requests divided by the slots, rounded up and clamped to 1-60 s, or 10 s before any request has finished. It is a whole number because litellm reads it with `int()`.
  - **429 body:** each dialect gets its own error shape (OpenAI `rate_limit_error` / `rate_limit_exceeded`, Anthropic `rate_limit_error`, the Responses error body). `_json` gains an optional `headers` argument for the `Retry-After` header.
  - **Metrics:** `/metrics` gains `totals.rejected`, `live.inflight` and `live.max_queue`, and `/v1/status` `activity` gains `admitted`, `max_queue` and `rejected`. No field was renamed.
  - **Settings:** `--max-queue`; a bad config value stops the start with a message, and the start prints one line when the cap is on.
- `serve/runconfig.py`: `max_queue` is editable from the web page's Model settings (`int>=0`, empty means no limit).
- `serve/test_queue_cap.py`: 25 tests.
  - Unit tests: the capacity, max_queue 0, the release floor, and the Retry-After estimate and clamps.
  - Over HTTP with the fake batch engine: a 429 with an integer `Retry-After` and the right body for all three dialects; a streamed request over the cap gets a JSON 429, not a stream; count_tokens and status are not limited; no limit when unset; the count returns to 0 after a finished, streamed, 400, `EngineDied` and disconnected request.
- `docs/DETAILS.md`: a "Limiting the queue" section with measurements and notes for litellm, the `/metrics` field, and the web-editable key.

## Extra Notes
- **Tests:** `python -m unittest serve.test_queue_cap serve.test_runconfig serve.test_parallel` gives 57 OK on this branch, which is current `main` with the commit on top. The full `serve` suite runs 622 tests; its 7 errors are all `No module named 'regex'` in my Python, in `test_detok` and `test_server`, and none come from this change.
- **Measured** on two RTX 3090s (layer split), UD-Q4_K_XL, 0.1.40.1 with this change, one request at a time:
  - With `max_queue` 2: of 5 requests sent at once, 3 were answered in 5.1, 7.7 and 10.1 s, and 2 got the 429 in under 2 ms.
  - Behind litellm with live traffic and `max_queue` 4: the `Retry-After` it sent over 5,379 rejections had a median of 8 s and a maximum of 31 s.
  - Over 10.6 hours it answered 3,885 requests and rejected 3,154 attempts, litellm's retries included.
- **Related:** #1619 / #1673 (a ping while a request waits for the turn) tackles the other side of the same wait. #1673 keeps a waiting client alive, and this PR limits how many can wait. They merge without conflicts, and with both applied, `serve.test_queue_cap`, `serve.test_parallel` and `serve.test_server` (including #1673's `QueuedHeartbeat`) pass; the only errors are the same `regex` ones.
- **Open questions:**
  - Requests from Strata's own web page count toward the cap. The page already shows the server's message ("the queue is full ...; retry after N s").
  - The estimate ignores how long the running requests have already gone. Under load they are often past the mean, so subtracting it would give 1 s and spend a client's retries in seconds.
  - With several slots, prompts are still read one at a time, so the estimate is rough there.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

本站相关内容

相关页面的快捷入口。