@qvac/sdk 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +124 -64
- package/README.md +19 -0
- package/dist/client/api/audio-gen.d.ts +17 -0
- package/dist/client/api/audio-gen.js +17 -0
- package/dist/client/api/finetune.d.ts +2 -0
- package/dist/client/api/finetune.js +7 -1
- package/dist/client/api/index.d.ts +1 -1
- package/dist/client/api/index.js +1 -1
- package/dist/client/api/load-model.d.ts +8 -0
- package/dist/client/api/transcribe.d.ts +19 -2
- package/dist/client/api/transcribe.js +66 -27
- package/dist/client/api/translate.d.ts +1 -0
- package/dist/client/api/translate.js +40 -19
- package/dist/client/api/vla.d.ts +42 -1
- package/dist/client/api/vla.js +50 -1
- package/dist/commands/bundle/entry-gen.d.ts +4 -0
- package/dist/commands/bundle/entry-gen.js +6 -0
- package/dist/commands/bundle/index.js +19 -12
- package/dist/examples/asr/parakeet-indic-conformer-filesystem.d.ts +2 -0
- package/dist/examples/asr/parakeet-indic-conformer-filesystem.js +52 -0
- package/dist/examples/audiogen/generate-cover.d.ts +2 -0
- package/dist/examples/audiogen/generate-cover.js +100 -0
- package/dist/examples/llamacpp-fallback.d.ts +2 -0
- package/dist/examples/llamacpp-fallback.js +34 -0
- package/dist/examples/multi-job-completion.d.ts +28 -0
- package/dist/examples/multi-job-completion.js +128 -0
- package/dist/examples/profiling/basic.js +32 -1
- package/dist/examples/system-resources.d.ts +2 -0
- package/dist/examples/system-resources.js +86 -0
- package/dist/examples/tts/audio8.d.ts +2 -0
- package/dist/examples/tts/audio8.js +54 -0
- package/dist/examples/tts/cosyvoice3.d.ts +2 -0
- package/dist/examples/tts/cosyvoice3.js +52 -0
- package/dist/examples/vla-groot.js +92 -49
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/models/registry/models.d.ts +782 -6
- package/dist/models/registry/models.js +2664 -1859
- package/dist/schemas/audio-gen.d.ts +103 -0
- package/dist/schemas/audio-gen.js +135 -4
- package/dist/schemas/common.d.ts +550 -11
- package/dist/schemas/constants-registry.d.ts +18 -4
- package/dist/schemas/constants-registry.js +16 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/llamacpp-config.d.ts +10 -10
- package/dist/schemas/llamacpp-config.js +12 -5
- package/dist/schemas/load-model.d.ts +3678 -323
- package/dist/schemas/load-model.js +21 -3
- package/dist/schemas/sdk-config.d.ts +60 -24
- package/dist/schemas/sdk-errors-server.d.ts +1 -0
- package/dist/schemas/sdk-errors-server.js +5 -0
- package/dist/schemas/text-to-speech.d.ts +1086 -110
- package/dist/schemas/text-to-speech.js +194 -4
- package/dist/schemas/tools.d.ts +0 -16
- package/dist/schemas/tools.js +0 -15
- package/dist/schemas/transcription-config.d.ts +2 -0
- package/dist/schemas/transcription-config.js +10 -4
- package/dist/schemas/transcription.d.ts +3 -0
- package/dist/schemas/vla.d.ts +83 -0
- package/dist/schemas/vla.js +105 -2
- package/dist/scripts/link-workspace-inference.d.ts +2 -0
- package/dist/scripts/link-workspace-inference.js +31 -0
- package/dist/server/bare/ops/cancel.d.ts +1 -1
- package/dist/server/bare/ops/cancel.js +1 -1
- package/dist/server/bare/ops/kv-cache-retention.js +11 -0
- package/dist/server/bare/ops/kv-cache-utils.d.ts +1 -1
- package/dist/server/bare/ops/kv-cache-utils.js +40 -12
- package/dist/server/bare/ops/transcribe.d.ts +2 -2
- package/dist/server/bare/ops/transcribe.js +54 -48
- package/dist/server/bare/ops/translate-stats.d.ts +5 -0
- package/dist/server/bare/ops/translate-stats.js +11 -0
- package/dist/server/bare/ops/translate.js +66 -38
- package/dist/server/bare/ops/unload-model.js +30 -24
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-input.d.ts +20 -0
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-input.js +153 -0
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-stream.js +52 -1
- package/dist/server/bare/plugins/audiogen-ggml/plugin.d.ts +28 -0
- package/dist/server/bare/plugins/ggml-ocr/plugin.js +3 -1
- package/dist/server/bare/plugins/ggml-vla/ops/vla-set-embodiment.d.ts +3 -0
- package/dist/server/bare/plugins/ggml-vla/ops/vla-set-embodiment.js +9 -0
- package/dist/server/bare/plugins/ggml-vla/plugin.d.ts +37 -0
- package/dist/server/bare/plugins/ggml-vla/plugin.js +21 -4
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-completion-stream.js +16 -15
- package/dist/server/bare/plugins/llamacpp-completion/ops/cache-logger.d.ts +1 -1
- package/dist/server/bare/plugins/llamacpp-completion/ops/cache-logger.js +2 -2
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stats.d.ts +6 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stats.js +10 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.d.ts +1 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.js +131 -123
- package/dist/server/bare/plugins/llamacpp-completion/ops/finetune.js +110 -20
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.d.ts +49 -23
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.js +308 -143
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.d.ts +1 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.js +2 -2
- package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +6 -6
- package/dist/server/bare/plugins/llamacpp-completion/plugin.js +55 -36
- package/dist/server/bare/plugins/llamacpp-completion/transform.js +1 -7
- package/dist/server/bare/plugins/llamacpp-embedding/plugin.js +2 -3
- package/dist/server/bare/plugins/nmtcpp-translation/plugin.js +6 -5
- package/dist/server/bare/plugins/parakeet-transcription/plugin.js +27 -16
- package/dist/server/bare/plugins/tts-ggml/ops/parler-options.js +23 -3
- package/dist/server/bare/plugins/tts-ggml/plugin.d.ts +302 -4
- package/dist/server/bare/plugins/tts-ggml/plugin.js +131 -0
- package/dist/server/bare/plugins/whispercpp-transcription/plugin.js +48 -39
- package/dist/server/bare/registry/model-registry.d.ts +1 -0
- package/dist/server/bare/runtime/request-registry-singleton.js +29 -15
- package/dist/server/bare/runtime/request-registry.d.ts +27 -0
- package/dist/server/bare/runtime/request-registry.js +246 -76
- package/dist/server/bare/types/addon-responses.d.ts +4 -12
- package/dist/server/rpc/handlers/load-model/handler.js +2 -2
- package/dist/server/rpc/handlers/load-model/resolve-session.d.ts +1 -1
- package/dist/server/rpc/handlers/load-model/resolve-session.js +3 -3
- package/dist/server/rpc/handlers/load-model/resolve.d.ts +12 -2
- package/dist/server/rpc/handlers/load-model/resolve.js +90 -12
- package/dist/server/utils/audio/decoder.d.ts +37 -1
- package/dist/server/utils/audio/decoder.js +81 -51
- package/dist/server/utils/config-transform.d.ts +3 -0
- package/dist/server/utils/config-transform.js +6 -0
- package/dist/server/utils/index.d.ts +0 -1
- package/dist/server/utils/index.js +0 -1
- package/dist/server/utils/shard-utils.d.ts +1 -0
- package/dist/server/utils/shard-utils.js +14 -0
- package/dist/server/utils/tool-integration.d.ts +3 -10
- package/dist/server/utils/tool-integration.js +3 -12
- package/dist/utils/errors-server.d.ts +3 -0
- package/dist/utils/errors-server.js +5 -0
- package/package.json +12 -14
- package/dist/examples/llamacpp-dynamic-tools.d.ts +0 -2
- package/dist/examples/llamacpp-dynamic-tools.js +0 -152
- package/dist/models/update-models/codegen.d.ts +0 -3
- package/dist/models/update-models/codegen.js +0 -196
- package/dist/models/update-models/companions.d.ts +0 -30
- package/dist/models/update-models/companions.js +0 -296
- package/dist/models/update-models/history.d.ts +0 -24
- package/dist/models/update-models/history.js +0 -107
- package/dist/models/update-models/index.d.ts +0 -2
- package/dist/models/update-models/index.js +0 -123
- package/dist/models/update-models/naming.d.ts +0 -4
- package/dist/models/update-models/naming.js +0 -396
- package/dist/models/update-models/processing.d.ts +0 -8
- package/dist/models/update-models/processing.js +0 -65
- package/dist/models/update-models/registry.d.ts +0 -5
- package/dist/models/update-models/registry.js +0 -87
- package/dist/models/update-models/shards.d.ts +0 -4
- package/dist/models/update-models/shards.js +0 -57
- package/dist/models/update-models/types.d.ts +0 -88
- package/dist/models/update-models/types.js +0 -1
- package/dist/models/update-models/utils.d.ts +0 -3
- package/dist/models/update-models/utils.js +0 -21
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-cancelled.d.ts +0 -23
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-cancelled.js +0 -30
- package/dist/server/utils/expand-gguf-shards.d.ts +0 -3
- package/dist/server/utils/expand-gguf-shards.js +0 -22
|
@@ -11,23 +11,19 @@ import { createRequestRegistry as createRegistry } from '../../../server/bare/ru
|
|
|
11
11
|
* baseline concurrency policies.
|
|
12
12
|
*/
|
|
13
13
|
let registry = null;
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
14
|
+
// One admission lane per model for the llama.cpp addon: completion,
|
|
15
|
+
// batchCompletion, LLM translate, and finetune all key on
|
|
16
|
+
// (LLAMACPP_COMPLETION_SLOT_GROUP, modelId), so they compete for the model's
|
|
17
|
+
// `parallel` slots first-come-first-serve and share ONE bounded FIFO wait queue.
|
|
18
|
+
// The queue depth is the per-model cap below (64): once 64 requests across those
|
|
19
|
+
// kinds are waiting, the 65th begin is rejected with RequestRejectedByPolicyError.
|
|
20
|
+
// Disk-KV-cache turns ride the same lane and serialise same-file writes per cache
|
|
21
|
+
// path in the KV-cache session, so they need no separate group. NMT translate
|
|
22
|
+
// passes an infinite cap and never enters the lane, so it stays ungated.
|
|
22
23
|
const LLAMACPP_COMPLETION_SLOT_GROUP = 'llamacppCompletion';
|
|
23
24
|
function installDefaultPolicies(r) {
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// rather than reject: the second waits FIFO. maxConcurrentPerModel: 1 is
|
|
27
|
-
// today's reality — raise it once continuous batching lands. The depth cap
|
|
28
|
-
// bounds queue memory. The shared slot group extends that serialization
|
|
29
|
-
// across `completion` + `batchCompletion` on the same model (see note
|
|
30
|
-
// above).
|
|
25
|
+
// Cap is the model's `parallel`, passed per request by the handlers; the value
|
|
26
|
+
// here is only the fallback when a caller supplies none.
|
|
31
27
|
r.policy({
|
|
32
28
|
kind: 'completion',
|
|
33
29
|
maxConcurrentPerModel: 1,
|
|
@@ -42,6 +38,24 @@ function installDefaultPolicies(r) {
|
|
|
42
38
|
maxQueueDepthPerModel: 64,
|
|
43
39
|
sharedSlotGroup: LLAMACPP_COMPLETION_SLOT_GROUP
|
|
44
40
|
});
|
|
41
|
+
// Finetune's only stop is the addon's global cancel, which would kill
|
|
42
|
+
// concurrent completions. Run it exclusively so nothing else is on the model.
|
|
43
|
+
r.policy({
|
|
44
|
+
kind: 'finetune',
|
|
45
|
+
maxConcurrentPerModel: 1,
|
|
46
|
+
onOverflow: 'queue',
|
|
47
|
+
maxQueueDepthPerModel: 64,
|
|
48
|
+
sharedSlotGroup: LLAMACPP_COMPLETION_SLOT_GROUP,
|
|
49
|
+
exclusive: true
|
|
50
|
+
});
|
|
51
|
+
// LLM translate shares the completion lane as a reader (capped by `parallel`),
|
|
52
|
+
// so a finetune blocks it too. NMT passes no cap and stays ungated.
|
|
53
|
+
r.policy({
|
|
54
|
+
kind: 'translate',
|
|
55
|
+
onOverflow: 'queue',
|
|
56
|
+
maxQueueDepthPerModel: 64,
|
|
57
|
+
sharedSlotGroup: LLAMACPP_COMPLETION_SLOT_GROUP
|
|
58
|
+
});
|
|
45
59
|
// ACE-Step owns one active job per model. Starting another run replaces
|
|
46
60
|
// the addon's active response, and model-scoped cancel targets that single
|
|
47
61
|
// active job. Keep the SDK registry authoritative by admitting one AudioGen
|
|
@@ -19,6 +19,18 @@ export interface BeginOpts {
|
|
|
19
19
|
* the parent does not require iterating the registry.
|
|
20
20
|
*/
|
|
21
21
|
parentSignal?: AbortSignal;
|
|
22
|
+
/**
|
|
23
|
+
* Per-request override of the kind policy's `maxConcurrentPerModel`. The
|
|
24
|
+
* policy value is one number for every model of a kind, but handlers may
|
|
25
|
+
* use a loaded model's `parallel` as its top-level request-permit cap.
|
|
26
|
+
* This counts requests, not native sequences: a multi-prompt batch still
|
|
27
|
+
* consumes one permit and the addon schedules its sequences internally.
|
|
28
|
+
* Must be uniform across all requests routed to the same `(lane, modelId)`
|
|
29
|
+
* (a given model always yields the same `parallel`): the FIFO slot
|
|
30
|
+
* hand-off assumes one cap per lane and does not re-check it. A finite
|
|
31
|
+
* value below 1 is floored to 1.
|
|
32
|
+
*/
|
|
33
|
+
maxConcurrentPerModel?: number;
|
|
22
34
|
}
|
|
23
35
|
export interface CancelByRequestId {
|
|
24
36
|
requestId: string;
|
|
@@ -97,6 +109,13 @@ export interface ConcurrencyPolicy {
|
|
|
97
109
|
* internal queue.
|
|
98
110
|
*/
|
|
99
111
|
sharedSlotGroup?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Admit at most one such request per lane AND only while no other request
|
|
114
|
+
* holds the lane — a writer in a reader/writer lock. Used for finetune,
|
|
115
|
+
* whose only cancel is the addon's global cancel: making it exclusive keeps
|
|
116
|
+
* completions out of the addon while it runs, so that cancel can't hit them.
|
|
117
|
+
*/
|
|
118
|
+
exclusive?: boolean;
|
|
100
119
|
}
|
|
101
120
|
/**
|
|
102
121
|
* `ManagedRequestContext` is the value `begin(...)` resolves to. It extends
|
|
@@ -169,6 +188,14 @@ export interface RequestRegistry {
|
|
|
169
188
|
* scope unwinding still happens on each handler's own dispose path.
|
|
170
189
|
*/
|
|
171
190
|
cancelAll(reason: 'shutdown' | 'modelUnload'): Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Cancel every request for a model and wait for each to fully dispose (native
|
|
193
|
+
* context freed, slot released) before running `teardown` with an admission
|
|
194
|
+
* barrier held — any begin for the model starts aborted — then lift it. Used
|
|
195
|
+
* by model unload so the model object isn't freed while a request is still
|
|
196
|
+
* mid-teardown, and no new request goes active against it during teardown.
|
|
197
|
+
*/
|
|
198
|
+
withModelDraining(modelId: string, teardown: () => Promise<void>): Promise<void>;
|
|
172
199
|
/**
|
|
173
200
|
* Mark a request finished and dispose its scope. Equivalent to
|
|
174
201
|
* `await ctx[Symbol.asyncDispose]()` with an explicit outcome.
|
|
@@ -48,6 +48,26 @@ export function createRequestRegistry(options) {
|
|
|
48
48
|
* FIFO in O(1) without scanning every `KeyState`.
|
|
49
49
|
*/
|
|
50
50
|
const waitersById = new Map();
|
|
51
|
+
/**
|
|
52
|
+
* Ids reserved synchronously at `begin(...)` start and cleared once the entry
|
|
53
|
+
* lands in `entries` (or the begin throws). A same-tick second `begin(...)`
|
|
54
|
+
* with the same id runs before the first reaches `entries`/`waitersById`, so
|
|
55
|
+
* without this it would slip past the duplicate check and both would execute.
|
|
56
|
+
*/
|
|
57
|
+
const reservedIds = new Set();
|
|
58
|
+
// Models being torn down: a begin(...) for one starts aborted. The barrier
|
|
59
|
+
// half of cancelAndDrain — catches begins its scan can't see yet.
|
|
60
|
+
const drainingModels = new Set();
|
|
61
|
+
// Entries whose disposal has started but not finished. disposeEntry removes
|
|
62
|
+
// from `entries` before unwinding, so a drain must consult this too or it
|
|
63
|
+
// would return while a request is still tearing down natively.
|
|
64
|
+
const disposingEntries = new Set();
|
|
65
|
+
// Request ids whose entry is mid-disposal. disposeEntry drops the id from
|
|
66
|
+
// `entries` before its async scope unwinds; without this the id looks free
|
|
67
|
+
// during that gap, so a same-id begin could activate concurrently with the
|
|
68
|
+
// still-tearing-down request and a cancel would land as a cancel-before-begin
|
|
69
|
+
// tripwire that aborts a later reuse. Held until teardown completes.
|
|
70
|
+
const disposingIds = new Set();
|
|
51
71
|
function logLifecycle(event, ctx, durationMs) {
|
|
52
72
|
const modelId = ctx.modelId !== undefined ? ctx.modelId : '-';
|
|
53
73
|
const base = `[request-lifecycle] ${event} requestId=${ctx.requestId} kind=${ctx.kind} modelId=${modelId} state=${ctx.state}`;
|
|
@@ -132,7 +152,7 @@ export function createRequestRegistry(options) {
|
|
|
132
152
|
* removal resolves (granted / graceful cancel) or rejects (timeout /
|
|
133
153
|
* teardown).
|
|
134
154
|
*/
|
|
135
|
-
function removeWaiter(key, waiter) {
|
|
155
|
+
function removeWaiter(key, waiter, drainAfter = true) {
|
|
136
156
|
if (waiter.timer !== undefined) {
|
|
137
157
|
clearTimeout(waiter.timer);
|
|
138
158
|
waiter.timer = undefined;
|
|
@@ -144,8 +164,14 @@ export function createRequestRegistry(options) {
|
|
|
144
164
|
const i = st.waiters.indexOf(waiter);
|
|
145
165
|
if (i >= 0)
|
|
146
166
|
st.waiters.splice(i, 1);
|
|
147
|
-
|
|
167
|
+
// Removing a queued writer can unblock the readers behind it, so drain
|
|
168
|
+
// (which also deletes the KeyState once idle). `cancelAll` passes false:
|
|
169
|
+
// it's tearing every waiter down, so granting one mid-sweep is wrong.
|
|
170
|
+
if (drainAfter)
|
|
171
|
+
drain(st, key);
|
|
172
|
+
else if (st.active <= 0 && !st.exclusiveActive && st.waiters.length === 0) {
|
|
148
173
|
keyStates.delete(key);
|
|
174
|
+
}
|
|
149
175
|
}
|
|
150
176
|
/**
|
|
151
177
|
* Admission gate. Resolves to the slot key the caller must release on
|
|
@@ -163,29 +189,50 @@ export function createRequestRegistry(options) {
|
|
|
163
189
|
*/
|
|
164
190
|
async function acquireSlot(opts) {
|
|
165
191
|
if (opts.modelId === undefined)
|
|
166
|
-
return { slotKey: undefined };
|
|
192
|
+
return { slotKey: undefined, exclusive: false };
|
|
167
193
|
const policy = policies.get(opts.kind);
|
|
168
|
-
if (!policy
|
|
169
|
-
return { slotKey: undefined };
|
|
194
|
+
if (!policy)
|
|
195
|
+
return { slotKey: undefined, exclusive: false };
|
|
196
|
+
// The per-request override (the model's own `parallel`) wins over the
|
|
197
|
+
// per-kind default. A non-finite effective cap disables gating entirely.
|
|
198
|
+
const requestedMax = opts.maxConcurrentPerModel ?? policy.maxConcurrent;
|
|
199
|
+
if (!Number.isFinite(requestedMax)) {
|
|
200
|
+
return { slotKey: undefined, exclusive: false };
|
|
170
201
|
}
|
|
202
|
+
const maxConcurrent = clampSlotCount(requestedMax);
|
|
171
203
|
// A parent (worker-shutdown) signal that's already aborted: don't
|
|
172
204
|
// queue behind live work that may never drain — let begin() proceed
|
|
173
205
|
// and abort immediately via the parentSignal path.
|
|
174
206
|
if (opts.parentSignal?.aborted)
|
|
175
|
-
return { slotKey: undefined };
|
|
207
|
+
return { slotKey: undefined, exclusive: false };
|
|
176
208
|
const modelId = opts.modelId;
|
|
177
|
-
//
|
|
178
|
-
// contend for one slot pool per model
|
|
179
|
-
// them on a single native context.
|
|
209
|
+
// Lane precedence: the policy's shared group → the kind itself. A shared
|
|
210
|
+
// lane lets unrelated kinds contend for one slot pool per model.
|
|
180
211
|
const key = slotKey(policy.slotGroup ?? opts.kind, modelId);
|
|
181
212
|
let st = keyStates.get(key);
|
|
182
213
|
if (!st) {
|
|
183
|
-
st = { active: 0, waiters: [] };
|
|
214
|
+
st = { active: 0, waiters: [], exclusiveActive: false };
|
|
184
215
|
keyStates.set(key, st);
|
|
185
216
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
217
|
+
const exclusive = policy.exclusive;
|
|
218
|
+
// A reader admits below the lane's pinned cap (see `readerCap`); an exclusive
|
|
219
|
+
// writer needs the lane empty. A queued waiter (of any kind) blocks admission
|
|
220
|
+
// so a writer isn't starved by a steady reader stream. For reader-only lanes
|
|
221
|
+
// `waiters` is empty whenever `active < cap`, so this guard is a no-op there.
|
|
222
|
+
const readerCap = st.readerCap ?? maxConcurrent;
|
|
223
|
+
const canAdmit = st.waiters.length === 0 &&
|
|
224
|
+
!st.exclusiveActive &&
|
|
225
|
+
(exclusive ? st.active === 0 : st.active < readerCap);
|
|
226
|
+
if (canAdmit) {
|
|
227
|
+
if (exclusive) {
|
|
228
|
+
st.exclusiveActive = true;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
st.active++;
|
|
232
|
+
if (st.readerCap === undefined)
|
|
233
|
+
st.readerCap = maxConcurrent;
|
|
234
|
+
}
|
|
235
|
+
return { slotKey: key, exclusive };
|
|
189
236
|
}
|
|
190
237
|
if (policy.onOverflow === 'reject') {
|
|
191
238
|
throw new RequestRejectedByPolicyError(opts.requestId, opts.kind, modelId, `another ${opts.kind} request is already running on this model`);
|
|
@@ -202,7 +249,9 @@ export function createRequestRegistry(options) {
|
|
|
202
249
|
enqueuedAt: Date.now(),
|
|
203
250
|
resolve,
|
|
204
251
|
reject,
|
|
205
|
-
timer: undefined
|
|
252
|
+
timer: undefined,
|
|
253
|
+
exclusive,
|
|
254
|
+
cap: maxConcurrent
|
|
206
255
|
};
|
|
207
256
|
if (policy.queueTimeoutMs !== undefined) {
|
|
208
257
|
const timeoutMs = policy.queueTimeoutMs;
|
|
@@ -214,32 +263,59 @@ export function createRequestRegistry(options) {
|
|
|
214
263
|
queue.waiters.push(waiter);
|
|
215
264
|
waitersById.set(opts.requestId, { key, waiter });
|
|
216
265
|
});
|
|
217
|
-
return granted ? { slotKey: key } : { slotKey: undefined };
|
|
266
|
+
return granted ? { slotKey: key, exclusive } : { slotKey: undefined, exclusive: false };
|
|
218
267
|
}
|
|
219
268
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
269
|
+
* Grant the lane to as many FIFO-head waiters as now fit: an exclusive writer
|
|
270
|
+
* only when the lane is idle, readers up to each one's own cap. Stops at the
|
|
271
|
+
* first head that can't be admitted, so FIFO order holds. Deletes the
|
|
272
|
+
* `KeyState` once fully idle.
|
|
224
273
|
*/
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
clearTimeout(next.timer);
|
|
235
|
-
next.timer = undefined;
|
|
274
|
+
function drain(st, key) {
|
|
275
|
+
while (st.waiters.length > 0) {
|
|
276
|
+
const head = st.waiters[0];
|
|
277
|
+
if (head === undefined)
|
|
278
|
+
break;
|
|
279
|
+
if (head.exclusive) {
|
|
280
|
+
if (st.exclusiveActive || st.active > 0)
|
|
281
|
+
break;
|
|
282
|
+
st.exclusiveActive = true;
|
|
236
283
|
}
|
|
237
|
-
|
|
238
|
-
|
|
284
|
+
else {
|
|
285
|
+
const readerCap = st.readerCap ?? head.cap;
|
|
286
|
+
if (st.exclusiveActive || st.active >= readerCap)
|
|
287
|
+
break;
|
|
288
|
+
st.active++;
|
|
289
|
+
if (st.readerCap === undefined)
|
|
290
|
+
st.readerCap = head.cap;
|
|
291
|
+
}
|
|
292
|
+
st.waiters.shift();
|
|
293
|
+
waitersById.delete(head.requestId);
|
|
294
|
+
if (head.timer !== undefined) {
|
|
295
|
+
clearTimeout(head.timer);
|
|
296
|
+
head.timer = undefined;
|
|
297
|
+
}
|
|
298
|
+
head.resolve(true);
|
|
239
299
|
}
|
|
240
|
-
st.active
|
|
241
|
-
if (st.active <= 0 && st.waiters.length === 0)
|
|
300
|
+
if (st.active <= 0 && !st.exclusiveActive && st.waiters.length === 0) {
|
|
242
301
|
keyStates.delete(key);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Release a held slot, then hand the freed capacity to waiters. Called from
|
|
306
|
+
* `disposeEntry` after the scope has fully unwound, so its deferred teardown
|
|
307
|
+
* (for llama.cpp completion, disposing the run response that stops native
|
|
308
|
+
* decode) has run before the next same-model request is admitted.
|
|
309
|
+
*/
|
|
310
|
+
function releaseSlot(key, wasExclusive) {
|
|
311
|
+
const st = keyStates.get(key);
|
|
312
|
+
if (!st)
|
|
313
|
+
return;
|
|
314
|
+
if (wasExclusive)
|
|
315
|
+
st.exclusiveActive = false;
|
|
316
|
+
else
|
|
317
|
+
st.active--;
|
|
318
|
+
drain(st, key);
|
|
243
319
|
}
|
|
244
320
|
function cancelEntry(entry, reason) {
|
|
245
321
|
if (entry.controller.signal.aborted)
|
|
@@ -250,8 +326,13 @@ export function createRequestRegistry(options) {
|
|
|
250
326
|
return true;
|
|
251
327
|
}
|
|
252
328
|
async function disposeEntry(entry, outcome) {
|
|
253
|
-
if (entry.scope.disposed)
|
|
329
|
+
if (entry.scope.disposed || entry.disposing)
|
|
254
330
|
return;
|
|
331
|
+
entry.disposing = true;
|
|
332
|
+
disposingEntries.add(entry);
|
|
333
|
+
// Keep the id reserved across async teardown so a same-id begin can't
|
|
334
|
+
// activate concurrently and a cancel can't strand a tripwire for a reuse.
|
|
335
|
+
disposingIds.add(entry.ctx.requestId);
|
|
255
336
|
entry.ctx.state = outcome;
|
|
256
337
|
entry.detachParent();
|
|
257
338
|
logLifecycle('end', entry.ctx, Date.now() - entry.startedAt);
|
|
@@ -269,46 +350,81 @@ export function createRequestRegistry(options) {
|
|
|
269
350
|
// guarantees the slot is freed even if a deferred cleanup throws,
|
|
270
351
|
// otherwise a throwing teardown would strand the queue forever.
|
|
271
352
|
if (entry.slotKey !== undefined)
|
|
272
|
-
releaseSlot(entry.slotKey);
|
|
353
|
+
releaseSlot(entry.slotKey, entry.exclusive);
|
|
354
|
+
disposingEntries.delete(entry);
|
|
355
|
+
// The id is free for reuse only now that native teardown is complete.
|
|
356
|
+
disposingIds.delete(entry.ctx.requestId);
|
|
357
|
+
// Signal any `cancelAndDrain` waiting on this request that its native
|
|
358
|
+
// teardown is complete.
|
|
359
|
+
entry.resolveDisposed();
|
|
273
360
|
}
|
|
274
361
|
}
|
|
275
362
|
async function begin(opts) {
|
|
276
|
-
// A request id is reserved for its whole lifecycle
|
|
277
|
-
//
|
|
278
|
-
// still-queued begins (
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
363
|
+
// A request id is reserved for its whole lifecycle: the time it spends
|
|
364
|
+
// *queued* for an admission slot, while it is active, and while it is
|
|
365
|
+
// tearing down. `waitersById` holds the still-queued begins (not yet in
|
|
366
|
+
// `entries`) and `disposingIds` holds the ones whose async teardown hasn't
|
|
367
|
+
// finished, so a duplicate id must be rejected against all of them —
|
|
368
|
+
// otherwise a second begin with the same id would enqueue behind the first
|
|
369
|
+
// and overwrite its `waitersById` index (unreachable by `cancel(...)`), or
|
|
370
|
+
// activate while the prior request is still releasing its native context.
|
|
371
|
+
if (entries.has(opts.requestId) ||
|
|
372
|
+
waitersById.has(opts.requestId) ||
|
|
373
|
+
reservedIds.has(opts.requestId) ||
|
|
374
|
+
disposingIds.has(opts.requestId)) {
|
|
283
375
|
throw new RequestIdConflictError(opts.requestId);
|
|
284
376
|
}
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
377
|
+
// Reserve synchronously so a same-tick duplicate begin is rejected before
|
|
378
|
+
// this one reaches `entries`/`waitersById`. Cleared on every exit below.
|
|
379
|
+
reservedIds.add(opts.requestId);
|
|
380
|
+
// A request cancelled BEFORE begin must not queue for a slot on a saturated
|
|
381
|
+
// lane: consume its marker first and skip admission, so it resolves at once
|
|
382
|
+
// into an aborted context holding no slot instead of waiting for the holder.
|
|
383
|
+
let preCancel = consumeCancelBeforeBegin(opts.requestId);
|
|
384
|
+
// Admission control runs before allocation so a rejected begin leaves no
|
|
385
|
+
// controller / scope behind. This may await: a gated `(kind, modelId)` at
|
|
386
|
+
// capacity queues the begin FIFO and resolves once a slot frees (or rejects
|
|
387
|
+
// on overflow / queue-depth cap / timeout).
|
|
388
|
+
let slotKey;
|
|
389
|
+
let exclusive = false;
|
|
390
|
+
if (!preCancel) {
|
|
391
|
+
// Plain try/catch, not `.catch()`, so admission adds no extra microtask
|
|
392
|
+
// and observers still see the request registered on the same tick.
|
|
393
|
+
let acquired;
|
|
394
|
+
try {
|
|
395
|
+
acquired = await acquireSlot(opts);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
reservedIds.delete(opts.requestId);
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
slotKey = acquired.slotKey;
|
|
402
|
+
exclusive = acquired.exclusive;
|
|
403
|
+
// The only interleaving point in this otherwise synchronous body is the
|
|
404
|
+
// await above. A duplicate id could (astronomically unlikely) have landed
|
|
405
|
+
// while we were queued — re-check and hand the just-acquired slot back
|
|
406
|
+
// rather than stranding it on the conflicting throw.
|
|
407
|
+
if (entries.has(opts.requestId)) {
|
|
408
|
+
if (slotKey !== undefined)
|
|
409
|
+
releaseSlot(slotKey, exclusive);
|
|
410
|
+
reservedIds.delete(opts.requestId);
|
|
411
|
+
throw new RequestIdConflictError(opts.requestId);
|
|
412
|
+
}
|
|
413
|
+
// A cancel that landed WHILE we were queued resolved acquireSlot with no
|
|
414
|
+
// slot and recorded a marker; consume it so the context starts aborted.
|
|
415
|
+
preCancel = consumeCancelBeforeBegin(opts.requestId);
|
|
298
416
|
}
|
|
417
|
+
// A begin arriving while its model is being torn down starts aborted — no
|
|
418
|
+
// native work against a model about to be freed.
|
|
419
|
+
const drainReason = opts.modelId !== undefined && drainingModels.has(opts.modelId) ? 'modelUnload' : undefined;
|
|
299
420
|
const controller = new AbortController();
|
|
300
421
|
const scope = createDisposableScope();
|
|
301
|
-
// Stop-button race close. If a
|
|
302
|
-
// `cancel({ requestId })` already arrived for this id, abort the
|
|
303
|
-
// new controller before observers can subscribe to it. The
|
|
304
|
-
// tripwire entry is consumed so a later, separate `begin(...)`
|
|
305
|
-
// with the same id is unaffected (in practice ids are UUIDv4 and
|
|
306
|
-
// never reused; this guard just keeps the contract self-
|
|
307
|
-
// consistent under retries).
|
|
308
|
-
const preCancel = consumeCancelBeforeBegin(opts.requestId);
|
|
309
422
|
if (preCancel) {
|
|
310
423
|
controller.abort(preCancel.reason);
|
|
311
424
|
}
|
|
425
|
+
else if (drainReason) {
|
|
426
|
+
controller.abort(drainReason);
|
|
427
|
+
}
|
|
312
428
|
let detachParent = () => { };
|
|
313
429
|
if (opts.parentSignal) {
|
|
314
430
|
const parent = opts.parentSignal;
|
|
@@ -333,17 +449,26 @@ export function createRequestRegistry(options) {
|
|
|
333
449
|
// already aborted at begin time. Both branches abort the
|
|
334
450
|
// controller above, so without this guard observers would see a
|
|
335
451
|
// momentarily-`running` context with an already-aborted signal.
|
|
336
|
-
state: preCancel || opts.parentSignal?.aborted ? 'cancelling' : 'running'
|
|
452
|
+
state: preCancel || drainReason || opts.parentSignal?.aborted ? 'cancelling' : 'running'
|
|
337
453
|
};
|
|
454
|
+
let resolveDisposed = () => { };
|
|
455
|
+
const disposed = new Promise((resolve) => {
|
|
456
|
+
resolveDisposed = resolve;
|
|
457
|
+
});
|
|
338
458
|
const entry = {
|
|
339
459
|
ctx,
|
|
340
460
|
controller,
|
|
341
461
|
scope,
|
|
342
462
|
detachParent,
|
|
343
463
|
startedAt: Date.now(),
|
|
344
|
-
slotKey
|
|
464
|
+
slotKey,
|
|
465
|
+
exclusive,
|
|
466
|
+
disposing: false,
|
|
467
|
+
disposed,
|
|
468
|
+
resolveDisposed
|
|
345
469
|
};
|
|
346
470
|
entries.set(opts.requestId, entry);
|
|
471
|
+
reservedIds.delete(opts.requestId);
|
|
347
472
|
logLifecycle('begin', ctx);
|
|
348
473
|
return {
|
|
349
474
|
get requestId() {
|
|
@@ -410,6 +535,12 @@ export function createRequestRegistry(options) {
|
|
|
410
535
|
cancelQueuedWaiterGracefully(queued.key, queued.waiter, target.reason);
|
|
411
536
|
return cancelled + 1;
|
|
412
537
|
}
|
|
538
|
+
// Mid-disposal: the request already reached a terminal outcome and is
|
|
539
|
+
// releasing its native context. There is nothing to cancel, and a
|
|
540
|
+
// cancel-before-begin marker would wrongly abort a later begin that
|
|
541
|
+
// reuses this id once teardown frees it.
|
|
542
|
+
if (disposingIds.has(target.requestId))
|
|
543
|
+
return cancelled;
|
|
413
544
|
// Stop-button race: the client beat its own
|
|
414
545
|
// `begin(...)`. Record the cancel so the next matching `begin`
|
|
415
546
|
// aborts immediately. The return value stays 0 — no in-flight
|
|
@@ -452,15 +583,48 @@ export function createRequestRegistry(options) {
|
|
|
452
583
|
// so there is nothing left for them to run. Snapshot first —
|
|
453
584
|
// `removeWaiter` mutates `waitersById`.
|
|
454
585
|
for (const { key, waiter } of Array.from(waitersById.values())) {
|
|
455
|
-
removeWaiter(key, waiter);
|
|
586
|
+
removeWaiter(key, waiter, false);
|
|
456
587
|
waiter.reject(new RequestRejectedByPolicyError(waiter.requestId, waiter.kind, waiter.modelId, `queued request cancelled (${reason})`));
|
|
457
588
|
}
|
|
458
|
-
//
|
|
459
|
-
// async sweep that awaits per-handler scope unwinding (e.g. join on
|
|
460
|
-
// the disposers). Today every handler unwinds on its own dispose
|
|
461
|
-
// path, so the function only needs to fire-and-forget the abort.
|
|
589
|
+
// Fire-and-forget the abort; each handler unwinds on its own dispose path.
|
|
462
590
|
return Promise.resolve();
|
|
463
591
|
}
|
|
592
|
+
// Cancel and drain every request for a model, then run `teardown` with the
|
|
593
|
+
// admission barrier held — any begin for the model (including one still
|
|
594
|
+
// suspended in admission) starts aborted — lifting it afterward. Scoped so the
|
|
595
|
+
// barrier can't be left raised.
|
|
596
|
+
async function withModelDraining(modelId, teardown) {
|
|
597
|
+
drainingModels.add(modelId);
|
|
598
|
+
try {
|
|
599
|
+
// Abort each active request and collect its disposal promise, so we wait
|
|
600
|
+
// for every handler's scope to fully unwind (native context freed, slot
|
|
601
|
+
// released) before teardown. Snapshot first — `disposeEntry` mutates
|
|
602
|
+
// `entries`.
|
|
603
|
+
const draining = [];
|
|
604
|
+
for (const entry of entries.values()) {
|
|
605
|
+
if (entry.ctx.modelId !== modelId)
|
|
606
|
+
continue;
|
|
607
|
+
cancelEntry(entry, 'modelUnload');
|
|
608
|
+
draining.push(entry.disposed);
|
|
609
|
+
}
|
|
610
|
+
// Requests already mid-disposal are out of `entries` but may still be
|
|
611
|
+
// tearing down natively — wait for them too.
|
|
612
|
+
for (const entry of disposingEntries) {
|
|
613
|
+
if (entry.ctx.modelId === modelId)
|
|
614
|
+
draining.push(entry.disposed);
|
|
615
|
+
}
|
|
616
|
+
// Queued waiters never held a slot, so just abort their begins.
|
|
617
|
+
for (const { key, waiter } of Array.from(waitersById.values())) {
|
|
618
|
+
if (waiter.modelId === modelId)
|
|
619
|
+
cancelQueuedWaiterGracefully(key, waiter, 'modelUnload');
|
|
620
|
+
}
|
|
621
|
+
await Promise.all(draining);
|
|
622
|
+
await teardown();
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
drainingModels.delete(modelId);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
464
628
|
async function end(requestId, outcome) {
|
|
465
629
|
const entry = entries.get(requestId);
|
|
466
630
|
if (!entry)
|
|
@@ -476,6 +640,7 @@ export function createRequestRegistry(options) {
|
|
|
476
640
|
list,
|
|
477
641
|
cancel,
|
|
478
642
|
cancelAll,
|
|
643
|
+
withModelDraining,
|
|
479
644
|
end,
|
|
480
645
|
policy,
|
|
481
646
|
// Test-only: lets the registry race tests assert the bound
|
|
@@ -490,6 +655,12 @@ export function createRequestRegistry(options) {
|
|
|
490
655
|
__keyStateSize: () => keyStates.size
|
|
491
656
|
};
|
|
492
657
|
}
|
|
658
|
+
// A finite slot count floored to an integer >= 1: a value below 1 would gate
|
|
659
|
+
// every request forever and a fractional one could over-admit. Callers gate on
|
|
660
|
+
// `Number.isFinite` first — Infinity means "ungated" and must stay Infinity.
|
|
661
|
+
function clampSlotCount(count) {
|
|
662
|
+
return Math.max(1, Math.floor(count));
|
|
663
|
+
}
|
|
493
664
|
/**
|
|
494
665
|
* Resolve a `ConcurrencyPolicy` to concrete numbers, applying defaults and
|
|
495
666
|
* the deprecated `oneAtATimePerModel` alias. Done once at registration so
|
|
@@ -512,16 +683,15 @@ function normalizePolicy(opts) {
|
|
|
512
683
|
maxConcurrent = Infinity;
|
|
513
684
|
onOverflow = opts.onOverflow ?? 'queue';
|
|
514
685
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (Number.isFinite(maxConcurrent) && maxConcurrent < 1)
|
|
518
|
-
maxConcurrent = 1;
|
|
686
|
+
if (Number.isFinite(maxConcurrent))
|
|
687
|
+
maxConcurrent = clampSlotCount(maxConcurrent);
|
|
519
688
|
return {
|
|
520
689
|
maxConcurrent,
|
|
521
690
|
onOverflow,
|
|
522
691
|
maxQueueDepth: opts.maxQueueDepthPerModel ?? DEFAULT_MAX_QUEUE_DEPTH_PER_MODEL,
|
|
523
692
|
queueTimeoutMs: opts.queueTimeoutMs,
|
|
524
|
-
slotGroup: opts.sharedSlotGroup
|
|
693
|
+
slotGroup: opts.sharedSlotGroup,
|
|
694
|
+
exclusive: opts.exclusive === true
|
|
525
695
|
};
|
|
526
696
|
}
|
|
527
697
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type ASRGgml from '@qvac/asr-ggml';
|
|
2
|
+
import type TranslationNmtcpp from '@qvac/translation-nmtcpp';
|
|
2
3
|
export interface LlmStats {
|
|
3
4
|
TTFT?: number;
|
|
4
5
|
TPS?: number;
|
|
@@ -7,23 +8,14 @@ export interface LlmStats {
|
|
|
7
8
|
generatedTokens?: number;
|
|
8
9
|
avgConcurrentSeq?: number;
|
|
9
10
|
backendDevice?: 'cpu' | 'gpu';
|
|
11
|
+
stopReason?: 'none' | 'eos' | 'antiprompt' | 'predictionLimit' | 'sequenceLimit' | 'contextOverflow';
|
|
10
12
|
}
|
|
11
13
|
export interface LlmResponse {
|
|
12
14
|
stats?: LlmStats;
|
|
13
15
|
iterate(): AsyncIterable<string>;
|
|
14
16
|
}
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
totalTokens?: number;
|
|
18
|
-
decodeTime?: number;
|
|
19
|
-
encodeTime?: number;
|
|
20
|
-
TPS?: number;
|
|
21
|
-
TTFT?: number;
|
|
22
|
-
}
|
|
23
|
-
export interface NmtResponse {
|
|
24
|
-
stats?: NmtStats;
|
|
25
|
-
iterate(): AsyncIterable<string>;
|
|
26
|
-
}
|
|
17
|
+
export type NmtStats = TranslationNmtcpp.RuntimeStats;
|
|
18
|
+
export type NmtResponse = TranslationNmtcpp.TranslationResponse;
|
|
27
19
|
export interface TtsStats {
|
|
28
20
|
audioDurationMs?: number;
|
|
29
21
|
totalSamples?: number;
|
|
@@ -74,7 +74,7 @@ export async function handleLoadModel(request, progressCallback) {
|
|
|
74
74
|
if (isReloadConfigRequest(request)) {
|
|
75
75
|
return handleConfigReload(request);
|
|
76
76
|
}
|
|
77
|
-
const { modelSrc, modelName, seed } = request;
|
|
77
|
+
const { modelSrc, modelName, seed, fallbackSrc } = request;
|
|
78
78
|
const canonicalModelType = normalizeModelType(request.modelType);
|
|
79
79
|
const profilingMeta = request[PROFILING_KEY];
|
|
80
80
|
const profilingEnabled = profilingMeta?.enabled !== false && !!profilingMeta;
|
|
@@ -113,7 +113,7 @@ export async function handleLoadModel(request, progressCallback) {
|
|
|
113
113
|
requestId
|
|
114
114
|
}
|
|
115
115
|
});
|
|
116
|
-
const primaryResolve = session.resolvePrimaryModelPath(modelSrc);
|
|
116
|
+
const primaryResolve = session.resolvePrimaryModelPath(modelSrc, fallbackSrc);
|
|
117
117
|
let resolvedModelPath;
|
|
118
118
|
let pluginResolveResult;
|
|
119
119
|
try {
|
|
@@ -25,7 +25,7 @@ export interface ResolveSessionOptions {
|
|
|
25
25
|
requestBinding?: DownloadRequestBinding | undefined;
|
|
26
26
|
}
|
|
27
27
|
export interface ResolveSession {
|
|
28
|
-
resolvePrimaryModelPath(modelSrc: unknown): Promise<string>;
|
|
28
|
+
resolvePrimaryModelPath(modelSrc: unknown, fallbackSrc?: string): Promise<string>;
|
|
29
29
|
createResolveContext(modelSrc: string, modelType: string, modelName?: string): ResolveContext;
|
|
30
30
|
getAggregateResult(): ResolveResult | undefined;
|
|
31
31
|
cancelAll(): void;
|