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,497 @@
1
+ import type { GenerateOptions } from "../../generate";
2
+ import type { RuntimeModel } from "../../model/factory";
3
+ import type { Cache } from "../../model/gemma4-base";
4
+ import type { MlxArray } from "../../mlx/array";
5
+ import * as ops from "../../mlx/ops";
6
+ import { clearCache } from "../../mlx/ffi";
7
+ import { makeStepSampler, type DeviceStepSampler, type NumberStepSampler } from "../../sampler";
8
+ import type { FillSession, Proposal } from "../../fill/fill-session";
9
+ import { disposeResources, cleanupFailure, withResource } from "../../engine/resources";
10
+ import { snapshotGenerationPolicy } from "./request-policy";
11
+ import { MlxPrefillCohort } from "./prefill-cohort";
12
+ import { MlxStateRows } from "./state-rows";
13
+ import { targetCacheLayout } from "./cache-layout";
14
+ import { leaseCacheStates } from "./state-views";
15
+ import { createKvMaintenance, type KvMaintenance } from "./kv-maintenance";
16
+ import { bindLegacyAutoregressiveModel, supportsCommittedAppendCache, type MlxAutoregressiveBinding, type MlxTokenAppend } from "./autoregressive";
17
+ import { appendHiddenRows } from "./fill-append";
18
+ import { advanceSpeculativeOutputs } from "./speculative-round";
19
+ import { bindRowCacheRollback } from "./rollback";
20
+ import { bindSpeculativeTargetModel, type MlxSpeculativeTargetBinding } from "./speculative";
21
+ import type { MlxForwardWork } from "./mixed-iteration";
22
+ import type { MlxGroupedMethod, MlxGroupMethodHost, MlxGroupMethodRequest,
23
+ MlxGroupPreparation, Row } from "./batch-group";
24
+
25
+ interface RequestState {
26
+ sampling: DeviceStepSampler;
27
+ fill: FillSession;
28
+ proposal?: { value: Proposal; emitted: number };
29
+ }
30
+
31
+ /** Strict continuations share preparation, row layouts and queueing with other
32
+ * methods. Sampled tokens remain pipelined; committed tokens bypass sampling.
33
+ * This binding is qualified independently before serving advertises it. */
34
+ export function bindFillGroupRequests(model: RuntimeModel) {
35
+ return (input: GenerateOptions): MlxGroupMethodRequest => {
36
+ const options = snapshotGenerationPolicy(input);
37
+ return {
38
+ key: JSON.stringify(["fill", options.kvBits ?? null, options.kvGroupSize ?? 64,
39
+ options.quantizedKvStart ?? null, options.turboQuant ?? null, options.kvConfig ?? null]),
40
+ data: options,
41
+ open: host => new FillGroup(host, model, options),
42
+ };
43
+ };
44
+ }
45
+
46
+ class FillGroup implements MlxGroupedMethod {
47
+ readonly #binding: MlxAutoregressiveBinding;
48
+ readonly #maintain: KvMaintenance;
49
+ readonly #append: MlxTokenAppend | null | undefined;
50
+ readonly #verify: MlxSpeculativeTargetBinding;
51
+ readonly #requests = new Map<Row, RequestState>();
52
+ #target: MlxStateRows<ReturnType<typeof targetCacheLayout>> | null = null;
53
+ #pending: MlxArray | null = null;
54
+ #known: boolean[] = [];
55
+ #real: boolean[] = [];
56
+ #steps = 0;
57
+
58
+ constructor(readonly host: MlxGroupMethodHost, readonly model: RuntimeModel, options: GenerateOptions) {
59
+ this.#binding = bindLegacyAutoregressiveModel(model);
60
+ this.#verify = bindSpeculativeTargetModel(model);
61
+ this.#maintain = createKvMaintenance(options);
62
+ const append = this.#binding.createAppend?.({ hasAdapters: !!this.#binding.adapters?.active.length, pagedKv: false });
63
+ this.#append = supportsCommittedAppendCache(append, options) ? append : null;
64
+ }
65
+
66
+ get runningTokens(): number { return this.host.rows.length * Math.max(1, this.#burstLength(), this.#verifyLength()); }
67
+
68
+ #verifyLength(): number {
69
+ if (!this.#pending || !this.host.rows.length ||
70
+ this.host.rows.some(row => this.#requests.get(row)!.proposal?.value.policy === "assert")) return 0;
71
+ return Math.max(...this.host.rows.map(row => {
72
+ const proposal = this.#requests.get(row)!.proposal;
73
+ return proposal ? proposal.value.ids.length - proposal.emitted : 0;
74
+ }));
75
+ }
76
+
77
+ #burstLength(): number {
78
+ const rows = this.host.rows;
79
+ if (!rows.length || !this.#pending || this.#known.some(known => !known) ||
80
+ (this.#append?.maxChunkSize(this.#target!.caches, rows.length) ?? 1) <= 1) return 0;
81
+ return Math.min(...rows.map(row => {
82
+ const proposal = this.#requests.get(row)!.proposal;
83
+ return proposal?.value.policy === "assert" ? proposal.value.ids.length - proposal.emitted : 0;
84
+ }));
85
+ }
86
+
87
+ prepare(first: Row): MlxGroupPreparation {
88
+ const ready: { row: Row; caches: Cache[]; retain?: () => void }[] = [];
89
+ const target = new MlxPrefillCohort({
90
+ model: this.model, chunkSize: this.host.prefillChunkSize,
91
+ tailSplit: this.host.runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true),
92
+ promptCache: this.host.promptCache, maintain: this.#maintain,
93
+ forward: async (ids, caches) => this.#binding.graph.forwardHidden(ids, caches),
94
+ project: hidden => this.#binding.graph.projectLogits(hidden, { type: "all" }),
95
+ complete: async (state, logits) => {
96
+ const row = state.row;
97
+ const request = this.#requests.get(row)!;
98
+ using scores = ops.reshape(logits, [1, logits.shape.at(-1)!]);
99
+ using token = request.sampling.sample(scores, 0).token;
100
+ row.generated = row.sampled = 1;
101
+ try {
102
+ const reason = await this.#emit(row, token.toIntTokens()[0]!, false);
103
+ this.#maintain(state.solo);
104
+ if (reason) {
105
+ this.#put(row, state.solo, state.retain);
106
+ state.solo = []; state.retain = undefined;
107
+ this.#closeRequest(row); this.host.finish(row, reason);
108
+ } else {
109
+ ready.push({ row, caches: state.solo, retain: state.retain });
110
+ }
111
+ } catch (error) { this.#closeRequest(row); throw error; }
112
+ },
113
+ reject: (row, error) => { this.#closeRequest(row); row.reject(error); },
114
+ });
115
+ const admit = (row: Row) => {
116
+ const options = row.req.method!.data as GenerateOptions;
117
+ const request: RequestState = { fill: options.fill!,
118
+ sampling: makeStepSampler(options, { tokenRepresentation: "device", grammarWait: "external",
119
+ historyUpdate: "manual", initialHistory: row.req.promptIds }) };
120
+ row.fill = request.fill.stats;
121
+ this.#requests.set(row, request);
122
+ try { target.admit(row); }
123
+ catch (error) { this.#closeRequest(row); throw error; }
124
+ };
125
+ const preparation: MlxGroupPreparation = {
126
+ get rows() { return [...target.rows, ...ready.map(state => state.row)]; },
127
+ get canAdmit() { return target.canAdmit; },
128
+ get tokenWeight() { return target.tokenWeight + ready.reduce((sum, state) => sum + state.row.promptTokens - state.row.cachedTokens, 0); },
129
+ admit,
130
+ advance: async work => {
131
+ if (!ready.length) {
132
+ const done = await target.advance(work);
133
+ return done && !ready.length;
134
+ }
135
+ // Flush before membership changes. Prepared output was already
136
+ // published on the preceding scheduler iteration.
137
+ await this.#flush();
138
+ while (ready.length) {
139
+ const state = ready.shift()!;
140
+ try {
141
+ if (state.row.req.signal?.aborted) {
142
+ this.#closeRequest(state.row); state.row.reject(state.row.req.signal.reason); continue;
143
+ }
144
+ this.#maintain.prepareBatch?.(state.caches);
145
+ this.#target ??= new MlxStateRows(state.caches.map(targetCacheLayout));
146
+ this.#target.append(state.caches);
147
+ // Copies must settle before releasing a restored SSD lease.
148
+ if (state.retain) withResource(leaseCacheStates(this.#target.caches), arrays => ops.evalAll([...arrays]));
149
+ this.host.join(state.row);
150
+ } finally { disposeResources([...state.caches, { dispose: () => state.retain?.() }]); }
151
+ }
152
+ return !target.rows.length;
153
+ },
154
+ dispose: () => {
155
+ const rows = preparation.rows;
156
+ disposeResources([target, ...ready.splice(0).flatMap(state => [...state.caches,
157
+ { dispose: () => state.retain?.() }]), { dispose: () => {
158
+ for (const row of rows) if (!this.host.rows.includes(row)) this.#closeRequest(row);
159
+ } }]);
160
+ },
161
+ };
162
+ try { admit(first); return preparation; }
163
+ catch (error) { return cleanupFailure(error, () => preparation.dispose()); }
164
+ }
165
+
166
+ async advance(work?: MlxForwardWork): Promise<void> {
167
+ const live = this.host.rows.flatMap((row, index) => row.req.signal?.aborted ? [] : [index]);
168
+ if (live.length !== this.host.rows.length) {
169
+ for (const row of this.host.rows) if (row.req.signal?.aborted) row.reject(row.req.signal.reason);
170
+ this.host.filterRows(live);
171
+ }
172
+ const rows = [...this.host.rows], B = rows.length;
173
+ if (!B) return;
174
+ const verifyLength = this.#verifyLength();
175
+ if (!work && verifyLength > 1) { await this.#verifyKnown(verifyLength); return; }
176
+ const burst = this.#burstLength();
177
+ if (!work && burst > 1) { await this.#appendKnown(burst); return; }
178
+ const previous = this.#pending, known = this.#known, real = this.#real;
179
+ const anyLive = rows.some(row => row.sampled < row.req.maxTokens);
180
+ let next: MlxArray | null = null;
181
+ const nextKnown: boolean[] = [], nextReal: boolean[] = [];
182
+ if (anyLive) {
183
+ using input = previous ? ops.reshape(previous, [B, 1]) : ops.fromInt32(rows.map(row => row.current), [B, 1]);
184
+ using hidden = await (work ? work(input, this.#target!.caches) : this.#binding.graph.forwardHidden(input, this.#target!.caches));
185
+ const selected = rows.map((row, index) => {
186
+ const request = this.#requests.get(row)!;
187
+ // The next selected token is not committed until it becomes input.
188
+ // Discarding a speculative sample for an assert cannot pollute history.
189
+ if (request.sampling.needsHistory) {
190
+ using current = input.slice([index, 0], [index + 1, 1]);
191
+ request.sampling.commitDevice(current);
192
+ }
193
+ const value = request.proposal?.value.policy === "assert"
194
+ ? request.proposal.value.ids[request.proposal.emitted + Number(!!previous && known[index])] : undefined;
195
+ nextKnown.push(value !== undefined); nextReal.push(row.sampled < row.req.maxTokens);
196
+ return value;
197
+ });
198
+ const needScores = rows.some((_row, index) => nextReal[index] && !nextKnown[index]);
199
+ using logits = needScores ? this.#binding.graph.projectLogits(hidden, { type: "last" }) : null;
200
+ const tokens: MlxArray[] = [];
201
+ try {
202
+ for (let index = 0; index < B; index++) {
203
+ const row = rows[index]!;
204
+ if (!nextReal[index]) { tokens.push(ops.fromInt32([0], [1])); continue; }
205
+ if (nextKnown[index]) tokens.push(ops.fromInt32([selected[index]!], [1]));
206
+ else {
207
+ using slice = logits!.slice([index, 0, 0], [index + 1, 1, logits!.shape[2]!]);
208
+ using scores = ops.reshape(slice, [1, slice.shape[2]!]);
209
+ tokens.push(this.#requests.get(row)!.sampling.sample(scores, row.sampled).token);
210
+ }
211
+ row.sampled++;
212
+ }
213
+ next = ops.concatAxis(tokens, 0);
214
+ // Constant known tokens have no dependency on the model graph.
215
+ // Submit state explicitly when the vocabulary projection is omitted.
216
+ if (needScores) ops.asyncEvalAll([next]);
217
+ else withResource(leaseCacheStates(this.#target!.caches), state => ops.asyncEvalAll([next!, hidden, ...state]));
218
+ } catch (error) { next?.dispose(); next = null; throw error; }
219
+ finally { disposeResources(tokens); }
220
+ }
221
+ this.#pending = next; this.#known = nextKnown; this.#real = nextReal;
222
+ if (previous) {
223
+ let values: number[];
224
+ try { values = previous.toIntTokens(); } finally { previous.dispose(); }
225
+ if (anyLive) for (let index = 0; index < B; index++) {
226
+ if (real[index]) rows[index]!.fed.push(values[index]!);
227
+ else rows[index]!.fedTainted = true;
228
+ }
229
+ await this.#emitRows(values, known);
230
+ } else if (anyLive) for (const row of rows) row.fed.push(row.current);
231
+ this.#checkPendingProposals();
232
+ if (++this.#steps % 256 === 0) clearCache();
233
+ }
234
+
235
+ /** The pending sample already verifies an echo's first token. Asserted
236
+ * continuations keep their append policy; other rows can join a verified
237
+ * span with an empty proposal through the existing row verifier. */
238
+ #checkPendingProposals(): void {
239
+ if (!this.#pending) return;
240
+ const rows = this.host.rows;
241
+ if (!rows.some(row => this.#requests.get(row)!.proposal?.value.policy === "verify")) return;
242
+ const tokens = this.#pending.toIntTokens();
243
+ for (const [index, row] of rows.entries()) {
244
+ const request = this.#requests.get(row)!, proposal = request.proposal;
245
+ if (proposal?.value.policy !== "verify") continue;
246
+ if (tokens[index] === proposal.value.ids[proposal.emitted]) this.#known[index] = true;
247
+ else {
248
+ request.fill.commit(proposal.value, proposal.emitted);
249
+ request.proposal = undefined;
250
+ }
251
+ }
252
+ }
253
+
254
+ async #verifyKnown(width: number): Promise<void> {
255
+ // Publish the already verified pending token before changing geometry.
256
+ await this.#flush();
257
+ const rows = [...this.host.rows];
258
+ if (!rows.length) return;
259
+ // Publishing an ordinary row can discover a strict template span. Let
260
+ // its append policy choose the next operation before verifying echoes.
261
+ if (rows.some(row => this.#requests.get(row)!.proposal?.value.policy === "assert")) return;
262
+ const pending = rows.map(row => row.current);
263
+ const proposals = rows.map(row => {
264
+ const proposal = this.#requests.get(row)!.proposal;
265
+ // A newly discovered proposal cannot exceed this iteration's declared
266
+ // token work. Its remaining continuation stays request-owned.
267
+ return proposal?.value.policy === "verify"
268
+ ? proposal.value.ids.slice(proposal.emitted, proposal.emitted + width - 1) : [];
269
+ });
270
+ const depth = Math.max(...proposals.map(ids => ids.length));
271
+ const samplers = rows.map(row => {
272
+ const sampling = this.#requests.get(row)!.sampling;
273
+ sampling.commitNumbers([row.current]);
274
+ let previous: number | undefined;
275
+ // A second sample is requested only after the preceding candidate was
276
+ // accepted. Leave the final correction out of manual processor history
277
+ // until the next forward actually consumes it.
278
+ return { ...sampling,
279
+ async sample(scores, step) {
280
+ if (previous !== undefined) sampling.commitNumbers([previous]);
281
+ const result = sampling.sample(scores, step);
282
+ try { previous = result.token.toIntTokens()[0]!; return { token: previous, extras: result.extras }; }
283
+ finally { result.token.dispose(); }
284
+ },
285
+ } satisfies NumberStepSampler;
286
+ });
287
+ const halted = new Set<Row>();
288
+ const rollback = bindRowCacheRollback(this.#target!.caches, rows.length);
289
+ const verifying = rows.filter(row => this.#requests.get(row)!.proposal?.value.policy === "verify")
290
+ .map(row => this.#requests.get(row)!.fill);
291
+ const pin = this.#verify.pinVerify?.();
292
+ try {
293
+ const completed = await advanceSpeculativeOutputs(rows.map((row, index) => ({
294
+ pending: pending[index]!, step: row.sampled, remaining: row.req.maxTokens - row.generated,
295
+ eosTokenIds: row.req.eosTokenIds, sampling: samplers[index]!,
296
+ output: { commit: async ids => {
297
+ const proposal = this.#requests.get(row)!.proposal;
298
+ const known = proposal?.value.policy === "verify" && ids[0] === proposal.value.ids[proposal.emitted];
299
+ if (!known) this.#settleProposal(row);
300
+ row.generated++;
301
+ const reason = await this.#emit(row, ids[0]!, known);
302
+ if (reason === "stop") { halted.add(row); return false; }
303
+ } },
304
+ })), depth, { draft: () => proposals, commit: () => {} }, {
305
+ transaction: { ...rollback,
306
+ begin(depth) {
307
+ const start = performance.now(); rollback.begin(depth);
308
+ const elapsed = performance.now() - start;
309
+ for (const fill of verifying) fill.noteVerifyEvent(elapsed);
310
+ },
311
+ resolve(accepted) {
312
+ const start = performance.now(); rollback.resolve(accepted);
313
+ const elapsed = performance.now() - start;
314
+ for (const fill of verifying) fill.stats.checkpointMs += elapsed;
315
+ },
316
+ },
317
+ forward: async ids => {
318
+ const result = await this.#verify.forward(ids, this.#target!.caches);
319
+ const context = result.ctxML ?? result.hidden;
320
+ try { return { logits: this.#verify.projectLogits(result.hidden), context }; }
321
+ catch (error) { context.dispose(); throw error; }
322
+ finally { if (result.ctxML) result.hidden.dispose(); }
323
+ },
324
+ });
325
+ const keep: number[] = [];
326
+ for (const [index, row] of rows.entries()) {
327
+ const output = completed.outputs[index]!, round = completed.rounds[index]!;
328
+ if (output.kind !== "failed" && output.kind !== "cancelled")
329
+ row.fed.push(pending[index]!, ...round.drafts.slice(0, output.accepted));
330
+ row.sampled += round.acceptance.emitted.length + Number(round.acceptance.sawEos);
331
+ if (output.kind === "continue") { row.current = output.pending; keep.push(index); }
332
+ else if (output.kind === "failed") row.reject(output.error);
333
+ else if (output.kind === "cancelled") row.reject(row.req.signal?.reason ?? new Error(output.reason));
334
+ else {
335
+ if (!halted.has(row) && round.acceptance.sawEos && row.generated < row.req.maxTokens) row.generated++;
336
+ this.#put(row, this.#target!.extractRow(index));
337
+ this.#settleProposal(row); this.host.finish(row, output.kind);
338
+ }
339
+ }
340
+ if (keep.length !== rows.length) this.host.filterRows(keep);
341
+ if (++this.#steps % 256 === 0) clearCache();
342
+ } finally { pin?.close(); }
343
+ }
344
+
345
+ /** Multi-position arithmetic is model-owned. Wider cohorts whose model
346
+ * declares one position keep the pipelined single-position implementation. */
347
+ async #appendKnown(length: number): Promise<void> {
348
+ const rows = [...this.host.rows], B = rows.length;
349
+ const spans = new Map(rows.map(row => {
350
+ const proposal = this.#requests.get(row)!.proposal!;
351
+ return [row, proposal.value.ids.slice(proposal.emitted, proposal.emitted + length)] as const;
352
+ }));
353
+ using input = ops.fromInt32(rows.flatMap(row => spans.get(row)!), [B, length]);
354
+ using hidden = await appendHiddenRows(this.#append!.forwardHidden.bind(this.#append), this.#target!.caches, input,
355
+ (caches, count) => Math.min(this.#append!.maxChunkSize(caches, count),
356
+ this.#maintain.maxAppendTokens?.(caches) ?? Infinity,
357
+ ...rows.map(row => this.#requests.get(row)!.fill.appendChunkSize || Infinity)));
358
+ const nextKnown: boolean[] = [], nextReal: boolean[] = [];
359
+ const selected = rows.map(row => {
360
+ const request = this.#requests.get(row)!, proposal = request.proposal!;
361
+ request.sampling.commitNumbers(spans.get(row)!);
362
+ row.fed.push(...spans.get(row)!);
363
+ const next = proposal.value.ids[proposal.emitted + length];
364
+ const real = row.generated + length < row.req.maxTokens;
365
+ nextKnown.push(next !== undefined); nextReal.push(real);
366
+ row.sampled = row.generated + length + Number(real);
367
+ return next;
368
+ });
369
+ const needScores = rows.some((_row, index) => nextReal[index] && !nextKnown[index]);
370
+ using logits = needScores ? this.#binding.graph.projectLogits(hidden, { type: "last" }) : null;
371
+ const tokens: MlxArray[] = [];
372
+ let next: MlxArray | null = null;
373
+ try {
374
+ for (const [index, row] of rows.entries()) {
375
+ if (!nextReal[index] || nextKnown[index]) tokens.push(ops.fromInt32([selected[index] ?? 0], [1]));
376
+ else {
377
+ using slice = logits!.slice([index, 0, 0], [index + 1, 1, logits!.shape[2]!]);
378
+ using scores = ops.reshape(slice, [1, slice.shape[2]!]);
379
+ tokens.push(this.#requests.get(row)!.sampling.sample(scores, row.generated + length).token);
380
+ }
381
+ }
382
+ next = ops.concatAxis(tokens, 0);
383
+ if (needScores) ops.asyncEvalAll([next]);
384
+ else withResource(leaseCacheStates(this.#target!.caches), state => ops.asyncEvalAll([hidden, ...state]));
385
+ this.#pending!.dispose(); this.#pending = next; next = null;
386
+ this.#known = nextKnown; this.#real = nextReal;
387
+ } finally { next?.dispose(); disposeResources(tokens); }
388
+ for (let column = 0; column < length && this.host.rows.length; column++) {
389
+ const active = [...this.host.rows];
390
+ await this.#emitRows(active.map(row => spans.get(row)![column]!), active.map(() => true));
391
+ }
392
+ if (++this.#steps % 256 === 0) clearCache();
393
+ }
394
+
395
+ async #emit(row: Row, token: number, known: boolean): Promise<"stop" | "length" | undefined> {
396
+ row.req.signal?.throwIfAborted();
397
+ const request = this.#requests.get(row)!;
398
+ if (row.req.eosTokenIds.includes(token)) return "stop";
399
+ // Account a known token even when the consumer stops on this token.
400
+ if (known) {
401
+ const proposal = request.proposal!;
402
+ if (++proposal.emitted === proposal.value.ids.length) {
403
+ request.fill.commit(proposal.value, proposal.emitted); request.proposal = undefined;
404
+ }
405
+ }
406
+ if (!known) {
407
+ const proposal = request.fill.push(token, row.req.maxTokens - row.generated);
408
+ if (proposal) {
409
+ request.proposal = { value: proposal, emitted: 0 };
410
+ }
411
+ }
412
+ const more = await this.host.publish(row, token);
413
+ if (more === false) return "stop";
414
+ if (row.generated >= row.req.maxTokens) return "length";
415
+ row.current = token;
416
+ }
417
+
418
+ async #emitRows(values: number[], known: readonly boolean[]): Promise<void> {
419
+ const rows = [...this.host.rows], keep: number[] = [];
420
+ const replacement = new Map<number, number>();
421
+ for (let index = 0; index < rows.length; index++) {
422
+ const row = rows[index]!;
423
+ row.generated++;
424
+ try {
425
+ const reason = await this.#emit(row, values[index]!, known[index] ?? false);
426
+ if (reason) {
427
+ this.#put(row, this.#target!.extractRow(index));
428
+ this.#settleProposal(row); this.host.finish(row, reason);
429
+ } else {
430
+ keep.push(index);
431
+ const request = this.#requests.get(row)!;
432
+ if (!known[index] && request.proposal?.value.policy === "assert" && this.#pending) {
433
+ replacement.set(index, request.proposal.value.ids[0]!);
434
+ request.fill.noteWastedSample(); this.#known[index] = true;
435
+ }
436
+ }
437
+ } catch (error) { row.reject(error); }
438
+ }
439
+ if (replacement.size) {
440
+ const parts: MlxArray[] = [];
441
+ try {
442
+ for (let index = 0; index < rows.length; index++) parts.push(replacement.has(index)
443
+ ? ops.fromInt32([replacement.get(index)!], [1]) : this.#pending!.slice([index], [index + 1]));
444
+ const next = ops.concatAxis(parts, 0); this.#pending!.dispose(); this.#pending = next;
445
+ } finally { disposeResources(parts); }
446
+ }
447
+ if (keep.length !== rows.length) this.host.filterRows(keep);
448
+ }
449
+
450
+ async #flush(): Promise<void> {
451
+ const pending = this.#pending, known = this.#known;
452
+ if (!pending) return;
453
+ this.#pending = null; this.#known = []; this.#real = [];
454
+ let values: number[];
455
+ try { values = pending.toIntTokens(); } finally { pending.dispose(); }
456
+ await this.#emitRows(values, known);
457
+ }
458
+
459
+ #put(row: Row, caches: Cache[], retain?: () => void): void {
460
+ try {
461
+ if (this.host.promptCache && !row.fedTainted && !row.req.signal?.aborted) {
462
+ this.host.promptCache.put([...row.req.promptIds, ...row.fed], caches,
463
+ row.cacheNamespace, retain, undefined, row.req.cacheSessionId);
464
+ caches = []; retain = undefined;
465
+ }
466
+ } finally { disposeResources([...caches, { dispose: () => retain?.() }]); }
467
+ }
468
+
469
+ #settleProposal(row: Row): void {
470
+ const request = this.#requests.get(row);
471
+ if (request?.proposal) {
472
+ const proposal = request.proposal; request.proposal = undefined;
473
+ request.fill.commit(proposal.value, proposal.emitted);
474
+ }
475
+ }
476
+ #closeRequest(row: Row): void {
477
+ this.#settleProposal(row);
478
+ const request = this.#requests.get(row); this.#requests.delete(row); request?.sampling.dispose();
479
+ }
480
+ filterRows(keep: readonly number[], _discard: boolean): void {
481
+ if (this.#pending) {
482
+ using indices = ops.fromInt32([...keep], [keep.length]);
483
+ const next = keep.length ? ops.takeAxis(this.#pending, indices, 0) : null;
484
+ this.#pending.dispose(); this.#pending = next;
485
+ }
486
+ this.#known = keep.map(index => this.#known[index]!); this.#real = keep.map(index => this.#real[index]!);
487
+ this.#target?.filterRows(keep);
488
+ const retained = new Set(keep.map(index => this.host.rows[index]!));
489
+ for (const row of this.host.rows) if (!retained.has(row)) this.#closeRequest(row);
490
+ }
491
+ dispose(): void {
492
+ const pending = this.#pending, target = this.#target;
493
+ this.#pending = null; this.#target = null;
494
+ disposeResources([...(pending ? [pending] : []), ...(target ? [target] : []),
495
+ { dispose: () => { for (const row of this.#requests.keys()) this.#closeRequest(row); } }]);
496
+ }
497
+ }
@@ -0,0 +1,32 @@
1
+ import type { MlxArray } from "../../mlx/array";
2
+ import * as ops from "../../mlx/ops";
3
+ import type { Cache } from "../../model/gemma4-base";
4
+ import { fullRowPadding } from "../../model/full-prefill-row";
5
+ import { disposeResources } from "../../engine/resources";
6
+
7
+ /** Assemble full-attention tensors at the model's B while storage retains
8
+ * per-row formats and positions. Returned arrays own their captured state. */
9
+ export function appendFullKvRows(rows: readonly Cache[], leftPad: readonly number[], width: number,
10
+ k: MlxArray, v: MlxArray, deferredValues = false): [MlxArray, MlxArray] {
11
+ const parts: [MlxArray, MlxArray][] = [], padded: MlxArray[] = [];
12
+ try {
13
+ for (const [index, row] of rows.entries()) {
14
+ using kr = k.slice([index, 0, 0, 0], [index + 1, k.shape[1]!, k.shape[2]!, k.shape[3]!]);
15
+ using vr = v.slice([index, 0, 0, 0], [index + 1, v.shape[1]!, v.shape[2]!, v.shape[3]!]);
16
+ parts.push(deferredValues && row.rotatedValueAttention
17
+ ? row.rotatedValueAttention.updateAndFetchDeferredV(kr, vr) : row.updateAndFetch(kr, vr));
18
+ }
19
+ const combine = (field: 0 | 1) => {
20
+ const views = parts.map((part, row) => {
21
+ const a = part[field], before = leftPad[row]! - fullRowPadding(rows[row]!), after = width - before - a.shape[2]!;
22
+ if (!before && !after) return a;
23
+ using prefix = ops.zeros([1, a.shape[1]!, before, a.shape[3]!], a.dtype);
24
+ using suffix = ops.zeros([1, a.shape[1]!, after, a.shape[3]!], a.dtype);
25
+ const result = ops.concatAxis([prefix, a, suffix], 2); padded.push(result); return result;
26
+ });
27
+ return views.length === 1 ? ops.contiguous(views[0]!) : ops.concatAxis(views, 0);
28
+ };
29
+ const keys = combine(0);
30
+ try { return [keys, combine(1)]; } catch (error) { keys.dispose(); throw error; }
31
+ } finally { disposeResources([...parts.flat(), ...padded]); }
32
+ }
@@ -0,0 +1,54 @@
1
+ import { captureKvDonorAttention, combineKvDonorAttention } from "../../model/kv-attention-view";
2
+ import type { MlxArray } from "../../mlx/array";
3
+ import * as ops from "../../mlx/ops";
4
+ import type { Cache, KvDonorRows } from "../../model/gemma4-base";
5
+ import { fullRowInner, fullRowPadding } from "../../model/full-prefill-row";
6
+ import { disposeResources } from "../../engine/resources";
7
+
8
+ /** Read full-attention rows in their existing physical columns. Valid token
9
+ * coverage excludes prefill padding; no append, conversion or compaction. */
10
+ export function captureFullKvDonorRows(rows: readonly Cache[], leftPad: readonly number[], width: number): KvDonorRows {
11
+ const parts: KvDonorRows[] = [], padded: MlxArray[] = [];
12
+ try {
13
+ for (const row of rows) parts.push(fullRowInner(row).captureDonorRows!());
14
+ const combine = (field: "keys" | "values") => {
15
+ const views = parts.map((part, index) => {
16
+ const a = part[field], before = leftPad[index]! - fullRowPadding(rows[index]!), after = width - before - a.shape[2]!;
17
+ if (!before && !after) return a;
18
+ using prefix = ops.zeros([1, a.shape[1]!, before, a.shape[3]!], a.dtype);
19
+ using suffix = ops.zeros([1, a.shape[1]!, after, a.shape[3]!], a.dtype);
20
+ const result = ops.concatAxis([prefix, a, suffix], 2); padded.push(result); return result;
21
+ });
22
+ return views.length === 1 ? ops.contiguous(views[0]!) : ops.concatAxis(views, 0);
23
+ };
24
+ const offsets = rows.map(row => row.offset), starts = [...leftPad];
25
+ const keys = combine("keys");
26
+ try { return { keys, values: combine("values"), offsets, starts, ends: offsets.map((offset, row) => starts[row]! + offset) }; }
27
+ catch (error) { keys.dispose(); throw error; }
28
+ } finally { disposeResources([...parts.flatMap(part => [part.keys, part.values]), ...padded]); }
29
+ }
30
+
31
+
32
+ /** Capture full rows in global columns while each encoded view retains its
33
+ * own physical width and numerical attention operation. */
34
+ export function captureFullKvDonorAttention(rows: readonly Cache[], leftPad: readonly number[], width: number): import("../../model/gemma4-base").KvDonorAttention {
35
+ const captured: import("../../model/gemma4-base").KvDonorAttention[] = [];
36
+ try {
37
+ for (const [index, row] of rows.entries()) {
38
+ const view = captureKvDonorAttention(fullRowInner(row));
39
+ const pad = leftPad[index]! - fullRowPadding(row);
40
+ captured.push({ width, dtype: view.dtype, offsets: [row.offset],
41
+ starts: [leftPad[index]!], ends: [leftPad[index]! + row.offset],
42
+ attend(q, scale, mask) {
43
+ if (!mask.arr) return view.attend(q, scale, mask);
44
+ const from = mask.arr.shape.map(() => 0), to = [...mask.arr.shape];
45
+ from[from.length - 1] = pad; to[to.length - 1] = pad + view.width;
46
+ using arr = mask.arr.slice(from, to);
47
+ return view.attend(q, scale, { mode: mask.mode, arr });
48
+ },
49
+ dispose: () => view.dispose(),
50
+ });
51
+ }
52
+ return combineKvDonorAttention(captured);
53
+ } catch (error) { disposeResources(captured); throw error; }
54
+ }
@@ -0,0 +1,65 @@
1
+ import { KVCache, type Cache, type PaddedPrefillCache, type PrefillPadding } from "../../model/gemma4-base";
2
+ import { FullPrefillRow, fullRowInner } from "../../model/full-prefill-row";
3
+ import { cloneKvCaches } from "../../kv-store";
4
+ import { FullKvPositions, SpeculativeTransitioningKvRows, type KvRowTransition, type SpeculativeTransitionedKvLayout } from "./transitioning-kv-rows";
5
+
6
+ type FullLayout = SpeculativeTransitionedKvLayout & PaddedPrefillCache & {
7
+ restorePrefillEnds(ends: readonly number[] | undefined): void;
8
+ }
9
+
10
+ class PrefillPositions extends FullKvPositions {
11
+ override sync(rows: readonly Cache[], preserve: boolean): void {
12
+ const offsets = rows.map(row => row instanceof FullPrefillRow ? row.positionOffset : row.offset);
13
+ this.mergeRows(preserve
14
+ ? [{ planes: [], rowOffsets: offsets, leftPad: [...this.leftPad] }]
15
+ : rows.map((row, index) => ({ planes: [], rowOffsets: [offsets[index]!],
16
+ leftPad: [row instanceof FullPrefillRow ? row.leftPadding : 0] })));
17
+ }
18
+ }
19
+
20
+ /** Full-attention padding composes with the existing precision transition
21
+ * and rollback lifecycle. Codec-specific attention stays in each adapter. */
22
+ export abstract class FullTransitioningKvRows<Layout extends FullLayout>
23
+ extends SpeculativeTransitioningKvRows<Layout> implements PaddedPrefillCache {
24
+ constructor(transition: KvRowTransition<Layout>, row?: Cache) {
25
+ super({ ...transition,
26
+ converted: row => transition.converted(fullRowInner(row)),
27
+ extractRow: row => row instanceof FullPrefillRow ? row.extract() : cloneKvCaches([row])[0]!,
28
+ packRows(layout, rows) {
29
+ layout.mergeRows(rows.map(fullRowInner));
30
+ if (rows.some(row => row instanceof FullPrefillRow)) {
31
+ layout.preparePrefill({ lengths: rows.map(() => 0),
32
+ leftPadding: rows.map(row => row instanceof FullPrefillRow ? row.leftPadding : 0) });
33
+ const ends = rows.map(row => row instanceof FullPrefillRow ? row.padding.endOffsets?.[0] : undefined);
34
+ layout.restorePrefillEnds(ends.some(end => end !== undefined) ? ends.map(end => end ?? Infinity) : undefined);
35
+ }
36
+ },
37
+ }, row, new PrefillPositions());
38
+ }
39
+ preparePrefill(padding: PrefillPadding): void {
40
+ if (this.packed) { this.packed.preparePrefill(padding); return; }
41
+ if (!this.rows.length) this.rows = padding.lengths.map(() => new KVCache());
42
+ this.rows = this.rows.map(row => row instanceof FullPrefillRow ? row : new FullPrefillRow(row as KVCache));
43
+ for (const [index, row] of this.rows.entries()) (row as FullPrefillRow).preparePrefill({
44
+ lengths: [padding.lengths[index]!],
45
+ ...(padding.leftPadding ? { leftPadding: [padding.leftPadding[index]!] } : {}),
46
+ ...(padding.rightPadding ? { rightPadding: [padding.rightPadding[index]!] } : {}),
47
+ });
48
+ this.syncPositions();
49
+ }
50
+ finalizePrefill(): void {
51
+ if (this.packed) { this.packed.finalizePrefill(); return; }
52
+ for (const row of this.rows) if (row instanceof FullPrefillRow) row.finalizePrefill();
53
+ this.syncPositions();
54
+ }
55
+ override filterRows(keep: readonly number[]): void {
56
+ if (!this.packed && keep.length) {
57
+ const pads = [...this.leftPad], shared = Math.min(...keep.map(row => pads[row]!));
58
+ for (const index of keep) {
59
+ const row = this.rows[index]!;
60
+ if (row instanceof FullPrefillRow) row.removeLeftPadding(Math.max(0, row.leftPadding - (pads[index]! - shared)));
61
+ }
62
+ }
63
+ super.filterRows(keep);
64
+ }
65
+ }