mlx-bun 0.0.10 → 0.0.12
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/README.md +107 -32
- package/THIRD_PARTY_LICENSES.md +10 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/colibri-glm52-port.md +1479 -0
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/pre-colibri-stabilization.md +329 -0
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/colibri-oracle-pin.md +1284 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/indexshare-performance-spike.md +357 -0
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/planning/release-notes-v0.0.12.md +80 -0
- package/docs/reference/cli.md +63 -18
- package/docs/reference/distribution.md +37 -6
- package/docs/reference/embedding.md +40 -16
- package/docs/reference/features-matrix.md +99 -0
- package/docs/reference/library-api.md +69 -20
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +60 -3
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +717 -24
- package/docs/reference/server-config.md +265 -129
- package/docs/reference/training.md +24 -16
- package/package.json +5 -3
- package/src/anthropic.ts +67 -44
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/chat-template.ts +155 -10
- package/src/cli.ts +483 -113
- package/src/config.ts +116 -24
- package/src/diffusion/diffusion-generate.ts +14 -8
- package/src/download.ts +143 -12
- package/src/eval/README.md +7 -6
- package/src/eval/ifeval.ts +63 -120
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +17 -8
- package/src/eval/tasks/ifeval.ts +167 -31
- package/src/expert-io.ts +391 -0
- package/src/expert-offload.ts +15 -2
- package/src/expert-residency.ts +1134 -0
- package/src/expert-usage.ts +415 -0
- package/src/flags.ts +13 -0
- package/src/generate.ts +438 -35
- package/src/grammar.ts +585 -0
- package/src/hf-push.ts +1 -1
- package/src/hub-rest.ts +242 -0
- package/src/index.ts +14 -2
- package/src/jobs/runner.ts +37 -18
- package/src/kv-store.ts +586 -93
- package/src/lora.ts +85 -14
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +426 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +149 -19
- package/src/mlx/autograd.ts +53 -16
- package/src/mlx/checkpoint.ts +46 -13
- package/src/mlx/custom-vjp.ts +83 -35
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +103 -6
- package/src/model/gemma4-base.ts +395 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/glm52-atlas.ts +511 -0
- package/src/model/glm52-cache.ts +706 -0
- package/src/model/glm52-config.ts +262 -0
- package/src/model/glm52-container.ts +305 -0
- package/src/model/glm52-coupling.ts +388 -0
- package/src/model/glm52-dsa.ts +602 -0
- package/src/model/glm52-expert-layout.ts +253 -0
- package/src/model/glm52-layout.ts +152 -0
- package/src/model/glm52-memory.ts +473 -0
- package/src/model/glm52-mla.ts +786 -0
- package/src/model/glm52-moe.ts +223 -0
- package/src/model/glm52-pilot.ts +537 -0
- package/src/model/glm52-reference.ts +445 -0
- package/src/model/glm52-residency.ts +557 -0
- package/src/model/glm52-resident-weights.ts +135 -0
- package/src/model/glm52-streamed-experts.ts +453 -0
- package/src/model/glm52-streamed-metal.ts +610 -0
- package/src/model/glm52-weights.ts +503 -0
- package/src/model/glm52.ts +1153 -0
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +19 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/native/expert_io.c +523 -0
- package/src/native-pack.ts +6 -4
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/responses.ts +62 -49
- package/src/serve/batch-scheduler.ts +1050 -78
- package/src/serve/generation-gateway.ts +252 -24
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1701 -175
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/glm52-mtp-source.ts +392 -0
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +617 -0
- package/src/spec/source.ts +118 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +111 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +438 -22
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -12
- package/src/train/lora-params.ts +17 -6
- package/src/train/trainer.ts +60 -30
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1197 -1791
- package/src/web/app.js +5694 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +300 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/web-tools.ts +54 -2
- package/src/weights.ts +82 -19
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
package/src/model/minicpm5.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Weights } from "../weights";
|
|
|
6
6
|
import { MlxArray } from "../mlx/array";
|
|
7
7
|
import { Dtype } from "../mlx/ffi";
|
|
8
8
|
import * as ops from "../mlx/ops";
|
|
9
|
+
import { unrotateValues as tqUnrotateValues } from "../mlx/turboquant-ops";
|
|
9
10
|
import {
|
|
10
11
|
argmaxLastPosition,
|
|
11
12
|
disposeTriple,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
KVCache,
|
|
14
15
|
LoraState,
|
|
15
16
|
QuantizedKVCache,
|
|
17
|
+
TurboQuantKVCache,
|
|
16
18
|
QuantizedEmbedding,
|
|
17
19
|
QuantizedLinear,
|
|
18
20
|
quantizedSdpa,
|
|
@@ -21,52 +23,27 @@ import {
|
|
|
21
23
|
type Cache,
|
|
22
24
|
type Mask,
|
|
23
25
|
} from "./gemma4-base";
|
|
24
|
-
import {
|
|
25
|
-
fusedSwiglu,
|
|
26
|
-
fusedSwigluEnabled,
|
|
27
|
-
fusedSwigluSupported,
|
|
28
|
-
swigluMMin,
|
|
29
|
-
swigluUnderCap,
|
|
30
|
-
} from "./fused-swiglu-kernel";
|
|
31
|
-
import { steelLinear } from "./steel-linear-kernel";
|
|
32
|
-
import { fusedMlp } from "./fused-mlp-kernel";
|
|
33
|
-
|
|
34
|
-
/** Break the graph at a boundary: contiguous → eval → fresh graph-free leaf — the
|
|
35
|
-
* exact primitive the segmented TRAINER uses so MLX never plans across boundaries
|
|
36
|
-
* and prior-segment activations are freed (copy of train/segmented.ts detachLeaf). */
|
|
37
|
-
function detachLeaf(a: MlxArray): MlxArray {
|
|
38
|
-
const c = ops.contiguous(a);
|
|
39
|
-
c.eval();
|
|
40
|
-
const leaf = MlxArray.fromBytesCopy(c.rawBytes(), c.shape, c.dtype);
|
|
41
|
-
c.dispose();
|
|
42
|
-
a.dispose();
|
|
43
|
-
return leaf;
|
|
44
|
-
}
|
|
26
|
+
import { CompiledFunction } from "../mlx/compile";
|
|
45
27
|
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
28
|
+
/** Verbatim port of mlx_lm/models/activations.py:
|
|
29
|
+
* `@partial(mx.compile, shapeless=True) def swiglu(gate, x): return nn.silu(gate) * x`
|
|
30
|
+
* mx.compile fuses sigmoid + mul + mul into ONE kernel (mlx-lm's decode
|
|
31
|
+
* `CV2ISigmoid…Multiply`) instead of our three separate dispatches. Traced
|
|
32
|
+
* once, replayed thereafter. Used on the decode (M=1) path only. */
|
|
33
|
+
let _swigluClosure: CompiledFunction | null = null;
|
|
34
|
+
export function compiledSwiglu(gate: MlxArray, up: MlxArray): MlxArray {
|
|
35
|
+
if (!_swigluClosure) {
|
|
36
|
+
_swigluClosure = new CompiledFunction((inputs) => {
|
|
37
|
+
const g = inputs[0]!, u = inputs[1]!; // nn.silu(gate) * x
|
|
38
|
+
const sig = ops.sigmoid(g);
|
|
39
|
+
const silu = ops.mul(g, sig); sig.dispose();
|
|
40
|
+
const out = ops.mul(silu, u); silu.dispose();
|
|
41
|
+
return [out];
|
|
42
|
+
});
|
|
59
43
|
}
|
|
60
|
-
|
|
61
|
-
const y = steelLinear(x2, lin.w, lin.scales, lin.biases, lin.spec);
|
|
62
|
-
if (x2 !== x) x2.dispose();
|
|
63
|
-
return x.shape.length === 2
|
|
64
|
-
? y
|
|
65
|
-
: disposing(y, ops.reshape(y, [...x.shape.slice(0, -1), lin.w.shape[0]!]));
|
|
44
|
+
return _swigluClosure.apply([gate, up])[0]!;
|
|
66
45
|
}
|
|
67
46
|
|
|
68
|
-
/** How often the inference fused path materializes the residual stream (layers). */
|
|
69
|
-
const EVAL_EVERY = Math.max(1, Number(process.env.MLX_BUN_SWIGLU_EVAL_EVERY) || 1);
|
|
70
47
|
import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
|
|
71
48
|
|
|
72
49
|
// Shared prompt-prefix plan (lever 7). When set, the attention ropes the
|
|
@@ -83,6 +60,11 @@ let _prefixPlan: PrefixPlan | null = null;
|
|
|
83
60
|
export function setMiniCpmPrefixPlan(p: PrefixPlan | null): void {
|
|
84
61
|
_prefixPlan = p;
|
|
85
62
|
}
|
|
63
|
+
/** True while a block-wise prefix-shared RoPE plan is active (ORPO forward).
|
|
64
|
+
* The generated debranched fast path must fall back to the monolith then. */
|
|
65
|
+
export function miniCpmPrefixPlanActive(): boolean {
|
|
66
|
+
return _prefixPlan != null;
|
|
67
|
+
}
|
|
86
68
|
|
|
87
69
|
/** Block-wise RoPE for the prefix-shared concat [prompt; chosen; rejected] along
|
|
88
70
|
* the sequence axis (axis 2 of [B,H,T,D]): prompt rotated at offset 0, each
|
|
@@ -108,7 +90,7 @@ function ropeBlocks(x: MlxArray, dims: number, base: number, plan: PrefixPlan):
|
|
|
108
90
|
return out;
|
|
109
91
|
}
|
|
110
92
|
|
|
111
|
-
class LlamaAttention {
|
|
93
|
+
export class LlamaAttention {
|
|
112
94
|
readonly qProj: QuantizedLinear;
|
|
113
95
|
readonly kProj: QuantizedLinear;
|
|
114
96
|
readonly vProj: QuantizedLinear;
|
|
@@ -167,6 +149,17 @@ class LlamaAttention {
|
|
|
167
149
|
attn = quantizedSdpa(q, keys, values, this.scale, mask, cache.groupSize, cache.bits);
|
|
168
150
|
disposeTriple(keys);
|
|
169
151
|
disposeTriple(values);
|
|
152
|
+
} else if (cache instanceof TurboQuantKVCache) {
|
|
153
|
+
// Deferred-V: sdpa runs on rotated values, then one InvFWHT on the
|
|
154
|
+
// output (linear in V) — see TurboQuantKVCache.updateAndFetchDeferredV.
|
|
155
|
+
const [keys, values] = cache.updateAndFetchDeferredV(k, v);
|
|
156
|
+
k.dispose();
|
|
157
|
+
v.dispose();
|
|
158
|
+
const rotated = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
|
|
159
|
+
keys.dispose();
|
|
160
|
+
values.dispose();
|
|
161
|
+
attn = tqUnrotateValues(rotated);
|
|
162
|
+
rotated.dispose();
|
|
170
163
|
} else {
|
|
171
164
|
const [keys, values] = cache.updateAndFetch(k, v);
|
|
172
165
|
k.dispose();
|
|
@@ -193,7 +186,7 @@ class LlamaAttention {
|
|
|
193
186
|
}
|
|
194
187
|
}
|
|
195
188
|
|
|
196
|
-
class LlamaMLP {
|
|
189
|
+
export class LlamaMLP {
|
|
197
190
|
readonly gate: QuantizedLinear;
|
|
198
191
|
readonly up: QuantizedLinear;
|
|
199
192
|
readonly down: QuantizedLinear;
|
|
@@ -204,49 +197,29 @@ class LlamaMLP {
|
|
|
204
197
|
this.down = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
|
|
205
198
|
}
|
|
206
199
|
|
|
207
|
-
/** MLP
|
|
208
|
-
* the kernel computes h + mlp(x) directly — the residual is ABSORBED, so nothing
|
|
209
|
-
* stays live across the kernel for MLX's planner to alias (it just runs our one
|
|
210
|
-
* dispatch). Otherwise returns mlp(x) and the caller adds the residual. */
|
|
200
|
+
/** MLP: returns mlp(x); the caller adds the residual when given. */
|
|
211
201
|
forward(x: MlxArray, residual?: MlxArray): MlxArray {
|
|
212
202
|
const H = x.shape[x.shape.length - 1]!;
|
|
213
203
|
const M = x.shape.reduce((a, b) => a * b, 1) / H;
|
|
214
|
-
if (
|
|
215
|
-
residual != null &&
|
|
216
|
-
fusedSwigluEnabled() && !isCompiledTrace() && M >= swigluMMin() && swigluUnderCap() &&
|
|
217
|
-
this.gate.adapters == null && this.up.adapters == null && this.down.adapters == null &&
|
|
218
|
-
this.down.biases != null && this.down.spec.groupSize % 32 === 0 &&
|
|
219
|
-
fusedSwigluSupported(x, this.gate.spec, this.up.spec, this.gate.biases, this.up.biases)
|
|
220
|
-
) {
|
|
221
|
-
// The WHOLE MLP + residual in ONE kernel — gate+up+silu+down then +h, `hidden`
|
|
222
|
-
// never materialized, residual absorbed. MLX runs one dispatch; nothing for
|
|
223
|
-
// its planner to alias → chains across layers with no boundary.
|
|
224
|
-
const Hin = this.gate.scales.shape[1]! * this.gate.spec.groupSize;
|
|
225
|
-
const Ho = this.down.w.shape[0]!;
|
|
226
|
-
const x2 = x.shape.length === 2 ? x : ops.reshape(x, [M, Hin]);
|
|
227
|
-
const h2 = residual.shape.length === 2 ? residual : ops.reshape(residual, [M, Ho]);
|
|
228
|
-
const out2 = fusedMlp(
|
|
229
|
-
x2,
|
|
230
|
-
this.gate.w, this.gate.scales, this.gate.biases!, this.gate.spec,
|
|
231
|
-
this.up.w, this.up.scales, this.up.biases!, this.up.spec,
|
|
232
|
-
this.down.w, this.down.scales, this.down.biases!, this.down.spec,
|
|
233
|
-
h2,
|
|
234
|
-
);
|
|
235
|
-
if (x2 !== x) x2.dispose();
|
|
236
|
-
if (h2 !== residual) h2.dispose();
|
|
237
|
-
return x.shape.length === 2
|
|
238
|
-
? out2
|
|
239
|
-
: disposing(out2, ops.reshape(out2, [...x.shape.slice(0, -1), Ho]));
|
|
240
|
-
}
|
|
241
204
|
const gate = this.gate.forward(x);
|
|
242
205
|
const up = this.up.forward(x);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
206
|
+
// Decode (M=1): fuse silu·up into one kernel (mlx_lm activations.py swiglu).
|
|
207
|
+
// Plain sigmoid+mul+mul otherwise (M>1 prefill/training, or inside a compile
|
|
208
|
+
// trace) — identical math either way. MLX_BUN_COMPILED_SWIGLU=0 disables.
|
|
209
|
+
let hidden: MlxArray;
|
|
210
|
+
if (M === 1 && !isCompiledTrace() && process.env.MLX_BUN_COMPILED_SWIGLU !== "0") {
|
|
211
|
+
hidden = compiledSwiglu(gate, up);
|
|
212
|
+
gate.dispose();
|
|
213
|
+
up.dispose();
|
|
214
|
+
} else {
|
|
215
|
+
const sig = ops.sigmoid(gate);
|
|
216
|
+
const silu = ops.mul(gate, sig);
|
|
217
|
+
gate.dispose();
|
|
218
|
+
sig.dispose();
|
|
219
|
+
hidden = ops.mul(silu, up);
|
|
220
|
+
silu.dispose();
|
|
221
|
+
up.dispose();
|
|
222
|
+
}
|
|
250
223
|
const m = this.down.forward(hidden);
|
|
251
224
|
hidden.dispose();
|
|
252
225
|
if (residual == null) return m;
|
|
@@ -254,7 +227,7 @@ class LlamaMLP {
|
|
|
254
227
|
}
|
|
255
228
|
}
|
|
256
229
|
|
|
257
|
-
class LlamaLayer {
|
|
230
|
+
export class LlamaLayer {
|
|
258
231
|
readonly attn: LlamaAttention;
|
|
259
232
|
readonly mlp: LlamaMLP;
|
|
260
233
|
readonly inputNorm: RMSNorm;
|
|
@@ -337,15 +310,7 @@ export class MiniCPM5Model {
|
|
|
337
310
|
}
|
|
338
311
|
|
|
339
312
|
protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
|
|
340
|
-
|
|
341
|
-
// each layer's output so the custom kernel sits in a shallow per-layer graph
|
|
342
|
-
// rather than a 24-deep lazy graph whose buffer planner aliases the kernel's
|
|
343
|
-
// output (the deep-graph corruption). Training/segmented-backward call
|
|
344
|
-
// runLayerRange directly and never set this, so autograd is untouched.
|
|
345
|
-
const h = this.runLayerRange(
|
|
346
|
-
h0, 0, this.layers.length, cache,
|
|
347
|
-
fusedSwigluEnabled() && process.env.MLX_BUN_SWIGLU_NOEVAL !== "1",
|
|
348
|
-
);
|
|
313
|
+
const h = this.runLayerRange(h0, 0, this.layers.length, cache);
|
|
349
314
|
h0.dispose(); // runLayerRange keeps its input alive; we own h0 here
|
|
350
315
|
return disposing(h, this.finalNorm.forward(h));
|
|
351
316
|
}
|
|
@@ -358,7 +323,7 @@ export class MiniCPM5Model {
|
|
|
358
323
|
* segmented-backward training path (docs/design/segmented-backward-training.md).
|
|
359
324
|
* MiniCPM5 is plain full-attention with no KV-sharing, so each layer reads
|
|
360
325
|
* its own stateless `cache[i]` and a single causal mask. */
|
|
361
|
-
runLayerRange(h: MlxArray, aIdx: number, bIdx: number, cache: Cache[]
|
|
326
|
+
runLayerRange(h: MlxArray, aIdx: number, bIdx: number, cache: Cache[]): MlxArray {
|
|
362
327
|
if (aIdx >= bIdx) return h;
|
|
363
328
|
const L = h.shape[1]!;
|
|
364
329
|
const mask = cache[aIdx]!.makeMask(L, null);
|
|
@@ -367,15 +332,6 @@ export class MiniCPM5Model {
|
|
|
367
332
|
const next = this.layers[i]!.forward(cur, mask, cache[i]!);
|
|
368
333
|
if (i > aIdx) cur.dispose(); // keep the input boundary leaf; drop interiors
|
|
369
334
|
cur = next;
|
|
370
|
-
// Per-layer boundary (segmented-trainer pattern): materialize each layer's
|
|
371
|
-
// output so MLX never plans across layers — our single-dispatch fused MLP is
|
|
372
|
-
// the only custom kernel it runs per layer. evalAll (GPU sync, no host copy)
|
|
373
|
-
// suffices for one kernel/boundary; MLX_BUN_SWIGLU_DETACH=1 forces the
|
|
374
|
-
// stronger graph-breaking detachLeaf copy.
|
|
375
|
-
if (evalPerLayer) {
|
|
376
|
-
if (process.env.MLX_BUN_SWIGLU_DETACH === "1") cur = detachLeaf(cur);
|
|
377
|
-
else ops.evalAll([cur]);
|
|
378
|
-
}
|
|
379
335
|
}
|
|
380
336
|
mask.arr?.dispose();
|
|
381
337
|
return cur;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Paged KV cache — vLLM-style block-pool storage behind the standard Cache
|
|
2
|
+
// interface (docs/design/paged-kv-cache.md). OPTIONAL and default-off
|
|
3
|
+
// (`--paged-kv` / GenerateOptions.pagedKv); v1 scope is serial batch=1
|
|
4
|
+
// decode on Gemma4-family plain full-attention layers, bf16 only.
|
|
5
|
+
//
|
|
6
|
+
// Storage: K/V live in fixed-size per-layer pool tensors
|
|
7
|
+
// [numBlocks, H_kv, blockSize, headDim]; a host-side block table maps the
|
|
8
|
+
// sequence's logical block order to physical pool slots. Writes go through
|
|
9
|
+
// ops.sliceUpdate into the tail block; reads gather the occupied blocks
|
|
10
|
+
// back into ONE contiguous [1, H, S, D] pair (ops.takeAxis over the pool's
|
|
11
|
+
// block axis + transpose + reshape) and hand that to the unchanged
|
|
12
|
+
// ops.sdpa call site — identical bytes to what a plain KVCache would have
|
|
13
|
+
// fetched, so the paged path is gated bit-exact, not KL-tolerated
|
|
14
|
+
// (tests/paged-kv-parity.test.ts). The gather IS the cost: a full K/V copy
|
|
15
|
+
// per step, pure bandwidth tax at batch=1 — v1 ships the abstraction for
|
|
16
|
+
// the batched/CoW follow-ups, not a speed win (see the design doc's
|
|
17
|
+
// Motivation, which says this honestly).
|
|
18
|
+
//
|
|
19
|
+
// Deliberately NOT a KVCache subclass (the TurboQuantKVCache reasoning):
|
|
20
|
+
// every instanceof gate in the tree — CompiledDecode.supports, the batch
|
|
21
|
+
// gateway's #modelCachesBatchable, generated forwards' #matches() — must
|
|
22
|
+
// EXCLUDE paged caches and fall back to the monolith/uncompiled path.
|
|
23
|
+
|
|
24
|
+
import { MlxArray } from "../mlx/array";
|
|
25
|
+
import { Dtype } from "../mlx/ffi";
|
|
26
|
+
import * as ops from "../mlx/ops";
|
|
27
|
+
import { createCausalMask, type Cache, type Mask } from "./gemma4-base";
|
|
28
|
+
|
|
29
|
+
/** Typed pool-exhaustion error: a generation outgrew its pool. Sizing from
|
|
30
|
+
* prompt+maxTokens at construction makes this unreachable in practice;
|
|
31
|
+
* it exists so an accounting bug is a clear throw, never silent
|
|
32
|
+
* truncation or an allocator OOM (the byte-capped-not-count-capped
|
|
33
|
+
* lesson). */
|
|
34
|
+
export class PagedPoolExhausted extends Error {
|
|
35
|
+
constructor(numBlocks: number) {
|
|
36
|
+
super(`paged KV pool exhausted (${numBlocks} blocks) — capacity accounting bug`);
|
|
37
|
+
this.name = "PagedPoolExhausted";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Per-layer block arena. One K and one V pool tensor, mlx-owned
|
|
42
|
+
* (ops.zeros — no host-pointer alignment/dtor hazards), shaped
|
|
43
|
+
* [numBlocks, H, blockSize, D]. alloc()/free() manage physical slot
|
|
44
|
+
* indices; the owning PagedKVCache writes/gathers through the pool. */
|
|
45
|
+
export class BlockPool {
|
|
46
|
+
keys: MlxArray;
|
|
47
|
+
values: MlxArray;
|
|
48
|
+
readonly numBlocks: number;
|
|
49
|
+
readonly blockSize: number;
|
|
50
|
+
#free: number[];
|
|
51
|
+
|
|
52
|
+
constructor(opts: {
|
|
53
|
+
numBlocks: number;
|
|
54
|
+
blockSize: number;
|
|
55
|
+
numKvHeads: number;
|
|
56
|
+
headDim: number;
|
|
57
|
+
/** V head dim when it differs from K's (attention_k_eq_v models don't
|
|
58
|
+
* reach v1's gemma4 scope, but the pool stays shape-honest). */
|
|
59
|
+
vHeadDim?: number;
|
|
60
|
+
dtype: Dtype;
|
|
61
|
+
}) {
|
|
62
|
+
this.numBlocks = opts.numBlocks;
|
|
63
|
+
this.blockSize = opts.blockSize;
|
|
64
|
+
this.keys = ops.zeros(
|
|
65
|
+
[opts.numBlocks, opts.numKvHeads, opts.blockSize, opts.headDim], opts.dtype);
|
|
66
|
+
this.values = ops.zeros(
|
|
67
|
+
[opts.numBlocks, opts.numKvHeads, opts.blockSize, opts.vHeadDim ?? opts.headDim],
|
|
68
|
+
opts.dtype);
|
|
69
|
+
// LIFO free list, low indices first — deterministic layout for tests.
|
|
70
|
+
this.#free = Array.from({ length: opts.numBlocks }, (_, i) => opts.numBlocks - 1 - i);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get freeBlocks(): number {
|
|
74
|
+
return this.#free.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
alloc(): number {
|
|
78
|
+
const idx = this.#free.pop();
|
|
79
|
+
if (idx === undefined) throw new PagedPoolExhausted(this.numBlocks);
|
|
80
|
+
return idx;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
free(physIdx: number): void {
|
|
84
|
+
this.#free.push(physIdx);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Write `k`/`v` ([1, H, l, D], l ≤ blockSize − within) into physical
|
|
88
|
+
* block `physIdx` starting at row `within`. Functional slice_update:
|
|
89
|
+
* the pool tensors are single-referenced here, so mlx donates the
|
|
90
|
+
* buffer and the write is in-place in the steady state. */
|
|
91
|
+
writeBlock(physIdx: number, k: MlxArray, v: MlxArray, within: number): void {
|
|
92
|
+
const l = k.shape[2]!;
|
|
93
|
+
const [, H, , kD] = this.keys.shape as [number, number, number, number];
|
|
94
|
+
const vD = this.values.shape[3]!;
|
|
95
|
+
const k2 = ops.sliceUpdate(
|
|
96
|
+
this.keys, k, [physIdx, 0, within, 0], [physIdx + 1, H, within + l, kD]);
|
|
97
|
+
this.keys.dispose();
|
|
98
|
+
this.keys = k2;
|
|
99
|
+
const v2 = ops.sliceUpdate(
|
|
100
|
+
this.values, v, [physIdx, 0, within, 0], [physIdx + 1, H, within + l, vD]);
|
|
101
|
+
this.values.dispose();
|
|
102
|
+
this.values = v2;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Gather `blockTable`'s blocks in logical order into one contiguous
|
|
106
|
+
* [1, H, nb·blockSize, D] pair (the fetch copy). Caller owns both. */
|
|
107
|
+
gather(blockTable: number[]): [MlxArray, MlxArray] {
|
|
108
|
+
const idx = MlxArray.fromInt32(Int32Array.from(blockTable), [blockTable.length]);
|
|
109
|
+
const pick = (pool: MlxArray): MlxArray => {
|
|
110
|
+
const took = ops.takeAxis(pool, idx, 0); // [nb, H, bs, D]
|
|
111
|
+
const [nb, H, bs, D] = took.shape as [number, number, number, number];
|
|
112
|
+
const t = ops.transposeAxes(took, [1, 0, 2, 3]); // [H, nb, bs, D] (view)
|
|
113
|
+
took.dispose();
|
|
114
|
+
const out = ops.reshape(t, [1, H, nb * bs, D]); // materializes seq order
|
|
115
|
+
t.dispose();
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
const k = pick(this.keys);
|
|
119
|
+
const v = pick(this.values);
|
|
120
|
+
idx.dispose();
|
|
121
|
+
return [k, v];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
dispose(): void {
|
|
125
|
+
this.keys.dispose();
|
|
126
|
+
this.values.dispose();
|
|
127
|
+
this.#free = [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Cache implementation over a BlockPool. Same values as a plain KVCache
|
|
132
|
+
* at every step (parity-gated); only the physical arrangement differs.
|
|
133
|
+
* The pool allocates lazily on the first write (KVCache's lazy-alloc
|
|
134
|
+
* shape): head count / head dims / dtype come from the first k/v pair,
|
|
135
|
+
* so the wiring (maybePageKv) needs no per-model shape plumbing. */
|
|
136
|
+
export class PagedKVCache implements Cache {
|
|
137
|
+
/** Matches KVCache.STEP: v1's growth granularity is a permutation of
|
|
138
|
+
* today's 256-token step into fixed reusable slots, not a new tuning
|
|
139
|
+
* axis (--paged-kv-block-size overrides for experiments). */
|
|
140
|
+
static readonly DEFAULT_BLOCK_SIZE = 256;
|
|
141
|
+
/** Compiled-decode trace adapters never wrap paged caches (see Cache);
|
|
142
|
+
* always unset here. */
|
|
143
|
+
readonly ropeOffsetArr?: MlxArray;
|
|
144
|
+
offset = 0;
|
|
145
|
+
/** Logical block order → physical pool slot. */
|
|
146
|
+
blockTable: number[] = [];
|
|
147
|
+
pool: BlockPool | null = null;
|
|
148
|
+
|
|
149
|
+
constructor(
|
|
150
|
+
/** Tokens this cache must be able to hold (prompt + maxTokens). */
|
|
151
|
+
readonly capacityTokens: number,
|
|
152
|
+
readonly blockSize: number,
|
|
153
|
+
) {}
|
|
154
|
+
|
|
155
|
+
get #blockSize(): number {
|
|
156
|
+
return this.blockSize;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Blocks needed to hold `n` tokens. */
|
|
160
|
+
#blocksFor(n: number): number {
|
|
161
|
+
return Math.ceil(n / this.#blockSize);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
165
|
+
const L = k.shape[2]!;
|
|
166
|
+
const [, H, , kD] = k.shape as [number, number, number, number];
|
|
167
|
+
const vD = v.shape[3]!;
|
|
168
|
+
if (!this.pool)
|
|
169
|
+
this.pool = new BlockPool({
|
|
170
|
+
numBlocks: poolBlocksFor(this.capacityTokens, this.blockSize),
|
|
171
|
+
blockSize: this.blockSize,
|
|
172
|
+
numKvHeads: H,
|
|
173
|
+
headDim: kD,
|
|
174
|
+
vHeadDim: vD,
|
|
175
|
+
dtype: k.dtype,
|
|
176
|
+
});
|
|
177
|
+
// Write, splitting the incoming L along block boundaries. Blocks past
|
|
178
|
+
// the current tail allocate from the free list as they're reached.
|
|
179
|
+
let written = 0;
|
|
180
|
+
while (written < L) {
|
|
181
|
+
const pos = this.offset + written;
|
|
182
|
+
const bi = Math.floor(pos / this.#blockSize);
|
|
183
|
+
const within = pos % this.#blockSize;
|
|
184
|
+
while (this.blockTable.length <= bi) this.blockTable.push(this.pool.alloc());
|
|
185
|
+
const l = Math.min(this.#blockSize - within, L - written);
|
|
186
|
+
// Full-range pieces skip the slice (fresh-view slice would be a
|
|
187
|
+
// gratuitous op); partial pieces slice the [written, written+l) rows.
|
|
188
|
+
if (l === L && written === 0) {
|
|
189
|
+
this.pool.writeBlock(this.blockTable[bi]!, k, v, within);
|
|
190
|
+
} else {
|
|
191
|
+
const kp = k.slice([0, 0, written, 0], [1, H, written + l, kD]);
|
|
192
|
+
const vp = v.slice([0, 0, written, 0], [1, H, written + l, vD]);
|
|
193
|
+
this.pool.writeBlock(this.blockTable[bi]!, kp, vp, within);
|
|
194
|
+
kp.dispose();
|
|
195
|
+
vp.dispose();
|
|
196
|
+
}
|
|
197
|
+
written += l;
|
|
198
|
+
}
|
|
199
|
+
this.offset += L;
|
|
200
|
+
|
|
201
|
+
// Fetch: gather occupied blocks contiguous, slice to the live prefix.
|
|
202
|
+
const nb = this.#blocksFor(this.offset);
|
|
203
|
+
const [gk, gv] = this.pool.gather(this.blockTable.slice(0, nb));
|
|
204
|
+
const keys = gk.slice([0, 0, 0, 0], [1, H, this.offset, kD]);
|
|
205
|
+
const values = gv.slice([0, 0, 0, 0], [1, H, this.offset, vD]);
|
|
206
|
+
gk.dispose();
|
|
207
|
+
gv.dispose();
|
|
208
|
+
return [keys, values];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Same mask policy as KVCache.makeMask (it reads only offset). */
|
|
212
|
+
makeMask(N: number, windowSize: number | null): Mask {
|
|
213
|
+
if (N === 1) return { mode: "", arr: null };
|
|
214
|
+
if (windowSize === null) return { mode: "causal", arr: null };
|
|
215
|
+
if (this.offset === 0 && N <= windowSize) return { mode: "causal", arr: null };
|
|
216
|
+
return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Pool tensors — what a prefill-chunk boundary must materialize.
|
|
220
|
+
* NOT prompt-cache-compatible in v1 (cloneKvCaches never sees paged
|
|
221
|
+
* caches; the serve lane bypasses take/put for paged requests). */
|
|
222
|
+
state(): MlxArray[] {
|
|
223
|
+
return this.pool ? [this.pool.keys, this.pool.values] : [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
isTrimmable(): boolean {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Rewind the last n tokens and return now-unoccupied tail blocks to
|
|
231
|
+
* the free list. Stale bytes past offset are never read (fetch slices
|
|
232
|
+
* to offset; writes cover exact ranges) — the KVCache padding
|
|
233
|
+
* invariant, block-shaped. */
|
|
234
|
+
trim(n: number): void {
|
|
235
|
+
this.offset = Math.max(0, this.offset - n);
|
|
236
|
+
const need = this.#blocksFor(this.offset);
|
|
237
|
+
while (this.blockTable.length > need) this.pool!.free(this.blockTable.pop()!);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
dispose(): void {
|
|
241
|
+
// The cache owns its pool in v1 (one sequence per pool).
|
|
242
|
+
this.pool?.dispose();
|
|
243
|
+
this.pool = null;
|
|
244
|
+
this.blockTable = [];
|
|
245
|
+
this.offset = 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Size a pool for one request: capacity tokens rounded up to whole
|
|
250
|
+
* blocks. Exported for the wiring in generate.ts (maybePageKv). */
|
|
251
|
+
export function poolBlocksFor(capacityTokens: number, blockSize: number): number {
|
|
252
|
+
return Math.max(1, Math.ceil(capacityTokens / blockSize));
|
|
253
|
+
}
|
package/src/model/qwen3-delta.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { MlxArray } from "../mlx/array";
|
|
23
23
|
import { Dtype } from "../mlx/ffi";
|
|
24
24
|
import { MetalKernel } from "../mlx/metal-kernel";
|
|
25
|
+
import { CompiledFunction } from "../mlx/compile";
|
|
25
26
|
import * as ops from "../mlx/ops";
|
|
26
27
|
import type { Cache, Mask } from "./gemma4-base";
|
|
27
28
|
|
|
@@ -118,21 +119,33 @@ function getKernel(): MetalKernel {
|
|
|
118
119
|
/** compute_g: exp(-exp(A_log_f32) * softplus(a + dt_bias)). Output is f32
|
|
119
120
|
* (the f32 exp(A_log) promotes the bf16 softplus term) — the kernel reads g
|
|
120
121
|
* as float, so this dtype is load-bearing. */
|
|
122
|
+
let _computeGFn: CompiledFunction | null = null;
|
|
121
123
|
export function computeG(aLog: MlxArray, a: MlxArray, dtBias: MlxArray): MlxArray {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
124
|
+
// mlx-lm gated_delta.py: `@partial(mx.compile, shapeless=True) compute_g` — the
|
|
125
|
+
// WHOLE chain (exp/neg/softplus/mul/exp) is ONE fused kernel there, not five
|
|
126
|
+
// separate dispatches. Traced once, replayed.
|
|
127
|
+
if (!_computeGFn) {
|
|
128
|
+
_computeGFn = new CompiledFunction((inp) => {
|
|
129
|
+
const aL = inp[0]!, aa = inp[1]!, dt = inp[2]!;
|
|
130
|
+
const aLogF = aL.astype(Dtype.float32);
|
|
131
|
+
const expA = ops.exp(aLogF); aLogF.dispose();
|
|
132
|
+
const negExpA = ops.neg(expA); expA.dispose();
|
|
133
|
+
const adt = ops.add(aa, dt);
|
|
134
|
+
// mlx nn.softplus(x) = mx.logaddexp(x, 0), where 0 is a weak Python int
|
|
135
|
+
// (no cast node). ops.softplus builds the 0 via scalarLike (fromFloat32 →
|
|
136
|
+
// f32→bf16 AsType). Build the 0 at adt's dtype from raw bytes so there is
|
|
137
|
+
// no cast — matching mlx's graph exactly.
|
|
138
|
+
const zbytes = adt.dtype === Dtype.float32
|
|
139
|
+
? new Uint8Array([0, 0, 0, 0])
|
|
140
|
+
: new Uint8Array([0, 0]);
|
|
141
|
+
const zero = MlxArray.fromBytesCopy(zbytes, [], adt.dtype);
|
|
142
|
+
const sp = ops.logaddexp(adt, zero); adt.dispose(); zero.dispose();
|
|
143
|
+
const prod = ops.mul(negExpA, sp); negExpA.dispose(); sp.dispose();
|
|
144
|
+
const g = ops.exp(prod); prod.dispose();
|
|
145
|
+
return [g];
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return _computeGFn.apply([aLog, a, dtBias])[0]!;
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
/** gated_delta_update (use_kernel path): returns [y, newState].
|
|
@@ -185,6 +198,19 @@ export class SSMCache implements Cache {
|
|
|
185
198
|
conv: MlxArray | null = null;
|
|
186
199
|
recurrent: MlxArray | null = null;
|
|
187
200
|
offset = 0;
|
|
201
|
+
/** Per-row token coverage (batch lane only; null on serial B=1 caches,
|
|
202
|
+
* where `offset` IS the row's count). mlx-lm's ArraysCache tracks no
|
|
203
|
+
* offset at all (cache.py:594-722) — per-sequence coverage is the
|
|
204
|
+
* server's bookkeeping; ours lives here because prompt-cache put()
|
|
205
|
+
* keys on EXACT coverage (#putOrDispose offset === tokens.length) and
|
|
206
|
+
* merged rows carry different totals. Seeded by mergeRows, advanced in
|
|
207
|
+
* lockstep by advance(), row-filtered by filter(). */
|
|
208
|
+
offsets: number[] | null = null;
|
|
209
|
+
|
|
210
|
+
/** Row `i`'s own token coverage (per-row when batched, `offset` serial). */
|
|
211
|
+
rowOffset(i: number): number {
|
|
212
|
+
return this.offsets ? this.offsets[i]! : this.offset;
|
|
213
|
+
}
|
|
188
214
|
|
|
189
215
|
/** Linear-attn layers don't go through the KV update path. */
|
|
190
216
|
updateAndFetch(): [MlxArray, MlxArray] {
|
|
@@ -201,6 +227,10 @@ export class SSMCache implements Cache {
|
|
|
201
227
|
|
|
202
228
|
advance(n: number): void {
|
|
203
229
|
this.offset += n;
|
|
230
|
+
// Batched rows step together (one forward advances every row), so the
|
|
231
|
+
// per-row counts move in lockstep; they DIFFER only in their merge-time
|
|
232
|
+
// seeds (each row's prompt length).
|
|
233
|
+
if (this.offsets) for (let i = 0; i < this.offsets.length; i++) this.offsets[i]! += n;
|
|
204
234
|
}
|
|
205
235
|
|
|
206
236
|
state(): MlxArray[] {
|
|
@@ -230,7 +260,8 @@ export class SSMCache implements Cache {
|
|
|
230
260
|
// left-padding, so the batched twins of mergeKVRows/filterKVRows collapse
|
|
231
261
|
// to B-axis concat/take. `offset` is bookkeeping only for SSM layers (RoPE
|
|
232
262
|
// reads the FULL-attention caches; the linear-attn forward never reads it),
|
|
233
|
-
// so merged rows carrying different token counts share the max
|
|
263
|
+
// so merged rows carrying different token counts share the max; the exact
|
|
264
|
+
// per-row counts live in `offsets` (extraction's coverage key).
|
|
234
265
|
|
|
235
266
|
/** Merge a fully-prefilled solo row into a running batched cache (`prev`
|
|
236
267
|
* null ⇒ the solo row becomes the batch). The returned cache owns fresh
|
|
@@ -253,6 +284,9 @@ export class SSMCache implements Cache {
|
|
|
253
284
|
solo.recurrent = null;
|
|
254
285
|
}
|
|
255
286
|
out.offset = Math.max(prev?.offset ?? 0, solo.offset);
|
|
287
|
+
// Seed per-row coverage: an adopted serial `prev` (offsets null) is one
|
|
288
|
+
// row whose exact count is its scalar offset.
|
|
289
|
+
out.offsets = [...(prev ? prev.offsets ?? [prev.offset] : []), solo.offset];
|
|
256
290
|
return out;
|
|
257
291
|
}
|
|
258
292
|
|
|
@@ -267,5 +301,32 @@ export class SSMCache implements Cache {
|
|
|
267
301
|
this.recurrent.dispose();
|
|
268
302
|
this.conv = conv;
|
|
269
303
|
this.recurrent = recurrent;
|
|
304
|
+
if (this.offsets) this.offsets = keep.map((i) => this.offsets![i]!);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Row `i` as a fresh SERIAL cache — port of mlx-lm ArraysCache.extract
|
|
308
|
+
* (cache.py:673-676: `cache.cache = [c[idx : idx + 1] for c in self.cache]`,
|
|
309
|
+
* a B-axis slice of every state slot). Ours are OWNED contiguous copies
|
|
310
|
+
* (the entry outlives the batch; the batched buffers must free when the
|
|
311
|
+
* batch moves on — same rule as extractKVRow, batched-mask.ts). Offset =
|
|
312
|
+
* the row's OWN coverage, not the shared max. */
|
|
313
|
+
extractRow(i: number): SSMCache {
|
|
314
|
+
if (!this.conv || !this.recurrent)
|
|
315
|
+
throw new Error("SSMCache.extractRow: empty cache");
|
|
316
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
317
|
+
const lo = a.shape.map(() => 0);
|
|
318
|
+
lo[0] = i;
|
|
319
|
+
const hi = [...a.shape];
|
|
320
|
+
hi[0] = i + 1;
|
|
321
|
+
const view = a.slice(lo, hi);
|
|
322
|
+
const own = ops.contiguous(view);
|
|
323
|
+
view.dispose();
|
|
324
|
+
return own;
|
|
325
|
+
};
|
|
326
|
+
const out = new SSMCache();
|
|
327
|
+
out.conv = cut(this.conv);
|
|
328
|
+
out.recurrent = cut(this.recurrent);
|
|
329
|
+
out.offset = this.rowOffset(i);
|
|
330
|
+
return out;
|
|
270
331
|
}
|
|
271
332
|
}
|