mlx-bun 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/README.md +4 -1
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/design/batching.md +1321 -49
- package/docs/design/decode-speed-program.md +331 -1163
- package/docs/design/generic-model-support.md +79 -3
- package/docs/design/kv-cache.md +198 -54
- package/docs/design/speculative-decoding.md +387 -24
- package/docs/design/turboquant.md +123 -13
- package/docs/design/unified-engine-frontier-plan.md +144 -142
- package/docs/planning/PRODUCT_ROADMAP.md +16 -0
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/reference/benchmarks.md +2457 -6
- package/docs/reference/cli.md +41 -2
- package/docs/reference/distribution.md +16 -9
- package/docs/reference/environment.md +55 -5
- package/docs/reference/models.md +13 -4
- package/docs/reference/server-api.md +79 -25
- package/docs/reference/server-config.md +178 -84
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +1 -1
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/batch-group.ts +368 -224
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +106 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +77 -18
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/model-serving.ts +1 -1
- package/src/backends/mlx/prefill-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +41 -3
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +35 -66
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
- package/src/cli.ts +11 -36
- package/src/contracts/execution.ts +11 -0
- package/src/contracts/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +18 -0
- package/src/engine/execution-plan.ts +18 -16
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +10 -5
- package/src/fit.ts +29 -15
- package/src/generate.ts +77 -173
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +70 -12
- package/src/kv-store.ts +70 -16
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/lab/paged-kv/paged-kv.ts +25 -11
- package/src/mlx/array.ts +2 -1
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +93 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +55 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +162 -80
- package/src/model/gemma4.ts +34 -26
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +67 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5.ts +82 -42
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +42 -44
- package/src/sampler.ts +36 -25
- package/src/serve/chat-stage.ts +2 -2
- package/src/serve/generation-gateway.ts +39 -30
- package/src/serve/model-host.ts +3 -4
- package/src/serve/request-plan.ts +4 -3
- package/src/serve/request-prep.ts +2 -4
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/server.ts +64 -108
- package/src/spec/assistant-rows.ts +92 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +53 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +92 -20
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +219 -0
- package/src/spec/qwen-mtp-source.ts +81 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +70 -68
- package/src/spec/source.ts +80 -15
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +43 -101
- package/src/ssd-cache.ts +33 -21
- package/src/ssd-durability.ts +11 -7
- package/src/web/app.js +57 -57
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
|
@@ -74,6 +74,10 @@ export interface DflashTrainOut { draftLogits: MlxArray; conf: MlxArray }
|
|
|
74
74
|
* positions — ABSENT when the caller passed collectLogits:false (the loop
|
|
75
75
|
* never materialized/concatenated the per-position logits). */
|
|
76
76
|
export interface DflashDraftBlock { tokens: number[]; conf: number[]; draftLogits?: MlxArray }
|
|
77
|
+
export interface DflashDraftRows { tokens: number[][]; conf: number[][]; draftLogits?: MlxArray; }
|
|
78
|
+
export interface DflashContextAttention {
|
|
79
|
+
attend(layer: number, query: MlxArray, keys: MlxArray, values: MlxArray, scale: number): MlxArray;
|
|
80
|
+
}
|
|
77
81
|
export interface DflashDraftOpts {
|
|
78
82
|
sample?: DSparkSampleConfig;
|
|
79
83
|
keys?: KeyStream;
|
|
@@ -92,6 +96,8 @@ export interface DflashDraftOpts {
|
|
|
92
96
|
* path runs its own verify lm-head and never reads it — pass false there
|
|
93
97
|
* to skip the dead concat + a host-unrelated GPU alloc every round. */
|
|
94
98
|
collectLogits?: boolean;
|
|
99
|
+
/** Return confidence telemetry; pruning still computes it when needed. */
|
|
100
|
+
collectConfidence?: boolean;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
const CDT = Dtype.float32;
|
|
@@ -245,7 +251,7 @@ export class DflashDrafter {
|
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
// --- Eq 3: one KV-injection layer. H_ctx read-only; H_d is queries+updated ---
|
|
248
|
-
#layer(i: number, H_d: MlxArray, H_ctx: MlxArray, maskBias: MlxArray | null, A: number, Lctx: number): MlxArray {
|
|
254
|
+
#layer(i: number, H_d: MlxArray, H_ctx: MlxArray, maskBias: MlxArray | null, A: number, Lctx: number, context?: DflashContextAttention): MlxArray {
|
|
249
255
|
const { dDraft, nHeads, gamma } = this.cfg;
|
|
250
256
|
const hd = dDraft / nHeads;
|
|
251
257
|
const eps = this.dims.eps;
|
|
@@ -253,24 +259,34 @@ export class DflashDrafter {
|
|
|
253
259
|
|
|
254
260
|
const residual = H_d;
|
|
255
261
|
const nd = ops.rmsNorm(H_d, this.get(`bb.${i}.attn_norm`), eps);
|
|
256
|
-
const nc = ops.rmsNorm(H_ctx, this.get(`bb.${i}.attn_norm`), eps); // SAME norm as block
|
|
262
|
+
const nc = context ? null : ops.rmsNorm(H_ctx, this.get(`bb.${i}.attn_norm`), eps); // SAME norm as block
|
|
257
263
|
|
|
258
264
|
// Q from block only
|
|
259
265
|
let q = ops.matmul(nd, this.get(`bb.${i}.q`)); // [A,γ,d]
|
|
260
266
|
q = disposing(q, ops.reshape(q, [A, gamma, nHeads, hd]));
|
|
261
267
|
q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3])); // [A,nHeads,γ,hd]
|
|
262
268
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
const kd = ops.matmul(nd, wk), vd = ops.matmul(nd, wv); nd.dispose();
|
|
270
|
+
let attn: MlxArray;
|
|
271
|
+
if (context) {
|
|
272
|
+
const shape = (array: MlxArray) => {
|
|
273
|
+
using rows = ops.reshape(array, [A, gamma, nHeads, hd]);
|
|
274
|
+
return ops.transposeAxes(rows, [0, 2, 1, 3]);
|
|
275
|
+
};
|
|
276
|
+
using keys = shape(kd), values = shape(vd); kd.dispose(); vd.dispose();
|
|
277
|
+
attn = context.attend(i, q, keys, values, Math.pow(hd, -0.5));
|
|
278
|
+
} else {
|
|
279
|
+
const kc = ops.matmul(nc!, wk), vc = ops.matmul(nc!, wv); nc!.dispose();
|
|
280
|
+
const shape = (array: MlxArray) => {
|
|
281
|
+
using rows = ops.reshape(array, [A, Lctx + gamma, nHeads, hd]);
|
|
282
|
+
return ops.transposeAxes(rows, [0, 2, 1, 3]);
|
|
283
|
+
};
|
|
284
|
+
using k = ops.concatAxis([kc, kd], 1), v = ops.concatAxis([vc, vd], 1);
|
|
285
|
+
kc.dispose(); kd.dispose(); vc.dispose(); vd.dispose();
|
|
286
|
+
using keys = shape(k), values = shape(v);
|
|
287
|
+
attn = ops.sdpa(q, keys, values, Math.pow(hd, -0.5), maskBias ? "array" : "", maskBias);
|
|
288
|
+
}
|
|
289
|
+
q.dispose();
|
|
274
290
|
attn = disposing(attn, ops.transposeAxes(attn, [0, 2, 1, 3])); // [A,γ,nHeads,hd]
|
|
275
291
|
attn = disposing(attn, ops.reshape(attn, [A, gamma, dDraft]));
|
|
276
292
|
attn = disposing(attn, ops.matmul(attn, this.get(`bb.${i}.o`)));
|
|
@@ -290,17 +306,35 @@ export class DflashDrafter {
|
|
|
290
306
|
}
|
|
291
307
|
|
|
292
308
|
/** hCtx [A,Lctx,m*H], anchorEmb [A,H], ctxMask [A,Lctx]|null → block [A,γ,d]. */
|
|
293
|
-
#backbone(hCtx: MlxArray, anchorEmb: MlxArray, ctxMask: MlxArray | null, A: number): MlxArray {
|
|
294
|
-
const Lctx = hCtx
|
|
295
|
-
const H_ctx = this.#buildContext(hCtx);
|
|
309
|
+
#backbone(hCtx: MlxArray | null, anchorEmb: MlxArray, ctxMask: MlxArray | null, A: number, context?: DflashContextAttention): MlxArray {
|
|
310
|
+
const Lctx = hCtx?.shape[1] ?? 0;
|
|
311
|
+
const H_ctx = context ? null : this.#buildContext(hCtx!);
|
|
296
312
|
let H_d = this.#buildBlock(anchorEmb, A);
|
|
297
313
|
const maskBias = this.#maskBias(ctxMask, A, Lctx);
|
|
298
|
-
for (let i = 0; i < this.cfg.nLayers; i++) H_d = disposing(H_d, this.#layer(i, H_d, H_ctx
|
|
299
|
-
H_ctx
|
|
314
|
+
for (let i = 0; i < this.cfg.nLayers; i++) H_d = disposing(H_d, this.#layer(i, H_d, H_ctx!, maskBias, A, Lctx, context));
|
|
315
|
+
H_ctx?.dispose(); maskBias?.dispose();
|
|
300
316
|
H_d = disposing(H_d, ops.rmsNorm(H_d, this.get("out_norm"), this.dims.eps));
|
|
301
317
|
return H_d;
|
|
302
318
|
}
|
|
303
319
|
|
|
320
|
+
/** Context projection is independent of the block and can be retained. */
|
|
321
|
+
projectContextRows(hidden: MlxArray): { k: MlxArray; v: MlxArray }[] {
|
|
322
|
+
const { nHeads, dDraft } = this.cfg, B = hidden.shape[0]!, N = hidden.shape[1]!;
|
|
323
|
+
using projected = this.#buildContext(hidden);
|
|
324
|
+
using pending = new DisposableStack();
|
|
325
|
+
const pairs = Array.from({ length: this.cfg.nLayers }, (_, layer) => {
|
|
326
|
+
using normalized = ops.rmsNorm(projected, this.get(`bb.${layer}.attn_norm`), this.dims.eps);
|
|
327
|
+
const plane = (name: string) => {
|
|
328
|
+
using flat = ops.matmul(normalized, this.get(name));
|
|
329
|
+
using rows = ops.reshape(flat, [B, N, nHeads, dDraft / nHeads]);
|
|
330
|
+
return pending.use(ops.transposeAxes(rows, [0, 2, 1, 3]));
|
|
331
|
+
};
|
|
332
|
+
return { k: plane(`bb.${layer}.k`), v: plane(`bb.${layer}.v`) };
|
|
333
|
+
});
|
|
334
|
+
pending.move();
|
|
335
|
+
return pairs;
|
|
336
|
+
}
|
|
337
|
+
|
|
304
338
|
#baseLogits(model: DraftProjection, block: MlxArray): MlxArray {
|
|
305
339
|
const hOut = ops.matmul(block, this.get("out_proj")); // [A,γ,H]
|
|
306
340
|
const hBf = hOut.astype(model.embed.scales.dtype); hOut.dispose();
|
|
@@ -401,167 +435,89 @@ export class DflashDrafter {
|
|
|
401
435
|
}
|
|
402
436
|
}
|
|
403
437
|
|
|
404
|
-
/**
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
* argmax result [1] uint32 feeds directly into the next position's
|
|
410
|
-
* takeAxis, no itemUint32 in the loop. Per-position token arrays are
|
|
411
|
-
* collected and resolved to host numbers with exactly ONE concat +
|
|
412
|
-
* toIntTokens() after the loop. Confidence is the one read the pruning
|
|
413
|
-
* ALGORITHM inherently needs per position (Alg 1 must decide whether to
|
|
414
|
-
* stop NOW) — so it stays per-position whenever thresholds/minConf are
|
|
415
|
-
* actually active; when pruning is inactive there is no such need, so
|
|
416
|
-
* confidence is deferred the same way (collect [1] arrays, one concat +
|
|
417
|
-
* one toFloat32 after the loop). The sampling path already forces a host
|
|
418
|
-
* read every position (sampleToken draws from a device RNG key) — that
|
|
419
|
-
* path is unchanged. */
|
|
420
|
-
forwardInfer(model: DraftProjection, hCtx: MlxArray, anchorTok: number, gamma: number, opts: DflashDraftOpts = {}): DflashDraftBlock {
|
|
421
|
-
const { dDraft, markovRank: r } = this.cfg;
|
|
422
|
-
const V = this.dims.vocabSize;
|
|
423
|
-
const sample = opts.sample && opts.sample.temperature > 0 ? opts.sample : null;
|
|
424
|
-
const keys = opts.keys ?? new KeyStream(opts.sample?.seed ?? 0);
|
|
425
|
-
const collectLogits = opts.collectLogits ?? true;
|
|
426
|
-
const pruningActive = opts.thresholds !== undefined || opts.minConf !== undefined;
|
|
427
|
-
|
|
428
|
-
const ids = ops.fromInt32([anchorTok], [1, 1]);
|
|
429
|
-
const ae3 = model.embed.encode(ids); ids.dispose();
|
|
430
|
-
const anchorEmb = ops.reshape(ae3, [1, this.dims.hiddenSize]); ae3.dispose();
|
|
431
|
-
const block = this.#backbone(hCtx, anchorEmb, null, 1); // [1,γ,d]
|
|
432
|
-
anchorEmb.dispose();
|
|
433
|
-
const U = this.#baseLogits(model, block);
|
|
434
|
-
const Uf = U.astype(CDT); U.dispose();
|
|
438
|
+
/** Single-request callers use the same graph and proposal head at B1. */
|
|
439
|
+
forwardInfer(model: DraftProjection, hCtx: MlxArray, anchor: number, gamma: number, opts: DflashDraftOpts = {}): DflashDraftBlock {
|
|
440
|
+
const result = this.forwardRows(model, hCtx, [anchor], gamma, opts);
|
|
441
|
+
return { tokens: result.tokens[0]!, conf: result.conf[0]!, draftLogits: result.draftLogits };
|
|
442
|
+
}
|
|
435
443
|
|
|
436
|
-
|
|
444
|
+
forwardRows(model: DraftProjection, hCtx: MlxArray | null, anchors: readonly number[], gamma: number,
|
|
445
|
+
opts: DflashDraftOpts = {}, context?: DflashContextAttention): DflashDraftRows {
|
|
446
|
+
const B = anchors.length, { dDraft, markovRank: r } = this.cfg, V = this.dims.vocabSize;
|
|
447
|
+
const sample = opts.sample && opts.sample.temperature > 0 ? opts.sample : null;
|
|
448
|
+
const keys = sample ? anchors.map((_, row) => row === 0 && opts.keys ? opts.keys : new KeyStream(opts.sample?.seed ?? 0)) : [];
|
|
449
|
+
const collect = opts.collectLogits ?? true, pruning = opts.thresholds !== undefined || opts.minConf !== undefined;
|
|
450
|
+
using ids = ops.fromInt32([...anchors], [B, 1]);
|
|
451
|
+
using embeddings = model.embed.encode(ids);
|
|
452
|
+
using anchorEmb = ops.reshape(embeddings, [B, this.dims.hiddenSize]);
|
|
453
|
+
using block = this.#backbone(hCtx, anchorEmb, null, B, context);
|
|
454
|
+
using raw = this.#baseLogits(model, block);
|
|
455
|
+
using base = raw.astype(CDT);
|
|
437
456
|
const isRnn = this.cfg.seqHead === "rnn";
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
const tokArrs: MlxArray[] = []; // per-position [1] device token ids, greedy path only
|
|
445
|
-
const confArrs: MlxArray[] = []; // per-position [1] device confidences, deferred (no pruning) only
|
|
446
|
-
let tokens: number[] = [];
|
|
447
|
-
let prevIdx: MlxArray = ops.fromInt32([anchorTok], [1]); // device token id fed to takeAxis
|
|
448
|
-
let ownPrevIdx = true; // false once prevIdx aliases a tokArrs/argmax entry we must not double-dispose
|
|
449
|
-
// Iteration-local temps register here at creation so the catch can reach
|
|
450
|
-
// them on a mid-flight throw; drained (length=0) at the bottom of each
|
|
451
|
-
// iteration once everything in it was disposed on the normal path.
|
|
452
|
-
// dispose() idempotence makes catch-side re-disposal of the already-freed
|
|
453
|
-
// ones inert.
|
|
454
|
-
const pending: MlxArray[] = [];
|
|
457
|
+
let state: MlxArray | null = isRnn ? zerosArray([B, r]) : null;
|
|
458
|
+
const tokens: MlxArray[] = [], confidence: MlxArray[] = [], logits: MlxArray[] = [];
|
|
459
|
+
const lengths: Array<number | undefined> = anchors.map(() => undefined);
|
|
460
|
+
const hostConfidence: number[][] = [];
|
|
461
|
+
const needConfidence = pruning || opts.collectConfidence !== false;
|
|
462
|
+
using first = ops.fromInt32([...anchors], [B]); let previous = first;
|
|
455
463
|
try {
|
|
456
|
-
for (let
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (ownPrevIdx) { prevIdx.dispose(); ownPrevIdx = false; } // consumed; prevIdx reassigned below
|
|
462
|
-
let Bk: MlxArray;
|
|
464
|
+
for (let step = 0; step < gamma; step++) {
|
|
465
|
+
using row = base.slice([0, step, 0], [B, step + 1, V]);
|
|
466
|
+
using flat = ops.reshape(row, [B, V]);
|
|
467
|
+
using e1 = ops.takeAxis(this.get("markov.w1"), previous, 0);
|
|
468
|
+
let bias: MlxArray;
|
|
463
469
|
if (isRnn) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
Bk = ops.matmul(e1, w2); pending.push(Bk);
|
|
474
|
-
}
|
|
475
|
-
const logitsK = ops.add(Uk, Bk); pending.push(logitsK);
|
|
476
|
-
Uk.dispose(); Bk.dispose();
|
|
477
|
-
// prevIdx becomes the SINGLE tracked owner of the next token id the
|
|
478
|
-
// moment it exists — no separate local for the catch to miss.
|
|
470
|
+
using recurrent = ops.matmul(state!, this.get("rnn.wH"));
|
|
471
|
+
using sum = ops.add(recurrent, e1);
|
|
472
|
+
using shifted = ops.add(sum, this.get("rnn.bH"));
|
|
473
|
+
const next = ops.tanh(shifted); state!.dispose(); state = next;
|
|
474
|
+
bias = ops.matmul(state, this.get("rnn.wO"));
|
|
475
|
+
} else bias = ops.matmul(e1, this.get("markov.w2"));
|
|
476
|
+
using correction = bias;
|
|
477
|
+
using scores = ops.add(flat, correction);
|
|
478
|
+
let token: MlxArray;
|
|
479
479
|
if (sample) {
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// return ≥1 token). Pruning inherently needs the confidence value
|
|
505
|
-
// THIS position, on the host, to decide whether to stop — no way
|
|
506
|
-
// around that read when active. When inactive, defer it (device
|
|
507
|
-
// array, resolved once after the loop) since nothing this iteration
|
|
508
|
-
// depends on its value.
|
|
509
|
-
const thr = opts.thresholds?.[k] ?? opts.minConf;
|
|
510
|
-
if (pruningActive) {
|
|
511
|
-
const cVal = cz.toFloat32()[0]!; cz.dispose();
|
|
512
|
-
if (thr !== undefined && k > 0 && cVal < thr) {
|
|
513
|
-
// Drop position k SYMMETRICALLY on both paths so tokens/conf/
|
|
514
|
-
// draftLogits stay aligned (the DflashDraftBlock contract —
|
|
515
|
-
// verifySampling slices draftLogits per token). Greedy: pop the
|
|
516
|
-
// device array (prevIdx aliases it — one dispose via the owning
|
|
517
|
-
// collection). Sample: pop the host token + free the standalone
|
|
518
|
-
// fromInt32.
|
|
519
|
-
if (!sample) tokArrs.pop()!.dispose(); // also frees prevIdx (same array)
|
|
520
|
-
else { prevIdx.dispose(); tokens.pop(); }
|
|
521
|
-
ownPrevIdx = false; // dead either way — post-loop/catch must not re-dispose
|
|
522
|
-
if (collectLogits) perPos.pop()!.dispose(); // logits row for the dropped position
|
|
523
|
-
break;
|
|
480
|
+
const sampled = anchors.map((_, index) => {
|
|
481
|
+
if (lengths[index] !== undefined) return 0;
|
|
482
|
+
using one = scores.slice([index, 0], [index + 1, V]);
|
|
483
|
+
using processed = processLogits(one, sample);
|
|
484
|
+
return sampleToken(processed, keys[index]!.next());
|
|
485
|
+
});
|
|
486
|
+
token = ops.fromInt32(sampled, [B]);
|
|
487
|
+
} else token = ops.argmaxAxis(scores, -1);
|
|
488
|
+
tokens.push(token); previous = token;
|
|
489
|
+
if (collect) logits.push(ops.reshape(scores, [B, 1, V]));
|
|
490
|
+
if (needConfidence) {
|
|
491
|
+
using h = block.slice([0, step, 0], [B, step + 1, dDraft]);
|
|
492
|
+
using hFlat = ops.reshape(h, [B, dDraft]);
|
|
493
|
+
using joined = ops.concatAxis([hFlat, e1], 1);
|
|
494
|
+
using product = ops.matmul(joined, this.get("conf.w"));
|
|
495
|
+
using shifted = ops.add(product, this.get("conf.b"));
|
|
496
|
+
const conf = ops.sigmoid(shifted); confidence.push(conf);
|
|
497
|
+
if (pruning) {
|
|
498
|
+
const values = conf.toFloat32(), threshold = opts.thresholds?.[step] ?? opts.minConf;
|
|
499
|
+
hostConfidence.push(Array.from(values));
|
|
500
|
+
if (step > 0 && threshold !== undefined) values.forEach((value, row) => {
|
|
501
|
+
if (lengths[row] === undefined && value < threshold) lengths[row] = step;
|
|
502
|
+
});
|
|
503
|
+
if (lengths.every(length => length !== undefined)) break;
|
|
524
504
|
}
|
|
525
|
-
conf.push(cVal);
|
|
526
|
-
} else {
|
|
527
|
-
confArrs.push(cz);
|
|
528
505
|
}
|
|
529
|
-
pending.length = 0; // everything above was disposed (or handed to a collection)
|
|
530
506
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
tokens = allToks.toIntTokens(); allToks.dispose();
|
|
507
|
+
using packed = ops.concatAxis(tokens, 0); const values = packed.toIntTokens();
|
|
508
|
+
using packedConfidence = !pruning && confidence.length ? ops.concatAxis(confidence, 0) : null;
|
|
509
|
+
const conf = packedConfidence?.toFloat32();
|
|
510
|
+
const sizes = lengths.map(length => length ?? tokens.length);
|
|
511
|
+
let draftLogits: MlxArray | undefined;
|
|
512
|
+
if (collect) {
|
|
513
|
+
using full = ops.concatAxis(logits, 1);
|
|
514
|
+
draftLogits = full.slice([0, 0, 0], [B, Math.max(...sizes), V]);
|
|
540
515
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
const draftLogits = collectLogits ? ops.concatAxis(perPos, 1) : undefined;
|
|
548
|
-
if (collectLogits) for (const a of perPos) a.dispose();
|
|
549
|
-
return { tokens, conf, draftLogits };
|
|
550
|
-
} catch (err) {
|
|
551
|
-
// Cleanup on a mid-flight throw — the collections, the carried state,
|
|
552
|
-
// AND the current iteration's registered temps (`pending`). dispose()
|
|
553
|
-
// is idempotent (array.ts #disposed guard) so re-disposing an array the
|
|
554
|
-
// try block already freed (or that aliases one of these collections,
|
|
555
|
-
// e.g. prevIdx === tokArrs[k]) is inert, never a double-free bug.
|
|
556
|
-
for (const a of pending) a.dispose();
|
|
557
|
-
if (ownPrevIdx) prevIdx.dispose();
|
|
558
|
-
for (const a of tokArrs) a.dispose();
|
|
559
|
-
for (const a of confArrs) a.dispose();
|
|
560
|
-
for (const a of perPos) a.dispose();
|
|
561
|
-
s?.dispose();
|
|
562
|
-
Uf.dispose(); block.dispose();
|
|
563
|
-
throw err;
|
|
564
|
-
}
|
|
516
|
+
return {
|
|
517
|
+
tokens: sizes.map((length, row) => Array.from({ length }, (_, step) => values[step * B + row]!)),
|
|
518
|
+
conf: sizes.map((length, row) => opts.collectConfidence === false ? [] : Array.from({ length }, (_, step) => pruning ? hostConfidence[step]![row]! : conf![step * B + row]!)), draftLogits
|
|
519
|
+
};
|
|
520
|
+
} finally { state?.dispose(); for (const array of [...tokens, ...confidence, ...logits]) array.dispose(); }
|
|
565
521
|
}
|
|
566
522
|
|
|
567
523
|
save(dir: string): void {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import type { Glm52DecoderLayer, Glm52Model } from "../model/glm52";
|
|
4
|
+
import type { MLACache } from "../model/glm52-cache";
|
|
5
|
+
import { rmsNormF32Mlx } from "../model/glm52-mla";
|
|
6
|
+
|
|
7
|
+
/** Native MTP numerical graph. Tokens, anchor hidden and compressed state may
|
|
8
|
+
* have one or several rows; sampling and state retention belong to callers. */
|
|
9
|
+
export class Glm52MtpGraph {
|
|
10
|
+
constructor(readonly model: Glm52Model, readonly layer: Glm52DecoderLayer) {}
|
|
11
|
+
|
|
12
|
+
async forward(ids: MlxArray, hidden: MlxArray, cache: MLACache): Promise<MlxArray> {
|
|
13
|
+
const { glmConfig: config, weights } = this.model;
|
|
14
|
+
const prefix = `model.layers.${config.numHiddenLayers}`;
|
|
15
|
+
using embedded = weights.embedding(ids, "model.embed_tokens.weight", config.vocabSize, config.hiddenSize);
|
|
16
|
+
using embeddedNorm = rmsNormF32Mlx(embedded, weights.tensor(`${prefix}.enorm.weight`), config.rmsNormEps);
|
|
17
|
+
using hiddenNorm = rmsNormF32Mlx(hidden, weights.tensor(`${prefix}.hnorm.weight`), config.rmsNormEps);
|
|
18
|
+
using joined = ops.concatAxis([embeddedNorm, hiddenNorm], 2);
|
|
19
|
+
using projected = weights.linear(joined, `${prefix}.eh_proj.weight`, config.hiddenSize, 2 * config.hiddenSize);
|
|
20
|
+
return await this.layer.forwardAsync(projected, cache, null);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
project(hidden: MlxArray): MlxArray {
|
|
24
|
+
const { glmConfig: config, weights } = this.model;
|
|
25
|
+
using normalized = rmsNormF32Mlx(hidden,
|
|
26
|
+
weights.tensor(`model.layers.${config.numHiddenLayers}.shared_head.norm.weight`), config.rmsNormEps);
|
|
27
|
+
return this.model.logitsFromHidden(normalized);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { Dtype } from "../mlx/ffi";
|
|
4
|
+
import { materializeCopy } from "../mlx/materialize";
|
|
5
|
+
import { MLACache } from "../model/glm52-cache";
|
|
6
|
+
import { toLogprobs } from "../sampler";
|
|
7
|
+
import { applyStateChanges, disposeResources } from "../engine/resources";
|
|
8
|
+
import type { PreparedStateChange } from "../contracts/resources";
|
|
9
|
+
import type { DraftRowSampling } from "./source";
|
|
10
|
+
import type { Glm52MtpGraph } from "./glm52-mtp-graph";
|
|
11
|
+
|
|
12
|
+
export interface Glm52MtpRowState {
|
|
13
|
+
readonly cache: MLACache;
|
|
14
|
+
readonly hidden: MlxArray;
|
|
15
|
+
readonly processedTokens: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Native Colibri draft history starts empty, independently of target prompt
|
|
19
|
+
* length. Accepted draft rows are rebuilt from true target hidden states. */
|
|
20
|
+
export class Glm52MtpRows {
|
|
21
|
+
#cache: MLACache;
|
|
22
|
+
#hidden: MlxArray | null = null;
|
|
23
|
+
#processed: number[] = [];
|
|
24
|
+
#drafts: number[][] = [];
|
|
25
|
+
#depth = 0;
|
|
26
|
+
|
|
27
|
+
constructor(readonly graph: Glm52MtpGraph, readonly sampling: DraftRowSampling | null) {
|
|
28
|
+
this.#cache = this.makeCache();
|
|
29
|
+
}
|
|
30
|
+
makeCache(): MLACache {
|
|
31
|
+
const c = this.graph.model.glmConfig;
|
|
32
|
+
return new MLACache({ kvLoraRank: c.kvLoraRank, ropeHeadDim: c.qkRopeHeadDim,
|
|
33
|
+
maxTokens: c.maxPositionEmbeddings, role: "mtp" });
|
|
34
|
+
}
|
|
35
|
+
get rowCount(): number { return this.#processed.length; }
|
|
36
|
+
append(states: readonly (Glm52MtpRowState | null)[]): void {
|
|
37
|
+
applyStateChanges([() => this.prepareAppend(states)]);
|
|
38
|
+
}
|
|
39
|
+
prepareAppend(states: readonly (Glm52MtpRowState | null)[]): PreparedStateChange {
|
|
40
|
+
if (!states.length) return { commit() {}, dispose() {} };
|
|
41
|
+
let cache: MLACache | null = this.makeCache(), hidden: MlxArray | null = null;
|
|
42
|
+
const held: { dispose(): void }[] = [];
|
|
43
|
+
const processed = [...this.#processed, ...states.map(state => state?.processedTokens ?? 0)];
|
|
44
|
+
try {
|
|
45
|
+
cache.mergeRows([...(this.rowCount ? [this.#cache] : []), ...states.map(state => {
|
|
46
|
+
if (state) return state.cache;
|
|
47
|
+
const empty = this.makeCache(); held.push(empty); return empty;
|
|
48
|
+
})]);
|
|
49
|
+
hidden = ops.concatAxis([...(this.#hidden ? [this.#hidden] : []), ...states.map(state => {
|
|
50
|
+
if (state) return state.hidden;
|
|
51
|
+
const zero = ops.zeros([1, 1, this.graph.model.glmConfig.hiddenSize], Dtype.float32);
|
|
52
|
+
held.push(zero); return zero;
|
|
53
|
+
})], 0);
|
|
54
|
+
} catch (error) { cache.dispose(); hidden?.dispose(); throw error; }
|
|
55
|
+
finally { disposeResources(held); }
|
|
56
|
+
return {
|
|
57
|
+
commit: () => {
|
|
58
|
+
const priorCache = this.#cache, priorHidden = this.#hidden;
|
|
59
|
+
this.#cache = cache!; this.#hidden = hidden; this.#processed = processed;
|
|
60
|
+
cache = priorCache; hidden = priorHidden;
|
|
61
|
+
},
|
|
62
|
+
dispose: () => { cache?.dispose(); hidden?.dispose(); cache = null; hidden = null; },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
prefill(tokens: MlxArray, context: MlxArray): void {
|
|
66
|
+
const [batch, length] = tokens.shape as [number, number];
|
|
67
|
+
using tail = context.slice([0, length - 1, 0], [batch, length, context.shape[2]!]);
|
|
68
|
+
const hidden = materializeCopy(tail);
|
|
69
|
+
this.#hidden?.dispose(); this.#hidden = hidden;
|
|
70
|
+
this.#processed = this.#processed.map(count => count + length);
|
|
71
|
+
}
|
|
72
|
+
materialize(): void {
|
|
73
|
+
ops.evalAll([...this.#cache.state(), ...(this.#hidden ? [this.#hidden] : [])]);
|
|
74
|
+
}
|
|
75
|
+
filterRows(keep: readonly number[]): void {
|
|
76
|
+
if (!keep.length) { this.dispose(); return; }
|
|
77
|
+
using indices = ops.fromInt32([...keep], [keep.length]);
|
|
78
|
+
const hidden = ops.takeAxis(this.#hidden!, indices, 0);
|
|
79
|
+
try { this.#cache.filterRows(keep); } catch (error) { hidden.dispose(); throw error; }
|
|
80
|
+
this.#hidden!.dispose(); this.#hidden = hidden;
|
|
81
|
+
this.#processed = keep.map(row => this.#processed[row]!);
|
|
82
|
+
}
|
|
83
|
+
async draft(pending: readonly number[], depth: number, steps: readonly number[]): Promise<number[][]> {
|
|
84
|
+
const B = pending.length, tokens: MlxArray[] = [];
|
|
85
|
+
let ids: MlxArray | null = ops.fromInt32([...pending], [B, 1]), chained: MlxArray | null = null;
|
|
86
|
+
this.#cache.specRoundBegin();
|
|
87
|
+
try {
|
|
88
|
+
for (let position = 0; position < depth; position++) {
|
|
89
|
+
const hidden = await this.graph.forward(ids!, chained ?? this.#hidden!, this.#cache);
|
|
90
|
+
chained?.dispose(); chained = hidden;
|
|
91
|
+
using logits = this.graph.project(hidden);
|
|
92
|
+
using flat = ops.reshape(logits, [B, logits.shape.at(-1)!]);
|
|
93
|
+
using logprobs = toLogprobs(flat);
|
|
94
|
+
const next = this.sampling!.sample(logprobs, steps.map(step => step + position));
|
|
95
|
+
tokens.push(next); ids!.dispose(); ids = ops.reshape(next, [B, 1]);
|
|
96
|
+
}
|
|
97
|
+
using packed = depth ? ops.concatAxis(tokens, 0) : null;
|
|
98
|
+
const read = packed?.toIntTokens() ?? [];
|
|
99
|
+
this.#drafts = Array.from({ length: B }, (_, row) =>
|
|
100
|
+
Array.from({ length: depth }, (_, position) => read[position * B + row]!));
|
|
101
|
+
this.#depth = depth; return this.#drafts;
|
|
102
|
+
} finally { ids?.dispose(); chained?.dispose(); disposeResources(tokens); }
|
|
103
|
+
}
|
|
104
|
+
async commit(accepted: readonly number[], context: MlxArray): Promise<void> {
|
|
105
|
+
const B = accepted.length, width = Math.max(...accepted);
|
|
106
|
+
if (this.#depth) this.#cache.specRoundRollback(1);
|
|
107
|
+
else this.#cache.specRoundCommit();
|
|
108
|
+
if (width) {
|
|
109
|
+
using ids = ops.fromInt32(this.#drafts.flatMap(tokens => Array.from({ length: width }, (_, i) => tokens[i] ?? 0)), [B, width]);
|
|
110
|
+
using hidden = context.slice([0, 0, 0], [B, width, context.shape[2]!]);
|
|
111
|
+
this.#cache.specRoundBegin();
|
|
112
|
+
using output = await this.graph.forward(ids, hidden, this.#cache);
|
|
113
|
+
output.eval(); this.#cache.specRoundRollback(accepted);
|
|
114
|
+
}
|
|
115
|
+
using positions = ops.fromInt32([...accepted], [B, 1, 1]);
|
|
116
|
+
const hidden = ops.takeAlongAxis(context, positions, 1);
|
|
117
|
+
this.#hidden!.dispose(); this.#hidden = hidden;
|
|
118
|
+
this.#processed = this.#processed.map((count, row) => count + 1 + accepted[row]!);
|
|
119
|
+
this.#drafts = []; this.#depth = 0;
|
|
120
|
+
}
|
|
121
|
+
extractRow(row: number): Glm52MtpRowState {
|
|
122
|
+
const cache = this.#cache.extractRow(row);
|
|
123
|
+
try {
|
|
124
|
+
using hidden = this.#hidden!.slice([row, 0, 0], [row + 1, 1, this.#hidden!.shape[2]!]);
|
|
125
|
+
return { cache, hidden: materializeCopy(hidden), processedTokens: this.#processed[row]! };
|
|
126
|
+
} catch (error) { cache.dispose(); throw error; }
|
|
127
|
+
}
|
|
128
|
+
dispose(): void {
|
|
129
|
+
this.#cache.dispose(); this.#hidden?.dispose(); this.#hidden = null;
|
|
130
|
+
this.#processed = []; this.#drafts = []; this.#depth = 0;
|
|
131
|
+
}
|
|
132
|
+
}
|