Skip to content

Commit 573710f

Browse files
committed
doc: describe multi-job admission and per-request cancel in lifecycle rules
Update the request-lifecycle rules to describe the shared completion and batch admission lane, per-request parallel caps, the disk-KV-cache lane, and per-request cancel routing.
1 parent 1ce0ab9 commit 573710f

1 file changed

Lines changed: 7 additions & 5 deletions

File tree

.cursor/rules/sdk/request-lifecycle-primitives.mdc

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,15 @@ completionStream: defineHandler({
192192
requestSchema: completionStreamRequestSchema,
193193
responseSchema: completionStreamResponseSchema,
194194
streaming: true,
195-
cancel: { scope: "model", hard: true },
195+
cancel: { scope: "request", hard: true },
196196
handler: async function* (request) { /* ... */ },
197197
}),
198198
```
199199

200200
`cancel.scope`:
201201

202202
- `"request"` — addon targets a specific in-flight request by id. Framework can route `cancel({ requestId })` straight to the addon without colliding with siblings on the same model.
203-
- `"model"` — addon cancels "whatever is currently running on this model" (today's `addon.cancel()` semantics). Sufficient under a one-at-a-time concurrency policy.
203+
- `"model"` — addon cancels "whatever is currently running on this model" (`addon.cancel()` semantics). Under continuous batching this also stops concurrent peers on the same model, so it is a coarse fallback, not a per-request cancel. `translate` and `finetune` use it; `completionStream` and `batchCompletionStream` route per-request cancel through the run response instead.
204204
- `"none"` — addon does not expose a cancel surface. SDK falls back to soft-cancel (stop yielding, drop result, skip post-processing; C++ work runs to completion).
205205

206206
`cancel.hard` (only meaningful for `"model"` / `"request"`):
@@ -214,7 +214,9 @@ Omitting the field entirely is equivalent to `{ scope: "none" }`. The runtime sc
214214

215215
| Plugin | Handler(s) | `cancel` |
216216
|------------------------------|---------------------------------------|-----------------------------------|
217-
| `llamacpp-completion` | `completionStream`, `translate` | `{ scope: "model", hard: true }` |
217+
| `llamacpp-completion` | `completionStream` | `{ scope: "request", hard: true }`|
218+
| `llamacpp-completion` | `batchCompletionStream` | `{ scope: "request", hard: true }`|
219+
| `llamacpp-completion` | `translate` | `{ scope: "model", hard: true }` |
218220
| `llamacpp-completion` | `finetune` | `{ scope: "model", hard: true }` |
219221
| `llamacpp-embedding` | `embed` | `{ scope: "model", hard: true }` |
220222
| `whispercpp-transcription` | `transcribe`, `transcribeStream` | `{ scope: "model", hard: true }` |
@@ -276,13 +278,13 @@ interface RequestRegistry {
276278
}
277279
```
278280

279-
A policy turns admission into a per-`(lane, modelId)` FIFO queue with a configurable concurrency limit. `maxConcurrentPerModel` is the slot count (`1` serializes — today's single-context reality; bump it to the addon's batching width once continuous batching lands; `Infinity`/omitted leaves the kind ungated). `onOverflow` decides what an over-capacity `begin(...)` does: `"queue"` (the default) waits FIFO for a slot; `"reject"` throws immediately. A `begin` rejects with `RequestRejectedByPolicyError` (code 52420) when `onOverflow: "reject"` hits capacity, the `(maxQueueDepthPerModel + 1)`th waiter would exceed the depth cap, or a queued waiter exceeds `queueTimeoutMs` — a typed error carrying `requestId` / `kind` / `modelId` / `reason` that survives the RPC boundary so clients can `instanceof`-narrow. `oneAtATimePerModel: true` is a back-compat alias for `{ maxConcurrentPerModel: 1, onOverflow: "reject" }`.
281+
A policy turns admission into a per-`(lane, modelId)` FIFO queue with a configurable concurrency limit. `maxConcurrentPerModel` is the slot count (`1` serializes; the completion handlers pass the loaded model's own `parallel` — its continuous-batching width — per request via `begin(...)`, so admission tracks the addon's real concurrency; `Infinity`/omitted leaves the kind ungated). `maxConcurrentPerModel` and `slotGroup` accept a per-request override on `begin(...)` that wins over the kind policy (each loaded model's real cap is its own `parallel`). `onOverflow` is a per-kind policy value: `"queue"` (the default) waits FIFO for a slot; `"reject"` throws immediately. A `begin` rejects with `RequestRejectedByPolicyError` (code 52420) when `onOverflow: "reject"` hits capacity, the `(maxQueueDepthPerModel + 1)`th waiter would exceed the depth cap, or a queued waiter exceeds `queueTimeoutMs` — a typed error carrying `requestId` / `kind` / `modelId` / `reason` that survives the RPC boundary so clients can `instanceof`-narrow. `oneAtATimePerModel: true` is a back-compat alias for `{ maxConcurrentPerModel: 1, onOverflow: "reject" }`.
280282

281283
### Shared slot groups
282284

283285
`sharedSlotGroup` keys the admission slot on `(group, modelId)` instead of `(kind, modelId)`, so **different kinds** that target the same model contend for one slot pool. This exists to mirror an addon that serializes unrelated kinds on a single native context.
284286

285-
`getRequestRegistry()` (the worker-process singleton in `request-registry-singleton.ts`) registers `completion` and `batchCompletion` on first use, both serializing (`maxConcurrentPerModel: 1, onOverflow: "queue", maxQueueDepthPerModel: 64`) and both keyed to the **same** `sharedSlotGroup: "llamacppCompletion"`. The reason: `@qvac/llm-llamacpp` funnels every `run()` (single-prompt **and** batch) through one per-instance `exclusiveRunQueue` plus a single-job native runner, so a completion and a batch on the same model can never actually execute at once. Sharing one lane makes the SDK admission queue the authoritative serialization point — a batch fired while a completion runs waits FIFO at the SDK layer instead of both being admitted and silently serializing inside the addon (which would hide the second request from the registry's queue-depth accounting, `requestId` diagnostics, and cancel routing). `audiogen` has its own one-slot FIFO policy because ACE-Step supports one active job per model and replaces the prior response when another run starts.
287+
`getRequestRegistry()` (the worker-process singleton in `request-registry-singleton.ts`) registers `completion` and `batchCompletion` on first use, both keyed to the **same** `sharedSlotGroup: "llamacppCompletion"` so single completions and batches compete for one slot pool per model first-come-first-serve. `@qvac/llm-llamacpp` now admits multiple concurrent top-level `run()` calls (each its own native job) up to the model's `parallel`, so a batch and singles can genuinely decode together. The registered `maxConcurrentPerModel: 1` is only the fallback; each handler passes the loaded model's `parallel` per request, capping admission at the model's slot count. Admission always queues the surplus FIFO (`onOverflow: "queue"`): a single-slot model (`parallel: 1` or unset) admits one at a time and the rest wait, unchanged from before; an N-way model admits up to `parallel` concurrently and queues the surplus. Disk-KV-cache completions on an N-way model divert to a private cap-1 lane (`slotGroup: "llamacppCompletionCached"`) so concurrent turns can't corrupt the shared on-disk cache — that lane serializes only cache writers, not the whole model. Both single-completion and batch cancel are per-request: `response.cancel()` routes to `cancelJob` for that call's own job or batch group, so cancelling one leaves concurrent peers on the model decoding. Both `completionStream` and `batchCompletionStream` therefore advertise `cancel: { scope: "request" }`.
286288

287289
Kinds without a registered policy are unconstrained, and a policy without `sharedSlotGroup` only contends with its own kind. `embeddings` / `transcribe` / `translate` / `finetune` deliberately register no policy — those addons tolerate concurrent requests against the same model (and the registry's per-request lifecycle is enough for cancel routing). Note this means `translate` / `finetune` on an llama.cpp model are *not* in the completion lane today; they still serialize against completion inside the addon's run queue, just not at the SDK admission layer. Fold them into `llamacppCompletion` if that addon-level serialization ever needs to be SDK-visible.
288290

0 commit comments

Comments
 (0)