mlx-bun 0.3.0 → 0.4.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/docs/README.md +4 -1
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/design/batching.md +1321 -49
- package/docs/design/decode-speed-program.md +331 -1163
- package/docs/design/generic-model-support.md +79 -3
- package/docs/design/kv-cache.md +198 -54
- package/docs/design/speculative-decoding.md +387 -24
- package/docs/design/turboquant.md +123 -13
- package/docs/design/unified-engine-frontier-plan.md +144 -142
- package/docs/planning/PRODUCT_ROADMAP.md +16 -0
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/reference/benchmarks.md +2457 -6
- package/docs/reference/cli.md +41 -2
- package/docs/reference/distribution.md +16 -9
- package/docs/reference/environment.md +55 -5
- package/docs/reference/models.md +13 -4
- package/docs/reference/server-api.md +79 -25
- package/docs/reference/server-config.md +178 -84
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +1 -1
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/batch-group.ts +368 -224
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +106 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +77 -18
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/model-serving.ts +1 -1
- package/src/backends/mlx/prefill-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +41 -3
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +35 -66
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
- package/src/cli.ts +11 -36
- package/src/contracts/execution.ts +11 -0
- package/src/contracts/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +18 -0
- package/src/engine/execution-plan.ts +18 -16
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +10 -5
- package/src/fit.ts +29 -15
- package/src/generate.ts +77 -173
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +70 -12
- package/src/kv-store.ts +70 -16
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/lab/paged-kv/paged-kv.ts +25 -11
- package/src/mlx/array.ts +2 -1
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +93 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +55 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +162 -80
- package/src/model/gemma4.ts +34 -26
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +67 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5.ts +82 -42
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +42 -44
- package/src/sampler.ts +36 -25
- package/src/serve/chat-stage.ts +2 -2
- package/src/serve/generation-gateway.ts +39 -30
- package/src/serve/model-host.ts +3 -4
- package/src/serve/request-plan.ts +4 -3
- package/src/serve/request-prep.ts +2 -4
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/server.ts +64 -108
- package/src/spec/assistant-rows.ts +92 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +53 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +92 -20
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +219 -0
- package/src/spec/qwen-mtp-source.ts +81 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +70 -68
- package/src/spec/source.ts +80 -15
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +43 -101
- package/src/ssd-cache.ts +33 -21
- package/src/ssd-durability.ts +11 -7
- package/src/web/app.js +57 -57
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
|
@@ -92,10 +92,8 @@ export function createRequestPrep(input: {
|
|
|
92
92
|
? Math.min(genTemp, NO_THINK_TEMPERATURE)
|
|
93
93
|
: genTemp;
|
|
94
94
|
return {
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// and clamps to maxSafeContext − promptLen. Only an explicit request
|
|
98
|
-
// max_tokens or the operator's --max-tokens narrows it further.
|
|
95
|
+
// Unset means generate until EOS/stop. Only a client cap or an explicit
|
|
96
|
+
// operator limit constrains completion length; fit estimates do not.
|
|
99
97
|
maxTokens: req.max_completion_tokens ?? req.max_tokens ??
|
|
100
98
|
defaultGeneratedTokens,
|
|
101
99
|
temperature: req.temperature ?? serverOptions.defaultTemperature ?? defaultTemp,
|
|
@@ -16,7 +16,7 @@ export class TextCompletionStage {
|
|
|
16
16
|
constructor(
|
|
17
17
|
private readonly ctx: ServingContext,
|
|
18
18
|
private readonly prep: RequestPrep,
|
|
19
|
-
private readonly
|
|
19
|
+
private readonly contextLimit: number | null,
|
|
20
20
|
/** `--max-tokens` / the GLM plan's reservation. No invented fallback:
|
|
21
21
|
* unset runs to EOS or the admitted context (with no template an EOS
|
|
22
22
|
* may never come — that run is bounded by admission, the real limit). */
|
|
@@ -81,7 +81,7 @@ export class TextCompletionStage {
|
|
|
81
81
|
promptIds,
|
|
82
82
|
options,
|
|
83
83
|
requestedMaxTokens,
|
|
84
|
-
|
|
84
|
+
contextLimit: this.contextLimit,
|
|
85
85
|
stream: request.stream,
|
|
86
86
|
wantLogprobs,
|
|
87
87
|
topLogprobs,
|
package/src/server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ContinuationPersistence } from "./backends/mlx/continuation-persistence";
|
|
1
2
|
import { modelServingBinding } from "./backends/mlx/model-serving";
|
|
2
3
|
import type { ServingContext } from "./serve/model-host";
|
|
3
4
|
export { generationCheckpointKey } from "./serve/checkpoint-identity";
|
|
@@ -109,13 +110,15 @@ export interface ServerOptions {
|
|
|
109
110
|
* (`--kv-budget`, batching-perf-path P3). Joiners whose projected KV
|
|
110
111
|
* (prompt + max_tokens, window-capped) would exceed it QUEUE until rows
|
|
111
112
|
* evict; a request over the budget alone is rejected. Unset = no
|
|
112
|
-
* aggregate cap (
|
|
113
|
+
* aggregate cap (an explicit memoryBudget also limits each request). */
|
|
113
114
|
kvBudgetBytes?: number;
|
|
114
115
|
/** KV quantization override. Unset/"off" is bf16. "config" applies the
|
|
115
116
|
* model's declared mixed-precision kv_config; supported per-layer schemes
|
|
116
117
|
* compose with continuous scheduling. A number forces uniform bits
|
|
117
118
|
* (group size 64, start 0) and uses the preserved serial executor. */
|
|
118
119
|
kvQuant?: "off" | "config" | number;
|
|
120
|
+
/** Optional absolute row offset for KV conversion; omitted preserves start-zero server policy. */
|
|
121
|
+
quantizedKvStart?: number;
|
|
119
122
|
/** TurboQuant scheme (docs/design/turboquant.md): a separate axis from
|
|
120
123
|
* kvQuant above, mutually exclusive with it (`--kv-quant turbo[:k<bits>v
|
|
121
124
|
* <bits>]` sets this instead of kvQuant). Solo-only in v1 — see
|
|
@@ -124,19 +127,13 @@ export interface ServerOptions {
|
|
|
124
127
|
/** OPTIONAL paged KV cache (`--paged-kv`, docs/design/kv-cache.md):
|
|
125
128
|
* vLLM-style block-pool storage for full-attention layers,
|
|
126
129
|
* gather-to-contiguous before the stock SDPA. Default off (unset = the
|
|
127
|
-
* plain KVCache path, byte-identical).
|
|
128
|
-
*
|
|
130
|
+
* plain KVCache path, byte-identical). Gemma4-family, bf16, B1/B>1;
|
|
131
|
+
* startup refuses `--kv-quant`,
|
|
129
132
|
* turbo, and `--draft-model` combinations; paged requests bypass the
|
|
130
133
|
* prompt cache and run uncompiled. Gated bit-exact vs the plain path. */
|
|
131
134
|
pagedKv?: { blockSize?: number };
|
|
132
|
-
/**
|
|
133
|
-
*
|
|
134
|
-
* context are rejected with 400 instead of crashing the GPU: the OOM
|
|
135
|
-
* crash class is UNCATCHABLE (Phase 6 — mlx throws from a Metal
|
|
136
|
-
* completion handler ⇒ std::terminate; optiq serve died exactly this
|
|
137
|
-
* way loading the 26B). Also caps the mlx allocator
|
|
138
|
-
* (mlx_set_memory_limit) as defense in depth. Default: machine RAM ×
|
|
139
|
-
* WIRED_FRACTION, admission check only, allocator untouched. */
|
|
135
|
+
/** Explicit opt-in process budget. Enforces the fit estimate's context
|
|
136
|
+
* limit and sets the MLX allocator limit. Unset keeps estimates advisory. */
|
|
140
137
|
memoryBudgetBytes?: number;
|
|
141
138
|
/** Who owns this server's lifetime: "serve" (persistent, mlx-bun
|
|
142
139
|
* serve) or "pi-session" (dies with the pi session that started
|
|
@@ -287,16 +284,8 @@ export interface ContextAdmissionDecision {
|
|
|
287
284
|
clamped: boolean;
|
|
288
285
|
}
|
|
289
286
|
|
|
290
|
-
/** Resolve a
|
|
291
|
-
*
|
|
292
|
-
* `max_tokens` is a ceiling, not a promise that every token will be emitted:
|
|
293
|
-
* when a prompt fits but a broad client-wide `max_tokens` would push the
|
|
294
|
-
* reservation past the safe context, the bound is CLAMPED to the remaining
|
|
295
|
-
* room instead of rejecting an otherwise valid request (the pre-v0.0.13
|
|
296
|
-
* behavior 400'd a prompt with thousands of tokens of generation room over a
|
|
297
|
-
* 17-token overshoot). This never weakens the OOM guard — generation stops at
|
|
298
|
-
* the clamped ceiling, inside the admitted context. Only a prompt that leaves
|
|
299
|
-
* no generation slot at all is rejected. */
|
|
287
|
+
/** Resolve a completion cap against an explicitly enforced context limit.
|
|
288
|
+
* Serving keeps fit estimates advisory unless the operator chooses a budget. */
|
|
300
289
|
export function admitRequestContext(
|
|
301
290
|
promptTokens: number,
|
|
302
291
|
requestedMaxTokens: number,
|
|
@@ -334,7 +323,7 @@ export function createServer(
|
|
|
334
323
|
// Both full-attention (CPM) and
|
|
335
324
|
// sliding-window (Gemma) models batch — the scheduler assembles each layer's
|
|
336
325
|
// cache by attention type. Non-batchable requests (vision / adapters /
|
|
337
|
-
//
|
|
326
|
+
// unsupported explicit kv-quant) drain to the serial executor
|
|
338
327
|
// (see GenerationGateway.place). No inference setting is rewritten.
|
|
339
328
|
// DEFAULT 8 (flipped 2026-07-05, Josh's call, after GATE-B1-SPEED): a
|
|
340
329
|
// lone request through the batch lane IS the serial engine (adopted
|
|
@@ -352,8 +341,7 @@ export function createServer(
|
|
|
352
341
|
throw new Error("--generation-checkpoint expects a positive integer token interval");
|
|
353
342
|
if (!serverOptions.ssdCacheDir)
|
|
354
343
|
throw new Error("--generation-checkpoint requires --ssd-cache <dir>");
|
|
355
|
-
|
|
356
|
-
throw new Error("--generation-checkpoint requires --batch 1 (in-flight resume is serial-only)");
|
|
344
|
+
|
|
357
345
|
}
|
|
358
346
|
const defaultGeneratedTokens =
|
|
359
347
|
serverOptions.defaultMaxTokens ?? ctx.glmMemoryPlan?.maxGenerationTokens;
|
|
@@ -376,32 +364,11 @@ export function createServer(
|
|
|
376
364
|
const resolvedKvScheme = resolveKvScheme({
|
|
377
365
|
override: serverOptions.kvQuant,
|
|
378
366
|
turboQuant: serverOptions.turboQuant,
|
|
367
|
+
quantizedKvStart: serverOptions.quantizedKvStart,
|
|
379
368
|
config: ctx.kvConfig,
|
|
380
369
|
});
|
|
381
370
|
const kvScheme = resolvedKvScheme.generationOptions;
|
|
382
|
-
//
|
|
383
|
-
// scheduler applies the mixed scheme per row); uniform kvBits and configs
|
|
384
|
-
// touching rotating layers still route those requests serial. The warning
|
|
385
|
-
// only fires for the still-serial compositions.
|
|
386
|
-
if (batch > 1 && kvScheme.kvBits)
|
|
387
|
-
console.warn(
|
|
388
|
-
`[batch] --batch ${batch} with uniform --kv-quant ${kvScheme.kvBits}: uniform ` +
|
|
389
|
-
`quantized KV is serial-only (it touches rotating layers) — those requests ` +
|
|
390
|
-
`won't batch. Use --kv-quant config for batched mixed-precision KV, or omit ` +
|
|
391
|
-
`--kv-quant to batch in bf16. (docs/design/unified-engine-frontier-plan.md)`,
|
|
392
|
-
);
|
|
393
|
-
// TurboQuant is solo-only in v1 (novel cache class, not batchable by
|
|
394
|
-
// construction — see GenerationGateway placement's cache-capability gate).
|
|
395
|
-
if (batch > 1 && kvScheme.turboQuant)
|
|
396
|
-
console.warn(
|
|
397
|
-
`[batch] --batch ${batch} with --kv-quant turbo: TurboQuant is serial-only in v1 ` +
|
|
398
|
-
`— those requests won't batch. Omit --kv-quant to batch in bf16. ` +
|
|
399
|
-
`(docs/design/turboquant.md)`,
|
|
400
|
-
);
|
|
401
|
-
// Quantized KV (any axis) excludes the spec lane: drafted requests fall to
|
|
402
|
-
// the normal serial path WITH the KV scheme applied rather than losing it
|
|
403
|
-
// silently (mirrors the affine kvBits/kvConfig exclusion; the spec loop is
|
|
404
|
-
// bf16-KV-only in v1).
|
|
371
|
+
// The model execution plan qualifies the method while retaining KV policy.
|
|
405
372
|
if (ctx.draft && (kvScheme.turboQuant || kvScheme.kvBits || kvScheme.kvConfig?.length))
|
|
406
373
|
console.warn(
|
|
407
374
|
`[spec] --draft-model with quantized KV (--kv-quant ${serverOptions.turboQuant ? "turbo" : String(serverOptions.kvQuant)}): ` +
|
|
@@ -427,15 +394,9 @@ export function createServer(
|
|
|
427
394
|
}
|
|
428
395
|
// Paged KV v1 (docs/design/kv-cache.md): explicit refusals, not
|
|
429
396
|
// silent downgrades — the incompatible combos would otherwise degrade
|
|
430
|
-
// quietly (
|
|
431
|
-
//
|
|
432
|
-
// exists to prevent; draft: the spec lane assumes serial-class caches).
|
|
397
|
+
// quietly (kv-quant: the swap would drop the scheme; speculative
|
|
398
|
+
// provider composition with paged storage is not implemented).
|
|
433
399
|
if (serverOptions.pagedKv) {
|
|
434
|
-
if (batch > 1)
|
|
435
|
-
throw new Error(
|
|
436
|
-
`--paged-kv is serial-only in v1 — add --batch 1 (got --batch ${batch}). ` +
|
|
437
|
-
`Batched paging is the follow-up PR (docs/design/kv-cache.md).`,
|
|
438
|
-
);
|
|
439
400
|
if (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant)
|
|
440
401
|
throw new Error(
|
|
441
402
|
`--paged-kv is bf16-only in v1 — omit --kv-quant (quantized paged ` +
|
|
@@ -521,11 +482,12 @@ export function createServer(
|
|
|
521
482
|
// own their bytes and no mapping outlives loadKvCache — nothing
|
|
522
483
|
// to pin, nothing to unmap. retain stays in the entry contract
|
|
523
484
|
// as a no-op so callers' dispose ordering is unchanged.
|
|
524
|
-
return { tokens: loaded.tokens, caches: loaded.caches, retain: () => {} };
|
|
485
|
+
return { tokens: loaded.tokens, caches: loaded.caches, attachments: loaded.attachments, retain: () => {} };
|
|
525
486
|
},
|
|
526
|
-
store: (tokens: number[], caches: import("./model/gemma4").Cache[], ns: string
|
|
487
|
+
store: (tokens: number[], caches: import("./model/gemma4").Cache[], ns: string,
|
|
488
|
+
attachments?: import("./backends/mlx/checkpoint-state").CheckpointAttachment[]) => {
|
|
527
489
|
if (ssdStore!.hasDurablePrefix(tokens, ns)) return true;
|
|
528
|
-
return ssdStore!.store(tokens, caches, ns);
|
|
490
|
+
return ssdStore!.store(tokens, caches, ns, attachments);
|
|
529
491
|
},
|
|
530
492
|
}
|
|
531
493
|
: null;
|
|
@@ -546,7 +508,7 @@ export function createServer(
|
|
|
546
508
|
ssdStore
|
|
547
509
|
? {
|
|
548
510
|
spillOwned: (entry) =>
|
|
549
|
-
spillQueue!.enqueue({ tokens: entry.tokens, caches: entry.caches, ns: entry.ns }),
|
|
511
|
+
spillQueue!.enqueue({ tokens: entry.tokens, caches: entry.caches, ns: entry.ns, attachments: entry.attachments }),
|
|
550
512
|
}
|
|
551
513
|
: null,
|
|
552
514
|
coldTier,
|
|
@@ -558,56 +520,38 @@ export function createServer(
|
|
|
558
520
|
// so its KV prefill is already cached.
|
|
559
521
|
const responseStore = new ResponseStore();
|
|
560
522
|
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
// transient. The active kv-quant scheme (uniform kvBits / per-layer
|
|
564
|
-
// kvConfig) is billed at its true bytes/element so a quantized cache
|
|
565
|
-
// advertises and admits the larger window it actually enables; only
|
|
566
|
-
// TurboQuant still bills bf16 (conservative — no projector for its
|
|
567
|
-
// layout yet, and it is solo-only in v1).
|
|
523
|
+
// Memory estimates remain available in diagnostics. Only an explicit
|
|
524
|
+
// operator budget or context cap constrains request planning.
|
|
568
525
|
const memoryAdmission = ctx.glmMemoryPlan ?? fit(
|
|
569
526
|
ctx.model.config, ctx.model.weightsBytes, 1,
|
|
570
527
|
undefined, undefined, 0, serverOptions.memoryBudgetBytes,
|
|
571
528
|
resolvedKvScheme.fitOptions,
|
|
572
529
|
);
|
|
573
|
-
// Isolated benchmark profile cap. It may narrow physical admission, never
|
|
574
|
-
// enlarge it. The normal request planner still clamps prompt + output.
|
|
575
530
|
const profileContext = runtimeValue("MLX_BUN_RD_CONTEXT_LIMIT");
|
|
576
|
-
const
|
|
577
|
-
if (
|
|
531
|
+
const profileLimit = profileContext === undefined ? null : Number(profileContext);
|
|
532
|
+
if (profileLimit !== null && (!Number.isSafeInteger(profileLimit) || profileLimit < 1))
|
|
578
533
|
throw new Error("MLX_BUN_RD_CONTEXT_LIMIT must be a positive integer");
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// still fires, with this same ceiling) or be a false positive from the
|
|
585
|
-
// fit model itself — it can never save anything the request gate
|
|
586
|
-
// doesn't. Serve until physically incapable.
|
|
587
|
-
if (admission.maxSafeContext < 1)
|
|
588
|
-
console.warn(
|
|
589
|
-
`[admission] memory budget ${(admission.usableBytes / 1e9).toFixed(2)} GB leaves no ` +
|
|
590
|
-
`safe context for ${ctx.modelId} (weights ${(ctx.model.weightsBytes / 1e9).toFixed(2)} GB) ` +
|
|
591
|
-
`— serving anyway; generation requests will be refused until the budget is raised`,
|
|
592
|
-
);
|
|
534
|
+
const budgetLimit = serverOptions.memoryBudgetBytes !== undefined
|
|
535
|
+
? memoryAdmission.maxSafeContext : ctx.glmMemoryPlan?.contextTokens ?? null;
|
|
536
|
+
const contextLimit = profileLimit === null ? budgetLimit
|
|
537
|
+
: Math.min(profileLimit, budgetLimit ?? Infinity);
|
|
538
|
+
const admission = memoryAdmission;
|
|
593
539
|
const allocatorLimit =
|
|
594
540
|
admission.allocatorLimitBytes ?? serverOptions.memoryBudgetBytes;
|
|
595
541
|
if (allocatorLimit) setMemoryLimit(allocatorLimit);
|
|
596
542
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
promptCache,
|
|
601
|
-
};
|
|
602
|
-
const runGeneration = serving.createSerial({
|
|
543
|
+
let checkpointPersistence: ContinuationPersistence | undefined;
|
|
544
|
+
const executionServices: import("./backends/mlx/serial-executor").MlxSerialServices = {
|
|
545
|
+
get checkpointPersistence() { return checkpointPersistence; },
|
|
603
546
|
promptCache, checkpoints: ssdStore,
|
|
604
|
-
memoryBudget,
|
|
605
547
|
checkpointEveryTokens: serverOptions.generationCheckpointTokens,
|
|
606
548
|
identity: { artifact: ctx.profile.artifact, implementation: ctx.profile.profile.execution,
|
|
607
549
|
stateAbi: "legacy-cache-array-v1", codecs: stateCodecs.id },
|
|
608
550
|
adapterNamespace: (adapters) => ctx.adapters.cacheNamespace(adapters),
|
|
609
551
|
cloneState,
|
|
610
|
-
}
|
|
552
|
+
};
|
|
553
|
+
serving.gateway.configureContinuation?.(executionServices);
|
|
554
|
+
const runGeneration = serving.createSerial(executionServices);
|
|
611
555
|
|
|
612
556
|
// Write-behind persistence (restart survival — the oMLX boundary-snapshot
|
|
613
557
|
// idea at whole-entry granularity, spill-on-evict alone can't survive a
|
|
@@ -637,7 +581,7 @@ export function createServer(
|
|
|
637
581
|
// pacing interleaved those slices exactly between decode tokens. A ~16k
|
|
638
582
|
// entry's flush overlapping the bench's ctx repeats depressed decode@ctx
|
|
639
583
|
// ~9% on e4b (mlx-lm runs no equivalent background work). Now every step
|
|
640
|
-
// — including the first — runs through gateway.
|
|
584
|
+
// — including the first — runs through gateway.runWhenIdle(), so the
|
|
641
585
|
// flush only progresses while NOTHING is generating and pauses when a
|
|
642
586
|
// request arrives mid-flush. Tradeoffs, accepted: durability waits for a
|
|
643
587
|
// quiet moment (single-user serving quiesces constantly; sustained
|
|
@@ -647,7 +591,7 @@ export function createServer(
|
|
|
647
591
|
// switch (restart survival then degrades to spill-on-evict only).
|
|
648
592
|
const writeBehindOn = runtimeValue("MLX_BUN_SSD_WRITEBEHIND") !== "0";
|
|
649
593
|
const ssdFlushStep = <T>(step: () => T): Promise<T> =>
|
|
650
|
-
gateway.
|
|
594
|
+
gateway.runWhenIdle(async () => step());
|
|
651
595
|
// Bounded write-behind queue (2026-07-07 review fix — see SpillQueue in
|
|
652
596
|
// kv-store.ts): pending clones pin their entries' GPU buffers while the
|
|
653
597
|
// idle gate starves under sustained traffic, so QUEUED bytes are capped —
|
|
@@ -664,21 +608,26 @@ export function createServer(
|
|
|
664
608
|
// its idle boundary to avoid competing with generation.
|
|
665
609
|
const gateway = new GenerationGateway(serving.gateway, batch, runGeneration, {
|
|
666
610
|
kvBudgetBytes: serverOptions.kvBudgetBytes,
|
|
667
|
-
memoryBudget,
|
|
668
611
|
checkpoints: !!(serverOptions.generationCheckpointTokens && ssdStore),
|
|
669
612
|
stateCodecs,
|
|
670
613
|
kvScheme: resolvedKvScheme,
|
|
671
614
|
promptCache,
|
|
672
|
-
|
|
615
|
+
adapterNamespace: (adapters) => ctx.adapters.cacheNamespace(adapters),
|
|
673
616
|
});
|
|
674
617
|
const spillQueueGbRaw = Number(runtimeValue("MLX_BUN_SSD_SPILL_QUEUE_GB"));
|
|
675
618
|
const spillQueueCapBytes =
|
|
676
619
|
(Number.isFinite(spillQueueGbRaw) && spillQueueGbRaw >= 0 ? spillQueueGbRaw : 2) * 1024 ** 3;
|
|
620
|
+
// Both queues share the configured total: reserve half for checkpoints
|
|
621
|
+
// only when that service exists. Each retains the existing soft-cap rule.
|
|
622
|
+
const checkpointQueueBytes = ssdStore && serverOptions.generationCheckpointTokens
|
|
623
|
+
? Math.floor(spillQueueCapBytes / 2) : 0;
|
|
624
|
+
if (ssdStore && serverOptions.generationCheckpointTokens)
|
|
625
|
+
checkpointPersistence = new ContinuationPersistence(ssdStore, { maxBytes: checkpointQueueBytes, runStep: ssdFlushStep });
|
|
677
626
|
const spillQueue = ssdStore
|
|
678
627
|
? new SpillQueue(
|
|
679
|
-
spillQueueCapBytes,
|
|
628
|
+
spillQueueCapBytes - checkpointQueueBytes,
|
|
680
629
|
cacheBytes,
|
|
681
|
-
(item) => ssdStore!.storeAsync(item.tokens, item.caches, item.ns, ssdFlushStep),
|
|
630
|
+
(item) => ssdStore!.storeAsync(item.tokens, item.caches, item.ns, ssdFlushStep, item.attachments),
|
|
682
631
|
(caches) => { for (const c of caches) c.dispose(); },
|
|
683
632
|
)
|
|
684
633
|
: null;
|
|
@@ -691,24 +640,31 @@ export function createServer(
|
|
|
691
640
|
(tokens, ns) => ssdStore.hasDurablePrefix(tokens, ns),
|
|
692
641
|
)
|
|
693
642
|
: null;
|
|
694
|
-
const durabilityStats = (): DurabilitySnapshotStats =>
|
|
695
|
-
durability?.stats ?? {
|
|
643
|
+
const durabilityStats = (): DurabilitySnapshotStats => {
|
|
644
|
+
const stats = durability?.stats ?? {
|
|
696
645
|
pendingSnapshots: 0,
|
|
697
646
|
pendingSpills: spillQueue?.pendingCount ?? 0,
|
|
698
647
|
pendingSpillBytes: spillQueue?.pendingBytes ?? 0,
|
|
699
648
|
droppedSpills: spillQueue?.droppedCount ?? 0,
|
|
700
649
|
failedSpills: spillQueue?.failedCount ?? 0,
|
|
701
650
|
};
|
|
651
|
+
const checkpoints = checkpointPersistence?.stats;
|
|
652
|
+
return { ...stats, pendingSpills: stats.pendingSpills + (checkpoints?.pendingCount ?? 0),
|
|
653
|
+
pendingSpillBytes: stats.pendingSpillBytes + (checkpoints?.pendingBytes ?? 0),
|
|
654
|
+
droppedSpills: stats.droppedSpills + (checkpoints?.dropped ?? 0),
|
|
655
|
+
failedSpills: stats.failedSpills + (checkpoints?.failed ?? 0) };
|
|
656
|
+
};
|
|
702
657
|
const flushDurability = async (): Promise<DurabilityFlushResult> => {
|
|
703
|
-
if (durability) return durability.flush();
|
|
704
658
|
const started = performance.now();
|
|
705
|
-
await
|
|
659
|
+
const checkpoints = await checkpointPersistence?.flush();
|
|
660
|
+
const result = durability ? await durability.flush() : null;
|
|
661
|
+
if (!result) await spillQueue?.drain();
|
|
706
662
|
const stats = durabilityStats();
|
|
707
663
|
return {
|
|
708
664
|
...stats,
|
|
709
|
-
durable: stats.pendingSpills === 0,
|
|
710
|
-
flushedSnapshots: 0,
|
|
711
|
-
missingSnapshots: 0,
|
|
665
|
+
durable: (result?.durable ?? stats.pendingSpills === 0) && (checkpoints?.durable ?? true) && stats.pendingSpills === 0,
|
|
666
|
+
flushedSnapshots: result?.flushedSnapshots ?? 0,
|
|
667
|
+
missingSnapshots: result?.missingSnapshots ?? 0,
|
|
712
668
|
elapsedMs: performance.now() - started,
|
|
713
669
|
};
|
|
714
670
|
};
|
|
@@ -777,10 +733,10 @@ export function createServer(
|
|
|
777
733
|
const { templateOptionsFor } = prep;
|
|
778
734
|
const preparation = createPreparationExecutor((work, signal) => gateway.runExclusive(work, undefined, signal), batch);
|
|
779
735
|
const chatStage = new ChatStage(
|
|
780
|
-
ctx, prep, promptCache,
|
|
736
|
+
ctx, prep, promptCache, contextLimit, serverOptions.defaultAdapter,
|
|
781
737
|
preparation, serving.buildPrompt);
|
|
782
738
|
const textStage = new TextCompletionStage(
|
|
783
|
-
ctx, prep,
|
|
739
|
+
ctx, prep, contextLimit, defaultGeneratedTokens, serverOptions.defaultAdapter);
|
|
784
740
|
const inferenceStage = new InferenceStage(completionExecutor);
|
|
785
741
|
const openAiMeta = (id: string) => ({ id, created: Math.floor(Date.now() / 1000), model: ctx.modelId });
|
|
786
742
|
/** Parse the JSON body under the request's trace; a bad body is a 400. */
|
|
@@ -1049,6 +1005,7 @@ export function createServer(
|
|
|
1049
1005
|
},
|
|
1050
1006
|
admission: {
|
|
1051
1007
|
max_safe_context: admission.maxSafeContext,
|
|
1008
|
+
enforced_context_tokens: contextLimit,
|
|
1052
1009
|
memory_budget_bytes:
|
|
1053
1010
|
ctx.glmMemoryPlan?.processLimitBytes ??
|
|
1054
1011
|
serverOptions.memoryBudgetBytes ?? null,
|
|
@@ -1074,7 +1031,6 @@ export function createServer(
|
|
|
1074
1031
|
// A parent-managed GPU job holds this response open. The connection owns
|
|
1075
1032
|
// the native lease, so parent death cannot strand the worker lock.
|
|
1076
1033
|
if (serverOptions.unixSocket && url.pathname === "/admin/lease" && request.method === "POST") {
|
|
1077
|
-
await flushDurability();
|
|
1078
1034
|
const lease = await gateway.acquireExecutionLease(request.signal);
|
|
1079
1035
|
let released = false;
|
|
1080
1036
|
const release = () => {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { materializeCopy } from "../mlx/materialize";
|
|
4
|
+
import { disposeResources, applyStateChanges } from "../engine/resources";
|
|
5
|
+
import type { PreparedStateChange } from "../contracts/resources";
|
|
6
|
+
import type { AssistantRowsTarget, DraftPrefillGroup, DraftRowGroup, DraftRowCheckpoint, GroupedDraftProvider } from "./source";
|
|
7
|
+
import type { GemmaAssistantDrafter } from "./drafter";
|
|
8
|
+
|
|
9
|
+
/** The assistant owns only its last true target hidden and coverage. Donor
|
|
10
|
+
* attention stays with target storage; checkpoints contain no duplicate KV. */
|
|
11
|
+
class AssistantRows implements DraftPrefillGroup, DraftRowGroup {
|
|
12
|
+
readonly prefillMode = "tail-split" as const;
|
|
13
|
+
readonly tapLayers: readonly number[] = [];
|
|
14
|
+
#hidden: MlxArray | null = null;
|
|
15
|
+
#processed: number[] = [];
|
|
16
|
+
constructor(readonly target: AssistantRowsTarget, readonly drafter: GemmaAssistantDrafter,
|
|
17
|
+
readonly namespace: string, checkpoints: readonly (DraftRowCheckpoint | null)[]) { this.append(checkpoints); }
|
|
18
|
+
get rowCount(): number { return this.#processed.length; }
|
|
19
|
+
append(checkpoints: readonly (DraftRowCheckpoint | null)[]): void {
|
|
20
|
+
if (checkpoints.length) applyStateChanges([() => this.prepareAppend(checkpoints)]);
|
|
21
|
+
}
|
|
22
|
+
prepareAppend(checkpoints: readonly (DraftRowCheckpoint | null)[]): PreparedStateChange {
|
|
23
|
+
const processed = [...this.#processed, ...checkpoints.map(checkpoint => checkpoint?.processedTokens ?? 0)];
|
|
24
|
+
const example = this.#hidden ?? checkpoints.find(checkpoint => checkpoint !== null)?.attachment.tensors[0];
|
|
25
|
+
const held: MlxArray[] = []; let hidden: MlxArray | null = null;
|
|
26
|
+
try {
|
|
27
|
+
for (const checkpoint of checkpoints) if (checkpoint && (checkpoint.attachment.schema !== "gemma-assistant-v1" ||
|
|
28
|
+
checkpoint.attachment.metadata.processedTokens !== checkpoint.processedTokens))
|
|
29
|
+
throw new Error("invalid assistant checkpoint alignment");
|
|
30
|
+
if (example) {
|
|
31
|
+
const zero = (rows: number) => { const value = ops.zeros([rows,1,this.target.hiddenSize],example.dtype); held.push(value); return value; };
|
|
32
|
+
hidden = ops.concatAxis([...(this.rowCount ? [this.#hidden ?? zero(this.rowCount)] : []),
|
|
33
|
+
...checkpoints.map(checkpoint => checkpoint?.attachment.tensors[0] ?? zero(1))],0);
|
|
34
|
+
}
|
|
35
|
+
} finally { disposeResources(held); }
|
|
36
|
+
return { commit: () => { const old = this.#hidden; this.#hidden=hidden; hidden=old; this.#processed=processed; },
|
|
37
|
+
dispose: () => { hidden?.dispose(); hidden=null; } };
|
|
38
|
+
}
|
|
39
|
+
filterRows(keep: readonly number[]): void {
|
|
40
|
+
if (!keep.length) { this.dispose(); return; }
|
|
41
|
+
using indices = ops.fromInt32([...keep],[keep.length]);
|
|
42
|
+
const next = this.#hidden && keep.length ? ops.takeAxis(this.#hidden,indices,0) : null;
|
|
43
|
+
this.#hidden?.dispose(); this.#hidden=next; this.#processed=keep.map(row=>this.#processed[row]!);
|
|
44
|
+
}
|
|
45
|
+
prefill(tokens: MlxArray, context?: MlxArray): void {
|
|
46
|
+
const [B,N] = tokens.shape as [number,number];
|
|
47
|
+
using tail = context!.slice([0,N-1,0],[B,N,context!.shape[2]!]);
|
|
48
|
+
const hidden = materializeCopy(tail);
|
|
49
|
+
this.#hidden?.dispose(); this.#hidden=hidden; this.#processed=this.#processed.map(count=>count+N);
|
|
50
|
+
}
|
|
51
|
+
materialize(): void { this.#hidden?.eval(); }
|
|
52
|
+
capture(row: number): DraftRowCheckpoint {
|
|
53
|
+
const processedTokens=this.#processed[row]!;
|
|
54
|
+
const hidden=this.#hidden!.slice([row,0,0],[row+1,1,this.target.hiddenSize]);
|
|
55
|
+
return { processedTokens, attachment: { schema:"gemma-assistant-v1",metadata:{processedTokens},tensors:[hidden] } };
|
|
56
|
+
}
|
|
57
|
+
draft(pending: readonly number[], depth: number, _steps: readonly number[]): number[][] {
|
|
58
|
+
if (!depth) return pending.map(()=>[]);
|
|
59
|
+
const B=pending.length, owned: MlxArray[]=[];
|
|
60
|
+
const donors=this.target.readDonors();
|
|
61
|
+
let ids=ops.fromInt32([...pending],[B,1]), hidden=this.#hidden!;
|
|
62
|
+
const tokens: MlxArray[]=[];
|
|
63
|
+
try {
|
|
64
|
+
for(let step=0;step<depth;step++) {
|
|
65
|
+
using emb=this.target.embed(ids);
|
|
66
|
+
const positions=donors.positions.map(position=>position+step);
|
|
67
|
+
const common=positions.every(position=>position===positions[0]);
|
|
68
|
+
using offsets=common ? null : ops.fromInt32(positions,[B]);
|
|
69
|
+
const next=this.drafter.forwardRows(emb,hidden,donors,offsets ?? positions[0]!);
|
|
70
|
+
owned.push(next.nextHidden); tokens.push(next.tokens); hidden=next.nextHidden;
|
|
71
|
+
ids.dispose(); ids=ops.reshape(next.tokens,[B,1]);
|
|
72
|
+
}
|
|
73
|
+
using packed=ops.concatAxis(tokens,0);
|
|
74
|
+
const values=packed.toIntTokens();
|
|
75
|
+
return pending.map((_,row)=>Array.from({length:depth},(_,step)=>values[step*B+row]!));
|
|
76
|
+
} finally { ids.dispose(); donors.dispose(); disposeResources([...owned,...tokens]); }
|
|
77
|
+
}
|
|
78
|
+
commit(accepted: readonly number[], context: MlxArray): void {
|
|
79
|
+
using indices=ops.fromInt32([...accepted],[accepted.length,1,1]);
|
|
80
|
+
const hidden=ops.takeAlongAxis(context,indices,1);
|
|
81
|
+
this.#hidden?.dispose(); this.#hidden=hidden;
|
|
82
|
+
this.#processed=this.#processed.map((count,row)=>count+accepted[row]!+1);
|
|
83
|
+
}
|
|
84
|
+
dispose(): void { this.#hidden?.dispose(); this.#hidden=null; this.#processed=[]; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function assistantGroups(drafter: GemmaAssistantDrafter, namespace: string): GroupedDraftProvider {
|
|
88
|
+
return {
|
|
89
|
+
openPrefill: ({target,checkpoints}) => new AssistantRows(target.assistantRows!,drafter,namespace,checkpoints),
|
|
90
|
+
open: ({target,checkpoints}) => new AssistantRows(target.assistantRows!,drafter,namespace,checkpoints),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as ops from "../mlx/ops";
|
|
2
|
+
import { artifactIdentity } from "../model/artifact-identity";
|
|
3
|
+
import { assistantGroups } from "./assistant-rows";
|
|
1
4
|
// AssistantSource — the optiq KV-borrowing Gemma "-assistant" drafter
|
|
2
5
|
// (src/spec/drafter.ts) behind the serve-time DraftSource seam. L2 oracle:
|
|
3
6
|
// optiq spec_generate. This is the SAME drafter the standalone specGenerate
|
|
@@ -22,7 +25,7 @@ import { readdirSync, statSync } from "node:fs";
|
|
|
22
25
|
import { join } from "node:path";
|
|
23
26
|
import type { MlxArray } from "../mlx/array";
|
|
24
27
|
import { GemmaAssistantDrafter } from "./drafter";
|
|
25
|
-
import type { DraftProvider, DraftSource, TargetView,
|
|
28
|
+
import type { DraftProvider, DraftSource, TargetView, AssistantRowsTarget } from "./source";
|
|
26
29
|
|
|
27
30
|
/** Sum of the drafter's on-disk safetensors — admission accounting only. */
|
|
28
31
|
function safetensorsBytes(dir: string): number {
|
|
@@ -35,20 +38,27 @@ function safetensorsBytes(dir: string): number {
|
|
|
35
38
|
export class AssistantProvider implements DraftProvider {
|
|
36
39
|
readonly id: string;
|
|
37
40
|
readonly weightsBytes: number;
|
|
41
|
+
readonly grouped: import("./source").GroupedDraftProvider;
|
|
38
42
|
|
|
39
43
|
private constructor(
|
|
40
44
|
private readonly drafter: GemmaAssistantDrafter,
|
|
41
45
|
id: string,
|
|
42
|
-
weightsBytes: number,
|
|
46
|
+
weightsBytes: number, namespace: string,
|
|
43
47
|
) {
|
|
44
48
|
this.id = id;
|
|
45
49
|
this.weightsBytes = weightsBytes;
|
|
50
|
+
this.grouped = assistantGroups(drafter,namespace);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
static async load(modelDir: string): Promise<AssistantProvider> {
|
|
49
54
|
const drafter = await GemmaAssistantDrafter.load(modelDir);
|
|
50
55
|
const id = modelDir.split("/").filter(Boolean).at(-1)!;
|
|
51
|
-
|
|
56
|
+
try {
|
|
57
|
+
const identity = await artifactIdentity(await Bun.file(`${modelDir}/config.json`).text(),
|
|
58
|
+
readdirSync(modelDir).filter(file => file.endsWith(".safetensors"))
|
|
59
|
+
.map(name => ({ name, path: join(modelDir, name) })));
|
|
60
|
+
return new AssistantProvider(drafter, id, safetensorsBytes(modelDir), `gemma-assistant-v1:${identity}`);
|
|
61
|
+
} catch (error) { drafter.dispose(); throw error; }
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
|
|
@@ -56,18 +66,18 @@ export class AssistantProvider implements DraftProvider {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
dispose(): void {
|
|
59
|
-
|
|
69
|
+
this.drafter.dispose();
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export class AssistantSource implements DraftSource {
|
|
64
74
|
readonly weightsBytes = 0; // provider-owned weights; per-request adds nothing
|
|
65
|
-
private readonly target:
|
|
75
|
+
private readonly target: AssistantRowsTarget;
|
|
66
76
|
|
|
67
|
-
constructor(private readonly drafter: Pick<GemmaAssistantDrafter, "
|
|
68
|
-
if (!target.
|
|
77
|
+
constructor(private readonly drafter: Pick<GemmaAssistantDrafter, "forwardRows">, target: TargetView) {
|
|
78
|
+
if (!target.assistantRows)
|
|
69
79
|
throw new Error("assistant drafter requires a Gemma4 target with donor views");
|
|
70
|
-
this.target = target.
|
|
80
|
+
this.target = target.assistantRows;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
/** No own cache to prime — the drafter reads the target's live state. */
|
|
@@ -79,8 +89,8 @@ export class AssistantSource implements DraftSource {
|
|
|
79
89
|
* specGenerate's 2a block. anchorHidden is borrowed; never disposed here. */
|
|
80
90
|
draft(feed: number[], n: number, _stepBase: number, anchorHidden?: MlxArray): number[] {
|
|
81
91
|
if (!anchorHidden) throw new Error("assistant drafter needs the target anchor hidden");
|
|
82
|
-
const position = this.target.position();
|
|
83
92
|
const shared = this.target.readDonors();
|
|
93
|
+
const position = shared.positions[0]!;
|
|
84
94
|
const drafts: number[] = [];
|
|
85
95
|
const ownedHiddens: MlxArray[] = [];
|
|
86
96
|
let dTok = feed[feed.length - 1]!; // the pending token (anchorHidden's token)
|
|
@@ -88,22 +98,22 @@ export class AssistantSource implements DraftSource {
|
|
|
88
98
|
let emb: MlxArray | null = null; // hoisted so a mid-loop throw still frees it
|
|
89
99
|
try {
|
|
90
100
|
for (let k = 0; k < n; k++) {
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
using ids = ops.fromInt32([dTok], [1, 1]);
|
|
102
|
+
emb = this.target.embed(ids);
|
|
103
|
+
const step = this.drafter.forwardRows(emb, dHid, shared, position + k);
|
|
104
|
+
ownedHiddens.push(step.nextHidden);
|
|
105
|
+
let token: number;
|
|
106
|
+
try { token = ops.itemUint32(step.tokens); } finally { step.tokens.dispose(); }
|
|
93
107
|
emb.dispose();
|
|
94
108
|
emb = null;
|
|
95
|
-
drafts.push(
|
|
96
|
-
|
|
97
|
-
dTok = step.token;
|
|
109
|
+
drafts.push(token);
|
|
110
|
+
dTok = token;
|
|
98
111
|
dHid = step.nextHidden;
|
|
99
112
|
}
|
|
100
113
|
} finally {
|
|
101
114
|
emb?.dispose();
|
|
102
115
|
for (const a of ownedHiddens) a.dispose();
|
|
103
|
-
|
|
104
|
-
k.dispose();
|
|
105
|
-
v.dispose();
|
|
106
|
-
}
|
|
116
|
+
shared.dispose();
|
|
107
117
|
}
|
|
108
118
|
return drafts;
|
|
109
119
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GenerateOptions } from "../generate";
|
|
2
|
+
|
|
3
|
+
/** Snapshot numerical configuration when the request is bound. Caller-owned
|
|
4
|
+
* arrays must not change the graph or persistence identity after grouping. */
|
|
5
|
+
export function captureSpeculativeOptions(options: GenerateOptions): GenerateOptions {
|
|
6
|
+
return { ...options,
|
|
7
|
+
...(options.kvConfig ? { kvConfig: options.kvConfig.map(({ layerIdx, bits, groupSize }) =>
|
|
8
|
+
({ layerIdx, bits, groupSize })).sort((a, b) => a.layerIdx - b.layerIdx) } : {}),
|
|
9
|
+
...(options.turboQuant ? { turboQuant: { ...options.turboQuant } } : {}),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Existing namespaces remain byte-identical when no per-layer scheme is
|
|
14
|
+
* supplied. Execution and persistence consume the same captured settings. */
|
|
15
|
+
export function speculativePrefixNamespace(method: string | undefined, adapters: string | string[],
|
|
16
|
+
options: GenerateOptions): string {
|
|
17
|
+
return JSON.stringify({ method, adapters, kvBits: options.kvBits ?? null,
|
|
18
|
+
kvGroupSize: options.kvGroupSize ?? 64, quantizedKvStart: options.quantizedKvStart ?? null,
|
|
19
|
+
turboQuant: options.turboQuant ?? null,
|
|
20
|
+
...(options.kvConfig?.length ? { kvConfig: options.kvConfig } : {}),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { projectedDraftGroups } from "./projected-draft-rows";
|
|
2
|
+
import type { GroupedDraftProvider, TargetView } from "./source";
|
|
3
|
+
import type { DeepspecDrafter } from "./dspark/deepspec-module";
|
|
4
|
+
|
|
5
|
+
/** Existing pairing requirements are checked once when the provider opens. */
|
|
6
|
+
export function bindDeepspecTarget(target: TargetView, drafter: Pick<DeepspecDrafter, "cfg">): void {
|
|
7
|
+
if (!target.gemmaTaps) throw new Error("DeepSpec drafter requires a Gemma4 target");
|
|
8
|
+
const layers = drafter.cfg.num_target_layers;
|
|
9
|
+
if (target.gemmaTaps.layerCount !== layers) throw new Error(
|
|
10
|
+
`DeepSpec drafter was trained for a ${layers}-layer target; ` +
|
|
11
|
+
`this model has ${target.gemmaTaps.layerCount} layers — wrong (target, drafter) pairing`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function deepspecGroups(drafter: DeepspecDrafter, namespace: string): GroupedDraftProvider {
|
|
15
|
+
return projectedDraftGroups(target => {
|
|
16
|
+
bindDeepspecTarget(target, drafter);
|
|
17
|
+
return {
|
|
18
|
+
namespace, schema: "deepspec-context-v1",
|
|
19
|
+
layers: drafter.cfg.num_hidden_layers, tapLayers: drafter.tapLayers,
|
|
20
|
+
project(hidden, positions) {
|
|
21
|
+
using projected = drafter.projectContext(hidden);
|
|
22
|
+
return drafter.projectContextKVRows(projected, positions);
|
|
23
|
+
},
|
|
24
|
+
draft(context, pending, positions, depth) {
|
|
25
|
+
return drafter.draftRows(context, pending, positions).tokens.map(row => row.slice(0, depth));
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|