You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
docs: add the OpenAI-compatible batch API proposal - #1812
Adds the design proposal for the OpenAI-compatible Batch API discussed in #1291, under docs/proposal/.
Neither vLLM nor SGLang serves /v1/files or /v1/batches on its online server (vLLM's batch mode is the offline vllm run-batch, which starts its own engine, and its /v1/chat/completions/batch is a single synchronous request), so Kthena owns the batch lifecycle and sends each input line through the router's own request path.
What the proposal covers:
API: OpenAI-compatible /v1/files and /v1/batches, the same object shapes, line format, status flow and error envelope, with every difference listed.
State: job records, queue, leases and checkpoints in Redis; file bytes behind a FileStore interface, first backed by a shared volume.
Ownership: a renewable lease and a random fence token, so one replica owns a batch and a partitioned owner cannot corrupt the new owner's work.
Resumption: per-owner result segments with a valid-byte prefix, plus a done bitmap, so every custom_id is answered exactly once in the committed output across a takeover.
Dispatch: each line goes through the router's handler chain in an engine that binds no listener, so route matching, rate limiting, fairness and the KV-cache aware plugins all apply, and the identity cannot be forged from the network.
Failures and limits: temporary failures retry instead of failing the line, and per-batch and per-tenant storage limits plus a sweeper keep one batch from filling the volume.
Operations: off by default, Helm values, an optional worker pool, readiness that never affects interactive traffic, cleanup, metrics, and a test plan.
The numbers in it come from a working proof of concept: 20k-request batches complete with 0 missing and 0 duplicated custom_id across a forced pod kill, and router memory stays at 15-40 MiB at 50k requests. Testing the proof of concept against this design found gaps, and the proposal lists each one with what the design does instead.
Documentation only, no code. Once the design is agreed I'll send the implementation as small serial PRs, in the order under Implementation Plan.
Five questions are open, each with a recommendation, under Decisions needed: the dispatch hook, how batch lines count against rate limits and fairness, the Redis persistence requirement, session stickiness, and one model per input file.
This pull-request has been approved by: Once this PR has been reviewed and has the lgtm label, please assign git-malu for approval. For more information see the Kubernetes Code Review Process.
The full list of commands accepted by this bot can be found here.
Details
Needs approval from an approver in each of these files:
The reason will be displayed to describe this comment to others. Learn more.
Read the full proposal and verified the codebase claims (the /v1/*path catch-all at cmd/kthena-router/app/router.go:296, common.UserIdKey, fairness keying). The overall shape — lease + fence, per-ownership segments with valid-byte prefixes, done bitmap, in-process dispatch — is sound, and the PoC evidence is appreciated. Left a few design questions inline; the main one is how the context-carried identity bridges into the gin middleware chain, since fairness/rate-limiting/access-log all key off gin context state set by middleware.
The reason will be displayed to describe this comment to others. Learn more.
Could the proposal specify exactly where in the chain a dispatched line enters? Today /v1 requests flow through AccessLogMiddleware and AuthMiddleware before router.HandlerFunc() (cmd/kthena-router/app/router.go:293-296), and per-user rate limiting / fairness read c.GetString(common.UserIdKey), which is set only by the auth middleware from the verified token (filters/auth/authentication.go:306).
If batch dispatch enters at HandlerFunc() directly, UserIdKey is never set, so batch lines run with an empty user — the "same rate limits, the same fairness queue, the same access log" claim wouldn't hold.
If it goes through the full gin engine, the auth middleware will reject lines that carry no credentials, so it must be taught to trust the context-carried identity — a security-sensitive change that deserves to be spelled out here (which code checks the context, and how it's kept unreachable from real network requests).
Either answer is fine, but the bridging mechanism is the crux of this design decision and should be explicit in the doc.
The reason will be displayed to describe this comment to others. Learn more.
doc didn't say, my bad. lines go through a second gin engine, not the network one -- recovery -> batchIdentity -> handler, never bound to a listener. batchIdentity stands in for AuthMiddleware: reads identity off the request context, 403 if missing, sets UserIdKey + gateway keys. real AuthMiddleware untouched. written up under Dispatch now
The reason will be displayed to describe this comment to others. Learn more.
Story 2 says "enable the batch worker on two of six router replicas", but the chart renders a single router Deployment, so worker.enabled applies to all replicas uniformly. How does an operator actually achieve the two-of-six split — a second Deployment / separate router pool with different values? Worth stating, since the isolation story (and the per-replica p99 numbers) leans on this capability.
The reason will be displayed to describe this comment to others. Learn more.
yeahh right, one Deployment so it's uniform. story 2 now says two router pools sharing one redis + volume, and Configuration spells out that the split needs a second chart release.
The reason will be displayed to describe this comment to others. Learn more.
The queue is FIFO by enqueue time with no per-tenant limits: one tenant queuing many 50k-line batches starves everyone else, and nothing bounds the total number of queued jobs. Consider per-tenant caps (max queued / max active batches) in the limits block — OpenAI enforces per-org batch queue limits for the same reason. Fine as a stated follow-up, but worth a sentence now.
The reason will be displayed to describe this comment to others. Learn more.
okiee agreed, nothing bounds it today. added maxQueuedBatchesPerTenant/maxActiveBatchesPerTenant as not-built plus a risks row. per-tenant index already exists, so it's really just the admission check on create and skipping a capped tenant in the claim loop.
The reason will be displayed to describe this comment to others. Learn more.
What happens when a response exceeds maxResponseBytes? batchWriter "collects the body up to a cap" — silent truncation would produce an invalid JSON line in the output file. Presumably the line should be recorded in the error file instead; worth specifying.
The reason will be displayed to describe this comment to others. Learn more.
it's not truncated -- the line is marked failed and written to the error file as response_too_large, counted in request_counts.failed. doc didn't say so though..
added
| One tenant starves the queue | Not yet addressed: the queue is FIFO with no per-tenant bound. `maxQueuedBatchesPerTenant` and `maxActiveBatchesPerTenant` are proposed in [Configuration](#configuration) |
| Abandoned files fill the volume | Files carry an expiry; a batch holds a reference on its input so a delete keeps the bytes until the batch is done; reap drops finished jobs |
Separate router pools do not isolate model backend capacity
docs/proposal/kthena-router-batch-api.md:162
Separate router pools do not isolate the model backends. Both deployments still route to the same ModelServers, and this proposal says adaptive back-off is not yet built; batch requests can therefore consume backend capacity and raise interactive latency. The worker switch isolates router-side CPU only, so this story cannot promise an unaffected interactive p99 without backend partitioning or priority/adaptive flow control.
The statement that all Redis keys are prefixed is misleading: the key table includes generic queue, active, and fence keys and unscoped type prefixes. Since Redis is optional today and can be shared by multiple router installations, an operator can cause batch state to collide with another deployment; specify a configurable installation prefix (while keeping the same prefix across the intentional API/worker pools).
Require atomic input reference acquisition
docs/proposal/kthena-router-batch-api.md:565
This race is acknowledged but left as an optional follow-up even though it violates the stated invariant that a delete keeps input bytes alive until batch creation is complete. A delete can remove the bytes between validation and fileref insertion, so atomic reference acquisition (or the documented recheck-and-release protocol) needs to be a required part of batch creation before implementation proceeds.
@YaoZengzeng
one more from going through this: batchIdentity restores UserIdKey and the gateway keys but not common.JWTTokenKey, so a ModelServer with sessionKeySource JWTClaim resolves to an empty session key on batch lines
(extract.go:77 -> ClaimFromContext). replaying the raw token would put a bearer credential in redis for the life of the batch and it can expire mid-run, so my instinct is to resolve the session key once at create time and replay just that string.
written up under Dispatch as a flagged decision. does that direction look right, or would you rather phase 1 declares JWTClaim affinity unsupported for batch?
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Critical unresolved issues remain in batch endpoint validation and failed-batch result completeness, alongside additional correctness, quota, worker-configuration, and volume-initialization gaps.
@YaoZengzeng pushed an update, it covers your comments and the open bot findings now. ptal when you get a chance ta!!
copilot left 6 more on the last push, all small edge cases (endpoint check, failed batches, volume-id, tenant quota, worker config). i have fixes ready but holding them so the doc doesn't keep growing, it's already big. happy to push them if you want
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What type of PR is this?
/kind documentation
What this PR does / why we need it:
Adds the design proposal for the OpenAI-compatible Batch API discussed in #1291, under
docs/proposal/.Neither vLLM nor SGLang serves
/v1/filesor/v1/batcheson its online server (vLLM's batch mode is the offlinevllm run-batch, which starts its own engine, and its/v1/chat/completions/batchis a single synchronous request), so Kthena owns the batch lifecycle and sends each input line through the router's own request path.What the proposal covers:
/v1/filesand/v1/batches, the same object shapes, line format, status flow and error envelope, with every difference listed.FileStoreinterface, first backed by a shared volume.custom_idis answered exactly once in the committed output across a takeover.The numbers in it come from a working proof of concept: 20k-request batches complete with 0 missing and 0 duplicated
custom_idacross a forced pod kill, and router memory stays at 15-40 MiB at 50k requests. Testing the proof of concept against this design found gaps, and the proposal lists each one with what the design does instead.Which issue(s) this PR fixes:
Part of #1291
Bug evidence (required for bug-related PRs):
N/A
Special notes for your reviewer:
Documentation only, no code. Once the design is agreed I'll send the implementation as small serial PRs, in the order under Implementation Plan.
Five questions are open, each with a recommendation, under Decisions needed: the dispatch hook, how batch lines count against rate limits and fairness, the Redis persistence requirement, session stickiness, and one model per input file.
Does this PR introduce a user-facing change?: