mlx-bun 0.3.0 → 0.5.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.
Files changed (231) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +58 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  5. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  6. package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
  7. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  8. package/docs/design/batching.md +1535 -50
  9. package/docs/design/decode-speed-program.md +394 -1171
  10. package/docs/design/generic-model-support.md +195 -3
  11. package/docs/design/kv-cache.md +538 -225
  12. package/docs/design/speculative-decoding.md +552 -50
  13. package/docs/design/turboquant.md +140 -17
  14. package/docs/design/unified-engine-frontier-plan.md +259 -150
  15. package/docs/planning/PRODUCT_ROADMAP.md +21 -6
  16. package/docs/planning/public-readiness-handoff.md +244 -0
  17. package/docs/planning/public-readiness.md +149 -0
  18. package/docs/reference/benchmarks.md +3537 -1112
  19. package/docs/reference/cli.md +102 -2
  20. package/docs/reference/distribution.md +45 -13
  21. package/docs/reference/environment.md +159 -13
  22. package/docs/reference/library-api.md +18 -4
  23. package/docs/reference/memory.md +1 -1
  24. package/docs/reference/models.md +28 -7
  25. package/docs/reference/server-api.md +257 -33
  26. package/docs/reference/server-config.md +372 -151
  27. package/docs/reference/troubleshooting.md +13 -21
  28. package/package.json +2 -2
  29. package/src/audio/audiotoolbox.ts +112 -0
  30. package/src/audio/conformer.ts +8 -0
  31. package/src/audio/mic-capture.ts +98 -0
  32. package/src/audio/silero-vad.ts +379 -0
  33. package/src/audio/whisper-decode.ts +774 -0
  34. package/src/audio/whisper-fast.ts +732 -0
  35. package/src/audio/whisper-format.ts +73 -0
  36. package/src/audio/whisper-mel.ts +185 -0
  37. package/src/audio/whisper-timing.ts +274 -0
  38. package/src/audio/whisper-tokenizer.ts +239 -0
  39. package/src/audio/whisper-transcribe.ts +410 -0
  40. package/src/backends/mlx/assistant-target.ts +46 -0
  41. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  42. package/src/backends/mlx/autoregressive.ts +22 -1
  43. package/src/backends/mlx/batch-group.ts +464 -236
  44. package/src/backends/mlx/cache-attachment.ts +67 -0
  45. package/src/backends/mlx/cache-layout.ts +31 -0
  46. package/src/backends/mlx/checkpoint-state.ts +53 -0
  47. package/src/backends/mlx/continuation-persistence.ts +107 -0
  48. package/src/backends/mlx/continuation-request.ts +43 -0
  49. package/src/backends/mlx/continuation.ts +77 -0
  50. package/src/backends/mlx/draft-target.ts +7 -22
  51. package/src/backends/mlx/encoder-cache.ts +25 -0
  52. package/src/backends/mlx/fill-append.ts +21 -8
  53. package/src/backends/mlx/fill-group.ts +497 -0
  54. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  55. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  56. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  57. package/src/backends/mlx/gateway-binding.ts +122 -19
  58. package/src/backends/mlx/kv-codec.ts +9 -0
  59. package/src/backends/mlx/kv-maintenance.ts +161 -0
  60. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  61. package/src/backends/mlx/mixed-iteration.ts +62 -0
  62. package/src/backends/mlx/model-prompt.ts +46 -32
  63. package/src/backends/mlx/model-serving.ts +5 -4
  64. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  65. package/src/backends/mlx/prefill-cohort.ts +100 -0
  66. package/src/backends/mlx/prefill-policy.ts +26 -0
  67. package/src/backends/mlx/prefill-rows.ts +244 -0
  68. package/src/backends/mlx/prefill.ts +1 -1
  69. package/src/backends/mlx/projected-context-rows.ts +72 -0
  70. package/src/backends/mlx/prompt-input.ts +29 -0
  71. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  72. package/src/backends/mlx/request-state-policy.ts +54 -0
  73. package/src/backends/mlx/rollback.ts +45 -4
  74. package/src/backends/mlx/row-sampling.ts +49 -0
  75. package/src/backends/mlx/serial-executor.ts +51 -73
  76. package/src/backends/mlx/speculative-group.ts +438 -0
  77. package/src/backends/mlx/speculative-round.ts +161 -0
  78. package/src/backends/mlx/speculative.ts +24 -7
  79. package/src/backends/mlx/state-rows.ts +104 -0
  80. package/src/backends/mlx/state-views.ts +21 -1
  81. package/src/backends/mlx/target-layout-capability.ts +35 -0
  82. package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
  83. package/src/cli.ts +333 -36
  84. package/src/contracts/execution.ts +21 -0
  85. package/src/contracts/object-cache.ts +8 -0
  86. package/src/contracts/pi-provider.ts +1 -1
  87. package/src/contracts/pi.ts +3 -1
  88. package/src/contracts/prefix-cache.ts +33 -0
  89. package/src/contracts/resources.ts +7 -0
  90. package/src/contracts/sampling.ts +23 -0
  91. package/src/contracts/scheduling.ts +27 -0
  92. package/src/engine/execution-plan.ts +38 -25
  93. package/src/engine/execution-tasks.ts +44 -0
  94. package/src/engine/namespaced-cache.ts +13 -0
  95. package/src/engine/resources.ts +12 -1
  96. package/src/engine/scheduler.ts +32 -7
  97. package/src/fill/fill-session.ts +14 -3
  98. package/src/fit.ts +29 -15
  99. package/src/generate.ts +105 -195
  100. package/src/grammar.ts +13 -0
  101. package/src/inference/draft-acceptance.ts +41 -0
  102. package/src/inference/draft-output.ts +49 -0
  103. package/src/inference/prefill.ts +6 -0
  104. package/src/inference/rollback.ts +2 -2
  105. package/src/kv-scheme.ts +92 -12
  106. package/src/kv-store.ts +183 -93
  107. package/src/lab/README.md +5 -1
  108. package/src/lab/paged-kv/paged-attention.ts +117 -0
  109. package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
  110. package/src/lab/paged-kv/paged-kv.ts +126 -46
  111. package/src/md-modules.d.ts +5 -0
  112. package/src/mlx/array.ts +39 -1
  113. package/src/mlx/ffi.ts +19 -0
  114. package/src/mlx/normalized-argmax.ts +116 -0
  115. package/src/mlx/ops.ts +97 -10
  116. package/src/model/artifact-identity.ts +73 -0
  117. package/src/model/batched-kv.ts +235 -0
  118. package/src/model/batched-mask.ts +12 -4
  119. package/src/model/batched-quant.ts +8 -2
  120. package/src/model/batched-quantized-kv.ts +144 -0
  121. package/src/model/batched-rotating-quant.ts +76 -15
  122. package/src/model/batched-rotating-state.ts +76 -0
  123. package/src/model/batched-rotating.ts +64 -22
  124. package/src/model/batched-row-storage.ts +52 -0
  125. package/src/model/batched-ssm.ts +52 -0
  126. package/src/model/batched-turboquant-kv.ts +94 -0
  127. package/src/model/compiled-decode.ts +7 -1
  128. package/src/model/decoded-kv-donor.ts +12 -0
  129. package/src/model/delayed-quantized-kv.ts +72 -0
  130. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  131. package/src/model/delayed-turboquant-kv.ts +57 -0
  132. package/src/model/factory.ts +26 -0
  133. package/src/model/fingerprint.ts +12 -0
  134. package/src/model/full-prefill-padding.ts +56 -0
  135. package/src/model/full-prefill-row.ts +105 -0
  136. package/src/model/gemma4-base.ts +183 -82
  137. package/src/model/gemma4.ts +116 -28
  138. package/src/model/generated/gemma4-12b.ts +4 -1
  139. package/src/model/generated/gemma4-26b.ts +4 -1
  140. package/src/model/generated/gemma4-e4b.ts +4 -1
  141. package/src/model/glm52-cache.ts +80 -4
  142. package/src/model/glm52-mla.ts +12 -6
  143. package/src/model/glm52.ts +19 -40
  144. package/src/model/kv-attention-view.ts +70 -0
  145. package/src/model/minicpm5.ts +11 -7
  146. package/src/model/profile.ts +29 -4
  147. package/src/model/quantized-append-attention.ts +31 -0
  148. package/src/model/qwen3-delta-state.ts +74 -0
  149. package/src/model/qwen3-delta.ts +59 -36
  150. package/src/model/qwen3-mrope.ts +7 -9
  151. package/src/model/qwen3_5.ts +177 -75
  152. package/src/model/rotating-kv-donor.ts +35 -0
  153. package/src/model/rotating-kv-layout.ts +262 -0
  154. package/src/model/rotating-row-transaction.ts +35 -0
  155. package/src/model/speculative-rotating-kv.ts +101 -0
  156. package/src/model/ssm-prefill-padding.ts +39 -0
  157. package/src/model/support.ts +8 -0
  158. package/src/model/token-groups.ts +66 -0
  159. package/src/model/trellis-linear.ts +4 -4
  160. package/src/model/turboquant-codec.ts +89 -0
  161. package/src/model/universal/dense.ts +14 -13
  162. package/src/model/whisper.ts +435 -0
  163. package/src/native/mic_capture.swift +92 -0
  164. package/src/native-pack.ts +6 -5
  165. package/src/pi-web.ts +9 -1
  166. package/src/preflight.ts +31 -15
  167. package/src/prompt-cache.ts +306 -86
  168. package/src/runtime-config.ts +1 -5
  169. package/src/safetensors.ts +9 -2
  170. package/src/sampler.ts +37 -25
  171. package/src/serve/audio-routes.ts +323 -0
  172. package/src/serve/chat-request.ts +2 -0
  173. package/src/serve/chat-stage.ts +19 -14
  174. package/src/serve/completion-executor.ts +9 -1
  175. package/src/serve/discovery-routes.ts +17 -1
  176. package/src/serve/generated-token-history.ts +57 -0
  177. package/src/serve/generation-gateway.ts +88 -38
  178. package/src/serve/model-binding.ts +7 -1
  179. package/src/serve/model-host.ts +3 -4
  180. package/src/serve/preparation.ts +3 -3
  181. package/src/serve/prompt-response-trace.ts +14 -0
  182. package/src/serve/request-plan.ts +6 -3
  183. package/src/serve/request-prep.ts +40 -25
  184. package/src/serve/text-completion-stage.ts +2 -2
  185. package/src/serve/transcription-server.ts +64 -0
  186. package/src/serve/transcription-service.ts +502 -0
  187. package/src/server.ts +185 -261
  188. package/src/spec/assistant-rows.ts +93 -0
  189. package/src/spec/assistant-source.ts +28 -18
  190. package/src/spec/cache-identity.ts +22 -0
  191. package/src/spec/deepspec-rows.ts +29 -0
  192. package/src/spec/deepspec-source.ts +12 -10
  193. package/src/spec/dflash-source.ts +28 -4
  194. package/src/spec/drafter.ts +153 -142
  195. package/src/spec/dspark/deepspec-module.ts +95 -120
  196. package/src/spec/dspark/module-dflash.ts +126 -170
  197. package/src/spec/glm52-mtp-graph.ts +29 -0
  198. package/src/spec/glm52-mtp-rows.ts +132 -0
  199. package/src/spec/glm52-mtp-source.ts +54 -136
  200. package/src/spec/glm52-mtp-state.ts +33 -0
  201. package/src/spec/ngram-source.ts +115 -21
  202. package/src/spec/projected-draft-rows.ts +73 -0
  203. package/src/spec/qwen-mtp-module.ts +171 -0
  204. package/src/spec/qwen-mtp-rows.ts +230 -0
  205. package/src/spec/qwen-mtp-source.ts +84 -226
  206. package/src/spec/qwen-mtp-state.ts +30 -0
  207. package/src/spec/serve-loop.ts +73 -69
  208. package/src/spec/source.ts +93 -15
  209. package/src/spec/two-model-rows.ts +62 -0
  210. package/src/spec/two-model.ts +45 -103
  211. package/src/ssd-cache.ts +107 -36
  212. package/src/ssd-durability.ts +29 -27
  213. package/src/storage/host-buffer.ts +24 -0
  214. package/src/storage/kv-writer.ts +83 -0
  215. package/src/storage/kv-writer.worker.js +264 -0
  216. package/src/storage/retention-policy.ts +37 -0
  217. package/src/tiered-prompt-cache.ts +45 -0
  218. package/src/vision/embedder.ts +8 -0
  219. package/src/vision/prompt.ts +101 -65
  220. package/src/vision/qwen3vl-prompt.ts +101 -64
  221. package/src/vision/qwen3vl-tower.ts +11 -0
  222. package/src/vision/siglip.ts +8 -0
  223. package/src/web/app.html +12 -0
  224. package/src/web/app.js +217 -57
  225. package/src/web/src/chat.ts +4 -1
  226. package/src/web/src/composer.ts +3 -0
  227. package/src/web/src/voice.ts +141 -0
  228. package/src/web/sw.js +1 -1
  229. package/src/backends/mlx/memory-guard.ts +0 -40
  230. package/src/spec/prefix-state.ts +0 -45
  231. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -0,0 +1,67 @@
1
+ import type { Cache } from "../../model/gemma4-base";
2
+ import type { MlxArray } from "../../mlx/array";
3
+ import * as ops from "../../mlx/ops";
4
+ import { legacyCacheCodecs, type CacheCodecProvider, type CacheHeaderEntry,
5
+ type SnapshotContext } from "../../kv-store";
6
+ import { disposeResources } from "../../engine/resources";
7
+ import type { CheckpointAttachment } from "./checkpoint-state";
8
+ type TensorSlot = SnapshotContext["slots"][number];
9
+
10
+ /** Reuse the backend cache codecs for method-owned companion state. Tensor
11
+ * slots address attachment indices here; the persistence tier supplies the
12
+ * outer file offsets and checksums. No host serialization of tensors occurs. */
13
+ export function captureCacheAttachment(schema: string, caches: readonly Cache[],
14
+ metadata: CheckpointAttachment["metadata"], codecs: CacheCodecProvider = legacyCacheCodecs): CheckpointAttachment {
15
+ const tensors: MlxArray[] = [], headers: Array<CacheHeaderEntry | null> = [];
16
+ try {
17
+ for (const cache of caches) {
18
+ if (!cache.offset) { headers.push(null); continue; }
19
+ const slots: TensorSlot[] = [];
20
+ const liveSlice = (array: MlxArray, length: number, axis = 2) => {
21
+ const end = [...array.shape]; end[axis] = length;
22
+ return array.slice(array.shape.map(() => 0), end);
23
+ };
24
+ const push = (array: MlxArray, owned: boolean) => {
25
+ const value = owned ? array : ops.copyOf(array);
26
+ const off = tensors.length; tensors.push(value);
27
+ slots.push({ off, bytes: value.nbytes, shape: [...value.shape], dtype: value.dtype, hash: "" });
28
+ };
29
+ const context: SnapshotContext = { slots, push, liveSlice,
30
+ liveMlaSlice: (array, length) => liveSlice(array, length, 1),
31
+ pushTriple(triple, length) {
32
+ for (const array of [triple.packed, triple.scales, triple.biases])
33
+ push(length === null ? array : liveSlice(array, length), length !== null);
34
+ },
35
+ };
36
+ const entry = codecs.forCache(cache).snapshot(cache, context);
37
+ if (cache.minimumReusableOffset !== undefined) entry.minimumReusableOffset = cache.minimumReusableOffset;
38
+ headers.push(entry);
39
+ }
40
+ return { schema, metadata: { ...metadata, cacheCodecs: codecs.id, cacheHeaders: JSON.stringify(headers) }, tensors };
41
+ } catch (error) { disposeResources(tensors); throw error; }
42
+ }
43
+
44
+ /** Returned caches own immutable aliases. The caller keeps an external SSD
45
+ * lease until its normal state materialization boundary has completed. */
46
+ export function restoreCacheAttachment(attachment: CheckpointAttachment, makeCache: () => Cache[],
47
+ codecs: CacheCodecProvider = legacyCacheCodecs): Cache[] {
48
+ if (attachment.metadata.cacheCodecs !== codecs.id) throw new Error("companion cache codec mismatch");
49
+ const headers = JSON.parse(attachment.metadata.cacheHeaders as string) as Array<CacheHeaderEntry | null>;
50
+ const caches = makeCache(), pending: MlxArray[] = [];
51
+ const arr = (slot: TensorSlot) => {
52
+ const value = ops.copyOf(attachment.tensors[slot.off]!); pending.push(value); return value;
53
+ };
54
+ try {
55
+ for (const [index, entry] of headers.entries()) {
56
+ if (!entry) continue;
57
+ const cache = codecs.forHeader(entry).load(entry, { path: attachment.schema, arr, grownArr: arr,
58
+ triple: (slots, at) => ({ packed: arr(slots[at]!), scales: arr(slots[at + 1]!), biases: arr(slots[at + 2]!) }),
59
+ });
60
+ pending.length = 0;
61
+ cache.minimumReusableOffset = entry.minimumReusableOffset ?? 0;
62
+ const previous = caches[index]; caches[index] = cache;
63
+ previous?.dispose();
64
+ }
65
+ return caches;
66
+ } catch (error) { disposeResources([...pending, ...caches]); throw error; }
67
+ }
@@ -0,0 +1,31 @@
1
+ import { TurboQuantKVCache, RotatingKVCache, RotatingQuantizedKVCache, isBatchableCache, type BatchableCache, type Cache } from "../../model/gemma4-base";
2
+ import { DelayedRotatingQuantizedKVCache } from "../../model/delayed-rotating-quantized-kv";
3
+ import { RotatingAffineLayout } from "../../model/rotating-kv-layout";
4
+ import { BatchedTurboQuantKVCache } from "../../model/batched-turboquant-kv";
5
+ import { targetRowLayoutFactory, type TargetRowLayout } from "./target-layout-capability";
6
+ import { PagedKVCache } from "../../lab/paged-kv/paged-kv";
7
+ import { PagedKvRows } from "../../lab/paged-kv/paged-kv-rows";
8
+
9
+ /** Bind encoded state to its row layout. Execution owners only move rows. */
10
+ export function ownedCacheLayout(cache: Cache): BatchableCache | undefined {
11
+ if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize, cache.direct, cache.quantization);
12
+ if (isBatchableCache(cache)) return cache.makeEmptyBatch();
13
+ if (cache instanceof TurboQuantKVCache) return new BatchedTurboQuantKVCache(cache.kBits, cache.vBits, cache.fusedDecode);
14
+ return undefined;
15
+ }
16
+
17
+ /** Prefill reuses decode's layouts; shape selection belongs to storage. */
18
+ export function prefillCacheLayout(cache: Cache): BatchableCache {
19
+ if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize, cache.direct, cache.quantization);
20
+ if (isBatchableCache(cache)) return cache.makeEmptyBatch();
21
+ if (cache instanceof RotatingKVCache) return new DelayedRotatingQuantizedKVCache(cache.maxSize, 64, 4, Infinity, () => {});
22
+ if (cache instanceof RotatingQuantizedKVCache) return new RotatingAffineLayout(cache.maxSize, cache.groupSize, cache.bits);
23
+ return targetCacheLayout(cache);
24
+ }
25
+
26
+ /** Target transaction layouts retain each source cache's numerical codec. */
27
+ export function targetCacheLayout(cache: Cache): TargetRowLayout {
28
+ const make = targetRowLayoutFactory(cache);
29
+ if (!make) throw new Error(`No target row layout for ${cache.signature()}`);
30
+ return make();
31
+ }
@@ -0,0 +1,53 @@
1
+ import type { Cache } from "../../model/gemma4-base";
2
+ import * as ops from "../../mlx/ops";
3
+ import { leaseCacheStates } from "./state-views";
4
+ import { withResource } from "../../engine/resources";
5
+ import type { MlxArray } from "../../mlx/array";
6
+ import { disposeResources, cleanupFailure } from "../../engine/resources";
7
+
8
+ /** Generation-method state accompanying the target cache. The method owns
9
+ * its schema and alignment; storage owns these immutable tensor snapshots.
10
+ * Attachments require an exact prefix boundary and are never trimmed. */
11
+ export interface CheckpointAttachment {
12
+ schema: string;
13
+ metadata: Record<string, string | number | boolean>;
14
+ tensors: MlxArray[];
15
+ }
16
+
17
+ export function attachmentBytes(attachments: readonly CheckpointAttachment[] = []): number {
18
+ return attachments.reduce((sum, entry) =>
19
+ sum + entry.tensors.reduce((bytes, tensor) => bytes + tensor.nbytes, 0), 0);
20
+ }
21
+
22
+ export function disposeAttachments(attachments: readonly CheckpointAttachment[] = []): void {
23
+ disposeResources(attachments.flatMap((entry) => entry.tensors));
24
+ }
25
+
26
+ export function cloneAttachments(attachments: readonly CheckpointAttachment[] = []): CheckpointAttachment[] {
27
+ const held: MlxArray[] = [];
28
+ try {
29
+ return attachments.map((entry) => ({
30
+ schema: entry.schema, metadata: { ...entry.metadata },
31
+ tensors: entry.tensors.map((tensor) => {
32
+ const copy = tensor.slice(tensor.shape.map(() => 0), [...tensor.shape]);
33
+ held.push(copy);
34
+ return copy;
35
+ }),
36
+ }));
37
+ } catch (error) { return cleanupFailure(error, () => disposeResources(held)); }
38
+ }
39
+
40
+ /** Shared cache port for MLX execution methods and their companion state. */
41
+ export type MlxPrefixCache = import("../../contracts/prefix-cache").PrefixCache<
42
+ import("../../model/gemma4").Cache[], CheckpointAttachment[]
43
+ >;
44
+
45
+ /** Resolve snapshot copies before storage retains them. A deferred compact
46
+ * row still owns its full parent batch; persistence may wait until idle.
47
+ * This only settles GPU state and performs no serialization or disk I/O. */
48
+ export function materializeCheckpoint(caches: Cache[], attachments: readonly CheckpointAttachment[] = []): void {
49
+ withResource(leaseCacheStates(caches), state => {
50
+ const tensors = [...state, ...attachments.flatMap(attachment => attachment.tensors)];
51
+ if (tensors.length) ops.evalAll(tensors);
52
+ });
53
+ }
@@ -0,0 +1,107 @@
1
+ import { SpillQueue, type SpillItem } from "../../kv-store";
2
+ import { cacheBytes } from "../../prompt-cache";
3
+ import { disposeResources } from "../../engine/resources";
4
+ import type { ContinuationStore, OrdinaryContinuationState } from "./continuation";
5
+
6
+ type Metadata = Parameters<ContinuationStore["storeGenerationCheckpoint"]>[2];
7
+ interface Attempt { key: string; completed: boolean; pending: number; failed: boolean; version: number; retired: boolean }
8
+ interface Job { attempt: Attempt; metadata?: Metadata; cleanup?: number }
9
+
10
+ /** One coordinator per checkpoint store. Queue owns snapshots through disk
11
+ * completion; per-key attempts order cleanup before subsequent writes. */
12
+ export class ContinuationPersistence {
13
+ readonly #attempts = new Map<string, Attempt>();
14
+ readonly #jobs = new WeakMap<SpillItem["caches"], Job>();
15
+ readonly #queue: SpillQueue;
16
+ readonly #cleanupFailures = new Map<string, Set<number>>();
17
+ readonly #cleanupPending = new Map<string, Set<number>>();
18
+ readonly #cleanupSucceeded = new Map<string, number>();
19
+ #nextBarrier = 0;
20
+ constructor(readonly store: ContinuationStore, options: {
21
+ maxBytes: number;
22
+ /** Optional owner-thread preparation hook; disk writes use the CPU worker. */
23
+ runStep?: <T>(step: () => T) => Promise<T>;
24
+ }) {
25
+ const runStep = options.runStep ?? (async <T>(step: () => T): Promise<T> => step());
26
+ this.#queue = new SpillQueue(options.maxBytes, cacheBytes, async item => {
27
+ const job = this.#jobs.get(item.caches)!;
28
+ if (job.cleanup) {
29
+ await runStep(() => store.removeGenerationCheckpoints(job.attempt.key));
30
+ return true;
31
+ }
32
+ const current = () => this.#attempts.get(job.attempt.key) === job.attempt && !job.attempt.completed;
33
+ if (!current()) return true;
34
+ const stored = await store.storeGenerationCheckpoint(item.tokens, item.caches, job.metadata!, options.runStep);
35
+ // No newer queued write can run until this callback returns. An old
36
+ // in-flight rename can therefore be removed without deleting new work.
37
+ if (!current()) await runStep(() => store.removeGenerationCheckpoints(job.attempt.key));
38
+ return stored;
39
+ }, disposeResources, item => !this.#jobs.get(item.caches)?.cleanup);
40
+ }
41
+ /** A completed response is ineligible immediately, before queued unlink. */
42
+ canRestore(key: string): boolean { return !this.#cleanupPending.has(key) && !this.#cleanupFailures.has(key); }
43
+ begin(key: string): Attempt {
44
+ const attempt = { key, completed: false, pending: 0, failed: false, version: 0, retired: false };
45
+ this.#attempts.set(key, attempt);
46
+ this.#queue.cancelWhere(item => !this.#jobs.get(item.caches)?.cleanup && this.#jobs.get(item.caches)?.attempt.key === key);
47
+ return attempt;
48
+ }
49
+ /** Takes ownership even when a completed/superseded attempt declines it. */
50
+ enqueue(attempt: Attempt, state: Omit<OrdinaryContinuationState, "seed">, metadata: Metadata): void {
51
+ if (attempt.completed || this.#attempts.get(attempt.key) !== attempt) {
52
+ disposeResources(state.caches); return;
53
+ }
54
+ this.#jobs.set(state.caches, { attempt, metadata: { ...metadata } });
55
+ attempt.pending++;
56
+ const version = ++attempt.version;
57
+ let completion: Promise<boolean>;
58
+ try { completion = this.#queue.enqueue({ caches: state.caches, tokens: [...state.cacheTokens], ns: attempt.key }); }
59
+ catch (error) { attempt.pending--; disposeResources(state.caches); throw error; }
60
+ void completion.then(stored => {
61
+ attempt.pending--;
62
+ if (version === attempt.version && !attempt.completed) attempt.failed = !stored;
63
+ this.#releaseSettled(attempt);
64
+ });
65
+ }
66
+ complete(attempt: Attempt): void {
67
+ attempt.completed = true;
68
+ this.#queue.cancelWhere(item => !this.#jobs.get(item.caches)?.cleanup && this.#jobs.get(item.caches)?.attempt === attempt);
69
+ if (this.#attempts.get(attempt.key) === attempt) {
70
+ this.#attempts.delete(attempt.key);
71
+ const barrier = ++this.#nextBarrier;
72
+ const pending = this.#cleanupPending.get(attempt.key) ?? new Set<number>();
73
+ pending.add(barrier); this.#cleanupPending.set(attempt.key, pending);
74
+ const caches: SpillItem["caches"] = [];
75
+ this.#jobs.set(caches, { attempt, cleanup: barrier });
76
+ void this.#queue.enqueue({ caches, tokens: [], ns: attempt.key }).then(stored => {
77
+ const key = attempt.key;
78
+ const outstanding = this.#cleanupPending.get(key)!;
79
+ outstanding.delete(barrier);
80
+ if (!outstanding.size) this.#cleanupPending.delete(key);
81
+ const failures = this.#cleanupFailures.get(key) ?? new Set<number>();
82
+ if (stored) {
83
+ this.#cleanupSucceeded.set(key, Math.max(barrier, this.#cleanupSucceeded.get(key) ?? 0));
84
+ // A successful unlink covers older failures, never a newer barrier.
85
+ for (const failed of failures) if (failed <= barrier) failures.delete(failed);
86
+ } else if (barrier > (this.#cleanupSucceeded.get(key) ?? 0)) failures.add(barrier);
87
+ if (failures.size) this.#cleanupFailures.set(key, failures);
88
+ else this.#cleanupFailures.delete(key);
89
+ if (!outstanding.size) this.#cleanupSucceeded.delete(key);
90
+ });
91
+ }
92
+ }
93
+ /** Request cancellation/disposal preserves queued and durable state. */
94
+ release(attempt: Attempt): void { attempt.retired = true; this.#releaseSettled(attempt); }
95
+ #releaseSettled(attempt: Attempt): void {
96
+ if (attempt.retired && attempt.pending === 0 && !attempt.failed && this.#attempts.get(attempt.key) === attempt)
97
+ this.#attempts.delete(attempt.key);
98
+ }
99
+ get stats() { return { pendingCount: this.#queue.pendingCount, pendingBytes: this.#queue.pendingBytes,
100
+ dropped: this.#queue.droppedCount, failed: this.#queue.failedCount }; }
101
+ async flush(): Promise<{ durable: boolean; pendingBytes: number; failed: number }> {
102
+ await this.#queue.drain();
103
+ const active = [...this.#attempts.values()].filter(attempt => !attempt.completed);
104
+ return { durable: this.#cleanupFailures.size === 0 && active.every(attempt => attempt.pending === 0 && !attempt.failed),
105
+ pendingBytes: this.#queue.pendingBytes, failed: this.#cleanupFailures.size + active.filter(attempt => attempt.failed).length };
106
+ }
107
+ }
@@ -0,0 +1,43 @@
1
+ import { snapshotGenerationPolicy } from "./request-policy";
2
+ import type { GenerateOptions } from "../../generate";
3
+ import type { ResolvedExecution } from "../../contracts/execution";
4
+ import { makeStepSampler } from "../../sampler";
5
+ import { createRowSampling } from "./row-sampling";
6
+ import { bindContinuationPolicy, type OrdinaryContinuation } from "./continuation";
7
+
8
+ /** Optional policy and sampler recovery for the existing ordinary driver.
9
+ * KV layout and delayed conversion remain owned by cache maintenance and persistence. */
10
+ export function createOrdinaryContinuationRequest(input: Omit<Parameters<typeof bindContinuationPolicy>[0], "namespace"> & {
11
+ interval: number;
12
+ persistence: import("./continuation-persistence").ContinuationPersistence;
13
+ onToken: Parameters<typeof createRowSampling>[1];
14
+ }) {
15
+ const options: GenerateOptions = snapshotGenerationPolicy(input.options);
16
+ if (!Number.isSafeInteger(input.interval) || input.interval < 1) throw new Error("continuation interval must be positive");
17
+ if (options.grammar || options.logprobs || options.topLogprobs || options.fill || options.pagedKv ||
18
+ input.execution.method !== "autoregressive")
19
+ throw new Error("ordinary continuation requires replayable ordinary sampling");
20
+ const make = (seed = options.seed, history: readonly number[] = input.prompt) => createRowSampling(makeStepSampler(
21
+ { ...options, seed }, { tokenRepresentation: "device", grammarWait: "external", historyUpdate: "after-sample",
22
+ initialHistory: [...history] }), input.onToken);
23
+ let sampling = make();
24
+ let policy: ReturnType<typeof bindContinuationPolicy> | undefined;
25
+ const continuation: OrdinaryContinuation = {
26
+ interval: input.interval,
27
+ restore(namespace) { policy = bindContinuationPolicy({ ...input, options, namespace }); return policy.restore(); },
28
+ resumeSampling(state) {
29
+ const next = make(state.seed, [...state.cacheTokens, state.pendingToken]);
30
+ const previous = sampling; sampling = next; previous.dispose();
31
+ },
32
+ captureOwned: state => policy!.captureOwned(state),
33
+ complete: () => policy?.complete(),
34
+ };
35
+ return {
36
+ continuation,
37
+ sample: ((...args) => sampling.sample(...args)) as ReturnType<typeof createRowSampling>["sample"],
38
+ // History-free greedy sampling needs no recovery work in the fast path.
39
+ plainGreedy: sampling.plainGreedy,
40
+ onToken: input.onToken,
41
+ dispose: () => { policy?.release(); sampling.dispose(); },
42
+ };
43
+ }
@@ -0,0 +1,77 @@
1
+ import type { ContinuationPersistence } from "./continuation-persistence";
2
+ import type { Cache } from "../../model/gemma4-base";
3
+ import type { GenerateOptions } from "../../generate";
4
+ import type { ResolvedExecution } from "../../contracts/execution";
5
+ import type { SsdCacheStore } from "../../ssd-cache";
6
+ import { generationCheckpointKey } from "../../serve/checkpoint-identity";
7
+ import { disposeResources } from "../../engine/resources";
8
+
9
+ export interface OrdinaryContinuationState {
10
+ caches: Cache[];
11
+ cacheTokens: number[];
12
+ generatedTokens: number;
13
+ pendingToken: number;
14
+ seed: number;
15
+ }
16
+ /** Request-owned policy. Restore and captureOwned transfer cache ownership.
17
+ * No store or key knowledge enters the driver. */
18
+ export interface OrdinaryContinuation {
19
+ readonly interval: number;
20
+ restore(namespace: string): OrdinaryContinuationState | null;
21
+ resumeSampling(state: Omit<OrdinaryContinuationState, "caches">): void;
22
+ captureOwned(state: Omit<OrdinaryContinuationState, "seed">): void;
23
+ complete(): void;
24
+ }
25
+ export type ContinuationStore = Pick<SsdCacheStore, "findGenerationCheckpoint" | "restore" |
26
+ "storeGenerationCheckpoint" | "removeGenerationCheckpoints">;
27
+
28
+ /** Shared persistence policy for compatibility and grouped ordinary drivers. */
29
+ export function bindContinuationPolicy(input: {
30
+ store: ContinuationStore;
31
+ restore: (entry: NonNullable<ReturnType<ContinuationStore["findGenerationCheckpoint"]>>) => ReturnType<ContinuationStore["restore"]>;
32
+ prompt: number[]; options: GenerateOptions; execution: ResolvedExecution;
33
+ identity: unknown; namespace: string;
34
+ persistence?: ContinuationPersistence;
35
+ }) {
36
+ const { store, options, prompt, namespace } = input;
37
+ const key = generationCheckpointKey(prompt, options, namespace, input.execution, input.identity);
38
+ let seed = options.seed ?? 0;
39
+ const attempt = input.persistence?.begin(key);
40
+ return {
41
+ key,
42
+ restore(): OrdinaryContinuationState | null {
43
+ if (input.persistence && !input.persistence.canRestore(key)) return null;
44
+ const entry = store.findGenerationCheckpoint(prompt, key, namespace);
45
+ const restored = entry ? input.restore(entry) : null;
46
+ if (!restored) return null;
47
+ const metadata = restored.header.generationCheckpoint;
48
+ if (!metadata || metadata.originalPromptTokens !== prompt.length ||
49
+ metadata.generatedTokens !== restored.tokens.length - prompt.length) {
50
+ disposeResources(restored.caches);
51
+ throw new Error("generation checkpoint has inconsistent continuation metadata");
52
+ }
53
+ seed = metadata.seed;
54
+ return { caches: restored.caches, cacheTokens: restored.tokens,
55
+ generatedTokens: metadata.generatedTokens, pendingToken: metadata.pendingToken, seed };
56
+ },
57
+ async capture(state: Omit<OrdinaryContinuationState, "seed">): Promise<void> {
58
+ const stored = await store.storeGenerationCheckpoint(state.cacheTokens, state.caches, {
59
+ key, cacheNs: namespace, originalPromptTokens: prompt.length,
60
+ generatedTokens: state.generatedTokens, pendingToken: state.pendingToken,
61
+ seed, seedWasExplicit: options.seedWasExplicit === true,
62
+ });
63
+ if (stored) console.log(`[generation-checkpoint] saved ${state.generatedTokens} emitted tokens`);
64
+ },
65
+ captureOwned(state: Omit<OrdinaryContinuationState, "seed">): void {
66
+ if (!input.persistence || !attempt) { disposeResources(state.caches); throw new Error("owned continuation requires persistence coordinator"); }
67
+ input.persistence.enqueue(attempt, state, { key, cacheNs: namespace, originalPromptTokens: prompt.length,
68
+ generatedTokens: state.generatedTokens, pendingToken: state.pendingToken,
69
+ seed, seedWasExplicit: options.seedWasExplicit === true });
70
+ },
71
+ release() { if (input.persistence && attempt) input.persistence.release(attempt); },
72
+ complete() {
73
+ if (input.persistence && attempt) input.persistence.complete(attempt);
74
+ else store.removeGenerationCheckpoints(key);
75
+ },
76
+ };
77
+ }
@@ -1,9 +1,10 @@
1
+ import { readAssistantDonors } from "./assistant-target";
1
2
  import type { RuntimeModel } from "../../model/factory";
2
3
  import { Gemma4Model, type Cache } from "../../model/gemma4";
3
4
  import { Qwen35Model } from "../../model/qwen3_5";
4
5
  import type { MlxArray } from "../../mlx/array";
5
6
  import * as ops from "../../mlx/ops";
6
- import type { TargetView, AssistantTarget } from "../../spec/source";
7
+ import type { TargetView } from "../../spec/source";
7
8
 
8
9
  /** Class checks and live cache layout stay at the compatibility boundary.
9
10
  * A replacement graph supplies the same ports without becoming these classes. */
@@ -14,27 +15,11 @@ export function bindLegacyDraftTarget(model: RuntimeModel, caches: Cache[]): Tar
14
15
  if (model.layers[i]!.layerType === "sliding_attention") sliding = i;
15
16
  else full = i;
16
17
  }
17
- const view = (index: number): [MlxArray, MlxArray] => {
18
- const cache = caches[index];
19
- if (!cache || !("temporalView" in cache) || typeof cache.temporalView !== "function")
20
- throw new Error("assistant target has no plain donor view");
21
- return cache.temporalView();
22
- };
23
- const assistant: AssistantTarget | undefined = sliding >= 0 && full >= 0 ? {
24
- position: () => caches[0]!.offset - 1,
25
- embedScaled(token) {
26
- const ids = ops.fromInt32([token], [1, 1]);
27
- let embedded: MlxArray | undefined;
28
- try { embedded = model.embed.encode(ids); return ops.mulScalar(embedded, model.embedScale); }
29
- finally { ids.dispose(); embedded?.dispose(); }
30
- },
31
- readDonors() {
32
- const slidingView = view(sliding);
33
- try { return { sliding: slidingView, full: view(full) }; }
34
- catch (error) { for (const array of slidingView) array.dispose(); throw error; }
35
- },
36
- } : undefined;
37
- return Object.freeze({ identity: model, assistant, gemmaTaps: Object.freeze({
18
+ return Object.freeze({ identity: model, assistantRows: sliding >= 0 && full >= 0 ? {
19
+ hiddenSize: model.config.text.hiddenSize,
20
+ embed(ids: MlxArray) { using embedded = model.embed.encode(ids); return ops.mulScalar(embedded, model.embedScale); },
21
+ readDonors: () => readAssistantDonors(caches[sliding]!, caches[full]!),
22
+ } : undefined, gemmaTaps: Object.freeze({
38
23
  layerCount: model.layers.length,
39
24
  projection: Object.freeze({ embed: model.embed, logitsFromHidden: model.logitsFromHidden.bind(model) }),
40
25
  }) });
@@ -0,0 +1,25 @@
1
+ import type { ObjectCache } from "../../contracts/object-cache";
2
+ import { cloneAttachments, disposeAttachments, type CheckpointAttachment } from "./checkpoint-state";
3
+ import type { MlxArray } from "../../mlx/array";
4
+
5
+ /** Encoder-owned tensor schema over the cache's exact-object interface. */
6
+ export class EncoderCache {
7
+ constructor(private readonly objects: ObjectCache<CheckpointAttachment[]>,
8
+ readonly identity: string) {}
9
+
10
+ async take(key: string): Promise<MlxArray | null> {
11
+ const lease = await this.objects.take(`encoder-v1:${this.identity}:${key}`);
12
+ if (!lease) return null;
13
+ try {
14
+ const tensor = lease.value[0]!.tensors[0]!;
15
+ return tensor.slice(tensor.shape.map(() => 0), [...tensor.shape]);
16
+ } finally { lease.dispose(); }
17
+ }
18
+
19
+ /** Borrow the result; storage retains its own immutable native view. */
20
+ put(key: string, features: MlxArray): void {
21
+ const owned = cloneAttachments([{ schema: "encoder-features-v1", metadata: {}, tensors: [features] }]);
22
+ try { this.objects.put(`encoder-v1:${this.identity}:${key}`, owned); }
23
+ catch (error) { disposeAttachments(owned); throw error; }
24
+ }
25
+ }
@@ -6,8 +6,8 @@ import { leaseCacheStates } from "./state-views";
6
6
 
7
7
  /** Append an already accepted span without intermediate vocabulary heads.
8
8
  * Chunking changes the graph shape, so callers must qualify its numerics for
9
- * their model. A verify round must use chunkSize=0: recurrent rollback records
10
- * one forward per round. The complete append precedes emission/cancellation
9
+ * their model. Verification uses its separate round operation because
10
+ * recurrent rollback records one forward per round. Append precedes emission/cancellation
11
11
  * boundaries, preserving the cache-covered prefix on a consumer break.
12
12
  */
13
13
  export async function appendFillHidden(
@@ -17,15 +17,29 @@ export async function appendFillHidden(
17
17
  chunkSize: (cache: readonly Cache[]) => number,
18
18
  ): Promise<MlxArray> {
19
19
  const input = ops.fromInt32(ids, [1, ids.length]);
20
+ try { return await appendHiddenRows(forward, cache, input, chunkSize); }
21
+ finally { input.dispose(); }
22
+ }
23
+
24
+ /** Append equal-length committed token rows [B,L]. The caller owns the input;
25
+ * the returned hidden rows belong to the caller. Model policy receives B so
26
+ * a larger cohort cannot silently cross its qualified projection geometry. */
27
+ export async function appendHiddenRows(
28
+ forward: (ids: MlxArray, cache: Cache[]) => MlxArray | Promise<MlxArray>,
29
+ cache: Cache[],
30
+ input: MlxArray,
31
+ chunkSize: (cache: readonly Cache[], rows: number) => number,
32
+ ): Promise<MlxArray> {
33
+ const [rows, length] = input.shape as [number, number];
20
34
  const hidden: MlxArray[] = [];
21
35
  try {
22
- for (let start = 0; start < ids.length;) {
23
- const limit = chunkSize(cache);
36
+ for (let start = 0; start < length;) {
37
+ const limit = chunkSize(cache, rows);
24
38
  if (!Number.isSafeInteger(limit) || limit < 1)
25
39
  throw new Error("Model append chunk limit must be a positive integer");
26
- const end = Math.min(ids.length, start + limit);
27
- if (start === 0 && end === ids.length) return await forward(input, cache);
28
- const part = input.slice([0, start], [1, end]);
40
+ const end = Math.min(length, start + limit);
41
+ if (start === 0 && end === length) return await forward(input, cache);
42
+ const part = input.slice([0, start], [rows, end]);
29
43
  try {
30
44
  const h = await forward(part, cache);
31
45
  hidden.push(h);
@@ -40,6 +54,5 @@ export async function appendFillHidden(
40
54
  return ops.concatAxis(hidden, 1);
41
55
  } finally {
42
56
  for (const h of hidden) h.dispose();
43
- input.dispose();
44
57
  }
45
58
  }