mlx-bun 0.0.12 → 0.2.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/README.md +40 -24
- package/bin/mlx-bun.mjs +1 -1
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/archive/planning/release-notes-v0.0.13.md +77 -0
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/colibri-glm52-port.md +8 -1
- package/docs/design/compat-cli-surface-design.md +1 -1
- package/docs/design/decode-speed-program.md +1 -1
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +1 -1
- package/docs/design/dspark-speculative-decoding.md +53 -3
- package/docs/design/grammar-spec-batching-integration.md +1 -1
- package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
- package/docs/design/orpo-dynamic-lambda.md +1 -1
- package/docs/design/orpo-future-enhancements.md +1 -1
- package/docs/design/orpo-training.md +2 -2
- package/docs/design/parity-tier-dag.md +1 -1
- package/docs/design/repo-cleanup-plan.md +46 -4
- package/docs/design/segmented-backward-training.md +2 -2
- package/docs/design/ssd-kv-cold-tier.md +127 -0
- package/docs/design/turboquant-weights.md +467 -0
- package/docs/design/w4a16-compute-precision-spike.md +1 -1
- package/docs/design/web-chat-redesign.md +5 -5
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/curve-sampler-research-plan.md +4 -4
- package/docs/planning/memory-docs-and-dag-plan.md +1 -1
- package/docs/planning/release-notes-v0.2.0.md +49 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/cli.md +15 -4
- package/docs/reference/distribution.md +1 -0
- package/docs/reference/features-matrix.md +3 -1
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +86 -19
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/chat-template.ts +34 -1
- package/src/cli.ts +131 -36
- package/src/config.ts +10 -0
- package/src/eval/kl.ts +27 -20
- package/src/eval/runner.ts +23 -4
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/expert-trace.ts +3 -1
- package/src/fit.ts +24 -43
- package/src/flags.ts +7 -9
- package/src/generate.ts +107 -92
- package/src/grammar.ts +2 -1
- package/src/harness-pi.ts +1 -1
- package/src/index.ts +20 -0
- package/src/jobs/job-entry.ts +3 -2
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +370 -205
- package/src/media-fetch.ts +19 -3
- package/src/memory/model.ts +2 -1
- package/src/memory/vault.ts +2 -1
- package/src/memory-plan.ts +18 -0
- package/src/mlx/ffi.ts +8 -11
- package/src/mlx/ops.ts +31 -6
- package/src/model/batched-mask.ts +1 -1
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +43 -97
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +56 -105
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +3 -2
- package/src/model/factory.ts +44 -49
- package/src/model/gemma4-base.ts +163 -10
- package/src/model/generated/gemma4-12b.ts +50 -146
- package/src/model/generated/gemma4-26b.ts +32 -92
- package/src/model/generated/gemma4-e4b.ts +26 -74
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/minicpm5.ts +2 -1
- package/src/model/profile.ts +433 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5.ts +217 -25
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +13 -6
- package/src/pi-web.ts +6 -5
- package/src/prompt-cache.ts +2 -2
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +6 -0
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +102 -2
- package/src/runtime-config.ts +67 -0
- package/src/sampler.ts +272 -0
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +200 -87
- package/src/serve/completion-executor.ts +346 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +205 -0
- package/src/serve/generation-gateway.ts +116 -109
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +338 -0
- package/src/serve/model-admin-routes.ts +153 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +158 -0
- package/src/serve/static-routes.ts +101 -0
- package/src/server.ts +768 -1367
- package/src/spec/dflash-source.ts +2 -1
- package/src/spec/dspark/module-dflash.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +8 -0
- package/src/spec/qwen-mtp-source.ts +431 -0
- package/src/spec/serve-loop.ts +36 -68
- package/src/ssd-cache.ts +17 -14
- package/src/ssd-durability.ts +195 -0
- package/src/train/flash-cce.ts +12 -11
- package/src/train/forward.ts +32 -1
- package/src/train/loss.ts +2 -1
- package/src/train/segmented.ts +4 -3
- package/src/train/trainer.ts +3 -2
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +47 -1
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +513 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.js +10 -3
- package/src/web/src/chat.ts +12 -5
- package/src/weights.ts +81 -26
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
|
@@ -26,6 +26,7 @@ import { Gemma4Model } from "../model/gemma4";
|
|
|
26
26
|
import type { DflashDrafter } from "./dspark/module-dflash";
|
|
27
27
|
import { loadDsparkDrafter } from "./dspark/loader";
|
|
28
28
|
import type { DraftProvider, DraftSource, TargetView } from "./source";
|
|
29
|
+
import { runtimeValue } from "../runtime-config";
|
|
29
30
|
|
|
30
31
|
function safetensorsBytes(dir: string): number {
|
|
31
32
|
let total = 0;
|
|
@@ -93,7 +94,7 @@ class DflashSource implements DraftSource {
|
|
|
93
94
|
draft(feed: number[], n: number, _stepBase: number, _anchorHidden?: MlxArray): number[] {
|
|
94
95
|
if (!this.hCtx) throw new Error("DSpark drafter: draft before prefill");
|
|
95
96
|
const anchorTok = feed[feed.length - 1]!;
|
|
96
|
-
const envMin =
|
|
97
|
+
const envMin = runtimeValue("MLX_BUN_DSPARK_MINCONF");
|
|
97
98
|
const block = this.drafter.forwardInfer(this.model, this.hCtx, anchorTok, n, {
|
|
98
99
|
thresholds: this.drafter.cfg.sts?.thresholds,
|
|
99
100
|
collectLogits: false, // serve loop runs its own verify lm-head, never reads this
|
|
@@ -403,7 +403,7 @@ export class DflashDrafter {
|
|
|
403
403
|
/** Inference: parallel backbone once, then sequential Markov sampling.
|
|
404
404
|
* hCtx [1,Lctx,m*H] (full current context), anchorTok the bonus token.
|
|
405
405
|
*
|
|
406
|
-
* Host-sync discipline (docs/investigations/dspark-handoff.md item 3): the
|
|
406
|
+
* Host-sync discipline (docs/archive/investigations/dspark-handoff.md item 3): the
|
|
407
407
|
* greedy path's token recurrence stays ON-DEVICE across positions — the
|
|
408
408
|
* argmax result [1] uint32 feeds directly into the next position's
|
|
409
409
|
* takeAxis, no itemUint32 in the loop. Per-position token arrays are
|
|
@@ -305,6 +305,14 @@ export class Glm52NativeMtpSource implements DraftSource {
|
|
|
305
305
|
config.rmsNormEps,
|
|
306
306
|
);
|
|
307
307
|
logits = this.model.logitsFromHidden(headInput);
|
|
308
|
+
// Sampler contract is [1, V] — same 3-D top-k hazard as
|
|
309
|
+
// qwen-mtp-source #sample (2026-08-20); see the comment there.
|
|
310
|
+
{
|
|
311
|
+
const V = logits.shape[logits.shape.length - 1]!;
|
|
312
|
+
const flat = ops.reshape(logits, [1, V]);
|
|
313
|
+
logits.dispose();
|
|
314
|
+
logits = flat;
|
|
315
|
+
}
|
|
308
316
|
logprobs = toLogprobs(logits);
|
|
309
317
|
sampled = this.sampler(logprobs, sampleStep);
|
|
310
318
|
return {
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// Native Qwen3.8 MTP draft source — the Qwen-TRAINED multi-step prediction
|
|
2
|
+
// head, split from the raw release's last shard and published as
|
|
3
|
+
// mlx-community/Qwen3.8-27B-MTP-* (model_type "qwen3_5_mtp", block_size 3).
|
|
4
|
+
//
|
|
5
|
+
// Reference implementation (locate + port + gate, don't design):
|
|
6
|
+
// mlx_vlm/speculative/drafters/qwen3_5_mtp/qwen3_5_mtp.py. Mechanism, per
|
|
7
|
+
// step: h = fc(concat(rms_emb(embed(token)), rms_hidden(hidden))) → ONE
|
|
8
|
+
// full-attention decoder layer (own KVCache, positions continue the target
|
|
9
|
+
// sequence) → norm → the TARGET's lm_head → next draft token; recursive for
|
|
10
|
+
// the block ("trained with multiple steps"). The drafter is NOT standalone:
|
|
11
|
+
// it binds the target's embed_tokens + lm_head (mtp_use_dedicated_embeddings
|
|
12
|
+
// false) and consumes the target's PRE-final-norm last-layer hidden (mlx-vlm
|
|
13
|
+
// captures it with skip_final_norm=True) — carried here by the tapLayers
|
|
14
|
+
// machinery, NOT the seam's post-norm anchorHidden.
|
|
15
|
+
//
|
|
16
|
+
// Row convention (predict-2-ahead): drafter KV row at position p is built
|
|
17
|
+
// from (embed(token at position p+1), hidden at position p). Invariant
|
|
18
|
+
// across rounds: after commit, the drafter offset equals the next pending
|
|
19
|
+
// token's position minus zero — i.e. draft() always opens by building the
|
|
20
|
+
// pending token's row from the TARGET's TRUE hidden at the emitted position
|
|
21
|
+
// (held over from the verify tap). Computationally identical to mlx-vlm's
|
|
22
|
+
// commit-time "seed" append, just performed at the top of the next round.
|
|
23
|
+
//
|
|
24
|
+
// Weights are plain bf16 (DenseLinear); the published drafter snapshots are
|
|
25
|
+
// already sanitized (norms in runtime layout — no +1.0 shift here).
|
|
26
|
+
|
|
27
|
+
import { MlxArray } from "../mlx/array";
|
|
28
|
+
import * as ops from "../mlx/ops";
|
|
29
|
+
import { toLogprobs } from "../sampler";
|
|
30
|
+
import { loadModelConfig, type ModelConfig } from "../config";
|
|
31
|
+
import { Weights } from "../weights";
|
|
32
|
+
import { disposing, KVCache, RMSNorm, type Mask } from "../model/gemma4-base";
|
|
33
|
+
import { DenseLinear } from "../model/universal/modules";
|
|
34
|
+
import { Qwen35Model } from "../model/qwen3_5";
|
|
35
|
+
import type { DraftProvider, DraftSource, TargetView } from "./source";
|
|
36
|
+
|
|
37
|
+
type Sampler = (logprobs: MlxArray, step: number) => MlxArray;
|
|
38
|
+
|
|
39
|
+
const DRAFT_PREFILL_CHUNK = 2048;
|
|
40
|
+
|
|
41
|
+
/** Dense-weight clone of Qwen3Attention.forward (src/model/qwen3_5.ts —
|
|
42
|
+
* ops copied verbatim; only the projection flavor differs: the drafter is
|
|
43
|
+
* bf16, the target's class hardwires QuantizedLinear). */
|
|
44
|
+
class MtpAttention {
|
|
45
|
+
readonly qProj: DenseLinear;
|
|
46
|
+
readonly kProj: DenseLinear;
|
|
47
|
+
readonly vProj: DenseLinear;
|
|
48
|
+
readonly oProj: DenseLinear;
|
|
49
|
+
readonly qNorm: RMSNorm;
|
|
50
|
+
readonly kNorm: RMSNorm;
|
|
51
|
+
readonly nHeads: number;
|
|
52
|
+
readonly nKvHeads: number;
|
|
53
|
+
readonly headDim: number;
|
|
54
|
+
readonly scale: number;
|
|
55
|
+
readonly ropeDims: number;
|
|
56
|
+
readonly ropeBase: number;
|
|
57
|
+
|
|
58
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
59
|
+
const t = config.text;
|
|
60
|
+
this.nHeads = t.numAttentionHeads;
|
|
61
|
+
this.nKvHeads = t.numKeyValueHeads;
|
|
62
|
+
this.headDim = t.headDim;
|
|
63
|
+
this.scale = Math.pow(this.headDim, -0.5);
|
|
64
|
+
this.ropeDims = Math.trunc(this.headDim * t.partialRotaryFactor);
|
|
65
|
+
this.ropeBase = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
|
|
66
|
+
const dense = (path: string): DenseLinear =>
|
|
67
|
+
new DenseLinear(weights.tensor(`${path}.weight`), null);
|
|
68
|
+
this.qProj = dense(`${prefix}.q_proj`);
|
|
69
|
+
this.kProj = dense(`${prefix}.k_proj`);
|
|
70
|
+
this.vProj = dense(`${prefix}.v_proj`);
|
|
71
|
+
this.oProj = dense(`${prefix}.o_proj`);
|
|
72
|
+
this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps);
|
|
73
|
+
this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
forward(x: MlxArray, mask: Mask, cache: KVCache): MlxArray {
|
|
77
|
+
const [B, L] = x.shape as [number, number, number];
|
|
78
|
+
const qp = this.qProj.forward(x);
|
|
79
|
+
const qpr = disposing(qp, ops.reshape(qp, [B, L, this.nHeads, this.headDim * 2]));
|
|
80
|
+
const [qHeads, gateHeads] = ops.split(qpr, [this.headDim], -1) as [MlxArray, MlxArray];
|
|
81
|
+
qpr.dispose();
|
|
82
|
+
const gate = disposing(gateHeads, ops.reshape(gateHeads, [B, L, this.nHeads * this.headDim]));
|
|
83
|
+
|
|
84
|
+
let k = this.kProj.forward(x);
|
|
85
|
+
let v = this.vProj.forward(x);
|
|
86
|
+
|
|
87
|
+
let q = this.qNorm.forward(qHeads);
|
|
88
|
+
qHeads.dispose();
|
|
89
|
+
q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
|
|
90
|
+
k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
|
|
91
|
+
k = disposing(k, this.kNorm.forward(k));
|
|
92
|
+
k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
|
|
93
|
+
v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
|
|
94
|
+
v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
|
|
95
|
+
|
|
96
|
+
q = disposing(q, ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
|
|
97
|
+
k = disposing(k, ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
|
|
98
|
+
|
|
99
|
+
const [keys, values] = cache.updateAndFetch(k, v);
|
|
100
|
+
k.dispose();
|
|
101
|
+
v.dispose();
|
|
102
|
+
const attn = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
|
|
103
|
+
keys.dispose();
|
|
104
|
+
values.dispose();
|
|
105
|
+
q.dispose();
|
|
106
|
+
|
|
107
|
+
const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
108
|
+
attn.dispose();
|
|
109
|
+
const merged = ops.reshape(attnT, [B, L, -1]);
|
|
110
|
+
attnT.dispose();
|
|
111
|
+
const sig = ops.sigmoid(gate);
|
|
112
|
+
gate.dispose();
|
|
113
|
+
const gated = ops.mul(merged, sig);
|
|
114
|
+
merged.dispose();
|
|
115
|
+
sig.dispose();
|
|
116
|
+
const out = this.oProj.forward(gated);
|
|
117
|
+
gated.dispose();
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The one MTP decoder block: fc-merge → attention → swiglu MLP → norm. */
|
|
123
|
+
class MtpModule {
|
|
124
|
+
readonly fc: DenseLinear;
|
|
125
|
+
readonly preFcNormEmbedding: RMSNorm;
|
|
126
|
+
readonly preFcNormHidden: RMSNorm;
|
|
127
|
+
readonly attn: MtpAttention;
|
|
128
|
+
readonly mlpGate: DenseLinear;
|
|
129
|
+
readonly mlpUp: DenseLinear;
|
|
130
|
+
readonly mlpDown: DenseLinear;
|
|
131
|
+
readonly inputNorm: RMSNorm;
|
|
132
|
+
readonly postAttnNorm: RMSNorm;
|
|
133
|
+
readonly finalNorm: RMSNorm;
|
|
134
|
+
|
|
135
|
+
constructor(weights: Weights, config: ModelConfig) {
|
|
136
|
+
const eps = config.text.rmsNormEps;
|
|
137
|
+
const dense = (path: string): DenseLinear =>
|
|
138
|
+
new DenseLinear(weights.tensor(`${path}.weight`), null);
|
|
139
|
+
this.fc = dense("fc");
|
|
140
|
+
this.preFcNormEmbedding = new RMSNorm(weights.tensor("pre_fc_norm_embedding.weight"), eps);
|
|
141
|
+
this.preFcNormHidden = new RMSNorm(weights.tensor("pre_fc_norm_hidden.weight"), eps);
|
|
142
|
+
this.attn = new MtpAttention(weights, config, "layers.0.self_attn");
|
|
143
|
+
this.mlpGate = dense("layers.0.mlp.gate_proj");
|
|
144
|
+
this.mlpUp = dense("layers.0.mlp.up_proj");
|
|
145
|
+
this.mlpDown = dense("layers.0.mlp.down_proj");
|
|
146
|
+
this.inputNorm = new RMSNorm(weights.tensor("layers.0.input_layernorm.weight"), eps);
|
|
147
|
+
this.postAttnNorm = new RMSNorm(weights.tensor("layers.0.post_attention_layernorm.weight"), eps);
|
|
148
|
+
this.finalNorm = new RMSNorm(weights.tensor("norm.weight"), eps);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** One block forward over [1,S,·]: token embeddings ([1,S,H], target
|
|
152
|
+
* embed_tokens output) paired with hiddens ([1,S,H], target pre-final-norm
|
|
153
|
+
* or the module's own chained output). Appends S rows to `cache`; returns
|
|
154
|
+
* the module output [1,S,H] (post final norm — what the target lm_head
|
|
155
|
+
* consumes AND what chains into the next step's `hidden`). */
|
|
156
|
+
forward(tokenEmbeds: MlxArray, hiddens: MlxArray, cache: KVCache): MlxArray {
|
|
157
|
+
const embNorm = this.preFcNormEmbedding.forward(tokenEmbeds);
|
|
158
|
+
const hidNorm = this.preFcNormHidden.forward(hiddens);
|
|
159
|
+
const joined = ops.concatAxis([embNorm, hidNorm], 2);
|
|
160
|
+
embNorm.dispose();
|
|
161
|
+
hidNorm.dispose();
|
|
162
|
+
const x = this.fc.forward(joined);
|
|
163
|
+
joined.dispose();
|
|
164
|
+
|
|
165
|
+
// Decoder layer (Qwen3Layer.forward shape, dense weights).
|
|
166
|
+
const L = x.shape[1]!;
|
|
167
|
+
const mask = cache.makeMask(L, null);
|
|
168
|
+
const xn = this.inputNorm.forward(x);
|
|
169
|
+
const r = this.attn.forward(xn, mask, cache);
|
|
170
|
+
xn.dispose();
|
|
171
|
+
mask.arr?.dispose();
|
|
172
|
+
const h = ops.add(x, r);
|
|
173
|
+
x.dispose();
|
|
174
|
+
r.dispose();
|
|
175
|
+
const hn = this.postAttnNorm.forward(h);
|
|
176
|
+
const g = this.mlpGate.forward(hn);
|
|
177
|
+
const u = this.mlpUp.forward(hn);
|
|
178
|
+
hn.dispose();
|
|
179
|
+
const silu = ops.silu(g);
|
|
180
|
+
g.dispose();
|
|
181
|
+
const act = ops.mul(silu, u);
|
|
182
|
+
silu.dispose();
|
|
183
|
+
u.dispose();
|
|
184
|
+
const m = this.mlpDown.forward(act);
|
|
185
|
+
act.dispose();
|
|
186
|
+
const out = ops.add(h, m);
|
|
187
|
+
h.dispose();
|
|
188
|
+
m.dispose();
|
|
189
|
+
return disposing(out, this.finalNorm.forward(out));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export class QwenMtpProvider implements DraftProvider {
|
|
194
|
+
readonly id: string;
|
|
195
|
+
readonly weightsBytes: number;
|
|
196
|
+
readonly #module: MtpModule;
|
|
197
|
+
readonly #config: ModelConfig;
|
|
198
|
+
|
|
199
|
+
private constructor(id: string, config: ModelConfig, weights: Weights, module: MtpModule) {
|
|
200
|
+
this.id = id;
|
|
201
|
+
this.#config = config;
|
|
202
|
+
this.#module = module;
|
|
203
|
+
this.weightsBytes = [...weights.shards.files.values()]
|
|
204
|
+
.reduce((a, f) => a + f.mmap.size, 0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static async load(dir: string): Promise<QwenMtpProvider> {
|
|
208
|
+
const config = await loadModelConfig(dir);
|
|
209
|
+
if (config.modelType !== "qwen3_5_mtp")
|
|
210
|
+
throw new Error(`${dir}: not a qwen3_5_mtp drafter (model_type ${config.modelType})`);
|
|
211
|
+
const weights = await Weights.open(dir);
|
|
212
|
+
return new QwenMtpProvider(
|
|
213
|
+
dir.split("/").filter(Boolean).at(-1) ?? "qwen-mtp",
|
|
214
|
+
config, weights, new MtpModule(weights, config),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
|
|
219
|
+
const target = opts.target.model;
|
|
220
|
+
if (!(target instanceof Qwen35Model))
|
|
221
|
+
throw new Error("qwen MTP drafting requires a qwen3_5-family target");
|
|
222
|
+
if (target.config.text.hiddenSize !== this.#config.text.hiddenSize) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`qwen MTP drafter hidden ${this.#config.text.hiddenSize} != target ` +
|
|
225
|
+
`${target.config.text.hiddenSize} — split from a different checkpoint?`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return new QwenMtpSource(target, this.#module, opts.sampler);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
dispose(): void {
|
|
232
|
+
// Weights are mmap-backed and pinned for the process (house rule: no JS
|
|
233
|
+
// dtors into mlx); the provider owns no other native state.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export class QwenMtpSource implements DraftSource {
|
|
238
|
+
// Full-prompt target prefill (the bonus token exists before round 1, and
|
|
239
|
+
// the tap covers every prompt position — mlx-vlm's flow).
|
|
240
|
+
readonly prefillMode = "full" as const;
|
|
241
|
+
readonly weightsBytes = 0; // provider owns the drafter weights
|
|
242
|
+
/** Pre-final-norm tap: the LAST layer's output stream (index nLayers-1).
|
|
243
|
+
* The seam's anchorHidden is post-final-norm and is deliberately unused. */
|
|
244
|
+
readonly tapLayers: number[];
|
|
245
|
+
|
|
246
|
+
readonly #target: Qwen35Model;
|
|
247
|
+
readonly #module: MtpModule;
|
|
248
|
+
readonly #sampler: Sampler;
|
|
249
|
+
readonly #cache = new KVCache();
|
|
250
|
+
/** Target pre-norm hidden at the position preceding the next pending
|
|
251
|
+
* token: prefill's last tapped row, then each commit's vCtx row at the
|
|
252
|
+
* emitted position. draft() consumes it to build the pending row. [1,1,H] */
|
|
253
|
+
#pendingTrueHidden: MlxArray | null = null;
|
|
254
|
+
#roundAppended = 0;
|
|
255
|
+
#closed = false;
|
|
256
|
+
|
|
257
|
+
constructor(target: Qwen35Model, module: MtpModule, sampler: Sampler) {
|
|
258
|
+
this.#target = target;
|
|
259
|
+
this.#module = module;
|
|
260
|
+
this.#sampler = sampler;
|
|
261
|
+
this.tapLayers = [target.config.text.numHiddenLayers - 1];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Drafter prefill: rows for positions 0..L-2, keyed (token_{p+1}, h_p) —
|
|
265
|
+
* the (pending, h_{L-1}) row is NOT built here (the pending token is
|
|
266
|
+
* sampled after target prefill); its true hidden is kept for round 1. */
|
|
267
|
+
async prefill(promptIds: number[], ctxML?: MlxArray): Promise<void> {
|
|
268
|
+
this.#checkOpen();
|
|
269
|
+
if (!ctxML)
|
|
270
|
+
throw new Error("qwen MTP prefill requires the tapped pre-final-norm context");
|
|
271
|
+
try {
|
|
272
|
+
if (this.#cache.offset !== 0)
|
|
273
|
+
throw new Error("qwen MTP source cannot be prefilled twice");
|
|
274
|
+
const L = promptIds.length;
|
|
275
|
+
const H = ctxML.shape[2]!;
|
|
276
|
+
if (ctxML.shape[1]! !== L)
|
|
277
|
+
throw new Error(`qwen MTP tap covered ${ctxML.shape[1]} of ${L} prompt positions`);
|
|
278
|
+
this.#pendingTrueHidden = ctxML.slice([0, L - 1, 0], [1, L, H]);
|
|
279
|
+
for (let pos = 0; pos + 1 < L; pos += DRAFT_PREFILL_CHUNK) {
|
|
280
|
+
const n = Math.min(DRAFT_PREFILL_CHUNK, L - 1 - pos);
|
|
281
|
+
const shifted = promptIds.slice(pos + 1, pos + 1 + n);
|
|
282
|
+
const ids = ops.fromInt32(shifted, [1, n]);
|
|
283
|
+
const embeds = this.#target.embed.encode(ids);
|
|
284
|
+
ids.dispose();
|
|
285
|
+
const hiddens = ctxML.slice([0, pos, 0], [1, pos + n, H]);
|
|
286
|
+
const out = this.#module.forward(embeds, hiddens, this.#cache);
|
|
287
|
+
embeds.dispose();
|
|
288
|
+
hiddens.dispose();
|
|
289
|
+
out.dispose(); // prefill outputs are not seeds; only KV matters here
|
|
290
|
+
}
|
|
291
|
+
if (this.#cache.offset !== L - 1)
|
|
292
|
+
throw new Error(`qwen MTP prefill offset ${this.#cache.offset}, expected ${L - 1}`);
|
|
293
|
+
} finally {
|
|
294
|
+
ctxML?.dispose(); // ownership per the seam contract
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async draft(feed: number[], n: number, stepBase: number): Promise<number[]> {
|
|
299
|
+
this.#checkOpen();
|
|
300
|
+
if (this.#roundAppended !== 0)
|
|
301
|
+
throw new Error("qwen MTP draft called before the prior round committed");
|
|
302
|
+
if (n <= 0) return [];
|
|
303
|
+
const pending = feed.at(-1);
|
|
304
|
+
if (!Number.isSafeInteger(pending) || pending! < 0)
|
|
305
|
+
throw new Error("qwen MTP requires a non-empty token feed");
|
|
306
|
+
|
|
307
|
+
const drafts: number[] = [];
|
|
308
|
+
let chained: MlxArray | null = null;
|
|
309
|
+
const startOffset = this.#cache.offset;
|
|
310
|
+
try {
|
|
311
|
+
// Build the pending token's row from the TRUE target hidden at the
|
|
312
|
+
// preceding position (prefill tail on round 1, verify tap afterwards).
|
|
313
|
+
if (!this.#pendingTrueHidden)
|
|
314
|
+
throw new Error("qwen MTP draft before prefill/commit");
|
|
315
|
+
const out = this.#stepOne(pending!, this.#pendingTrueHidden);
|
|
316
|
+
this.#pendingTrueHidden.dispose();
|
|
317
|
+
this.#pendingTrueHidden = null;
|
|
318
|
+
this.#roundAppended++;
|
|
319
|
+
drafts.push(this.#sample(out, stepBase));
|
|
320
|
+
chained = out;
|
|
321
|
+
while (drafts.length < n) {
|
|
322
|
+
const out = this.#stepOne(drafts.at(-1)!, chained!);
|
|
323
|
+
chained!.dispose();
|
|
324
|
+
chained = out;
|
|
325
|
+
this.#roundAppended++;
|
|
326
|
+
drafts.push(this.#sample(out, stepBase + drafts.length));
|
|
327
|
+
}
|
|
328
|
+
return drafts;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
const appended = this.#cache.offset - startOffset;
|
|
331
|
+
if (appended > 0) this.#cache.trim(appended);
|
|
332
|
+
this.#roundAppended = 0;
|
|
333
|
+
throw error;
|
|
334
|
+
} finally {
|
|
335
|
+
chained?.dispose();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** mlx-vlm accept semantics: keep drafted rows for accepted positions,
|
|
340
|
+
* trim the rejected tail, then append the missing accepted row (all-accept
|
|
341
|
+
* case) and the correction/bonus row using the TARGET's verified pre-norm
|
|
342
|
+
* hiddens — harvesting the last output as the next round's seed. */
|
|
343
|
+
async commit(
|
|
344
|
+
d: number,
|
|
345
|
+
kAccept: number,
|
|
346
|
+
vCtxML?: MlxArray,
|
|
347
|
+
_verifiedHidden?: MlxArray,
|
|
348
|
+
acceptedTokens: readonly number[] = [],
|
|
349
|
+
): Promise<void> {
|
|
350
|
+
this.#checkOpen();
|
|
351
|
+
try {
|
|
352
|
+
if (!vCtxML)
|
|
353
|
+
throw new Error("qwen MTP commit requires the tapped verify context");
|
|
354
|
+
if (kAccept > 0 && acceptedTokens.length !== kAccept)
|
|
355
|
+
throw new Error(
|
|
356
|
+
`qwen MTP received ${acceptedTokens.length} accepted tokens for k=${kAccept}`,
|
|
357
|
+
);
|
|
358
|
+
// Rows appended this round cover window tokens 0..d-1 (pending +
|
|
359
|
+
// drafts[0..d-2]); rows keyed by ACCEPTED window tokens (0..kAccept)
|
|
360
|
+
// survive — trim the rejected tail.
|
|
361
|
+
const trim = Math.max(this.#roundAppended - 1 - kAccept, 0);
|
|
362
|
+
if (trim > 0) this.#cache.trim(trim);
|
|
363
|
+
|
|
364
|
+
// All-accept case: window token d (= drafts[d-1]) has no drafted row —
|
|
365
|
+
// append it from the TRUE verify hidden (vCtx row d-1, the hidden at
|
|
366
|
+
// its preceding position). The correction/bonus row is NOT built here:
|
|
367
|
+
// the serve loop re-feeds that token in the next draft()'s `feed`, and
|
|
368
|
+
// draft() opens by building its row from #pendingTrueHidden below.
|
|
369
|
+
const H = vCtxML.shape[2]!;
|
|
370
|
+
if (kAccept === d && d > 0) {
|
|
371
|
+
const ids = ops.fromInt32([acceptedTokens[kAccept - 1]!], [1, 1]);
|
|
372
|
+
const embeds = this.#target.embed.encode(ids);
|
|
373
|
+
ids.dispose();
|
|
374
|
+
const hidden = vCtxML.slice([0, d - 1, 0], [1, d, H]);
|
|
375
|
+
const out = this.#module.forward(embeds, hidden, this.#cache);
|
|
376
|
+
embeds.dispose();
|
|
377
|
+
hidden.dispose();
|
|
378
|
+
out.dispose();
|
|
379
|
+
}
|
|
380
|
+
// Next round's pending row input: the target's verified pre-norm
|
|
381
|
+
// hidden at the emitted position.
|
|
382
|
+
this.#pendingTrueHidden?.dispose();
|
|
383
|
+
this.#pendingTrueHidden = vCtxML.slice([0, kAccept, 0], [1, kAccept + 1, H]);
|
|
384
|
+
} finally {
|
|
385
|
+
vCtxML?.dispose(); // seam: commit takes ownership
|
|
386
|
+
this.#roundAppended = 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
dispose(): void {
|
|
391
|
+
if (this.#closed) return;
|
|
392
|
+
this.#closed = true;
|
|
393
|
+
this.#cache.dispose();
|
|
394
|
+
this.#pendingTrueHidden?.dispose();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** One module forward for (token, hidden) — appends one KV row. */
|
|
398
|
+
#stepOne(token: number, hidden: MlxArray): MlxArray {
|
|
399
|
+
const ids = ops.fromInt32([token], [1, 1]);
|
|
400
|
+
const embed = this.#target.embed.encode(ids);
|
|
401
|
+
ids.dispose();
|
|
402
|
+
const out = this.#module.forward(embed, hidden, this.#cache);
|
|
403
|
+
embed.dispose();
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Sample a draft token from the module output via the TARGET's lm head
|
|
408
|
+
* and the request sampler (per-step RNG stream discipline). */
|
|
409
|
+
#sample(moduleOut: MlxArray, step: number): number {
|
|
410
|
+
const logits = this.#target.logitsFromHidden(moduleOut);
|
|
411
|
+
// Sampler contract is [1, V] (the main decode loop's shape). moduleOut
|
|
412
|
+
// is [1, 1, H] → logits [1, 1, V]; without this reshape any sampler
|
|
413
|
+
// that slices 2-D (top-k) throws "[slice] Invalid number of indices…
|
|
414
|
+
// dimension 3" — the serve-lane MTP 500 (chat defaults carry the
|
|
415
|
+
// model's top_k=20; the greedy bench harness never hit it).
|
|
416
|
+
const V = logits.shape[logits.shape.length - 1]!;
|
|
417
|
+
const flat = ops.reshape(logits, [1, V]);
|
|
418
|
+
logits.dispose();
|
|
419
|
+
const logprobs = toLogprobs(flat);
|
|
420
|
+
flat.dispose();
|
|
421
|
+
const tok = this.#sampler(logprobs, step);
|
|
422
|
+
logprobs.dispose();
|
|
423
|
+
const id = ops.itemUint32(tok);
|
|
424
|
+
tok.dispose();
|
|
425
|
+
return id;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#checkOpen(): void {
|
|
429
|
+
if (this.#closed) throw new Error("qwen MTP source is closed");
|
|
430
|
+
}
|
|
431
|
+
}
|
package/src/spec/serve-loop.ts
CHANGED
|
@@ -46,7 +46,7 @@ import { flagOn } from "../flags";
|
|
|
46
46
|
import type { RuntimeModel } from "../model/factory";
|
|
47
47
|
import type { Cache } from "../model/gemma4";
|
|
48
48
|
import type { GenerateOptions, GenerateStats } from "../generate";
|
|
49
|
-
import { makeSampler,
|
|
49
|
+
import { makeSampler, makeStepSampler } from "../sampler";
|
|
50
50
|
import type { OnToken } from "../serve/generation-gateway";
|
|
51
51
|
import type { DraftProvider } from "./source";
|
|
52
52
|
|
|
@@ -132,7 +132,15 @@ export async function specServeRun(
|
|
|
132
132
|
const maxTokens = options.maxTokens ?? 512;
|
|
133
133
|
const eos = options.eosTokenIds ?? model.config.eosTokenIds;
|
|
134
134
|
const sampler = makeSampler(options);
|
|
135
|
-
const
|
|
135
|
+
const stepSampler = makeStepSampler(options, {
|
|
136
|
+
tokenRepresentation: "number",
|
|
137
|
+
grammarWait: "before-sample",
|
|
138
|
+
historyUpdate: "after-sample",
|
|
139
|
+
initialHistory: promptIds,
|
|
140
|
+
acceptGrammar: true,
|
|
141
|
+
eosTokenIds: eos,
|
|
142
|
+
sampler,
|
|
143
|
+
});
|
|
136
144
|
const gamma = Math.max(1, numDraftTokens);
|
|
137
145
|
|
|
138
146
|
// Allocated INSIDE the try below (2026-07-07 review): provider.open()
|
|
@@ -159,11 +167,6 @@ export async function specServeRun(
|
|
|
159
167
|
let vCtxML: MlxArray | null = null;
|
|
160
168
|
let vLogits: MlxArray | null = null;
|
|
161
169
|
let roundRow: MlxArray | null = null; // the in-flight verify/continuation row
|
|
162
|
-
// Device-side token history for the logits processors (repetition/presence/
|
|
163
|
-
// frequency penalties, logit_bias) — generate()'s pushHistory discipline.
|
|
164
|
-
let history: MlxArray | null =
|
|
165
|
-
processors.length > 0 ? ops.fromInt32(promptIds, [promptIds.length]) : null;
|
|
166
|
-
|
|
167
170
|
const extras: SpecServeExtras = {
|
|
168
171
|
drafted: 0,
|
|
169
172
|
accepted: 0,
|
|
@@ -185,65 +188,12 @@ export async function specServeRun(
|
|
|
185
188
|
spec: extras,
|
|
186
189
|
};
|
|
187
190
|
|
|
188
|
-
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* log-softmax → sampler. The GRAMMAR walk is inherently sequential: the
|
|
192
|
-
* mask at position i reflects every token emitted before it, so this
|
|
193
|
-
* awaits the matcher's async fill (fired by the previous accept) before
|
|
194
|
-
* masking, samples, then advances the matcher — the matcher only ever
|
|
195
|
-
* moves on tokens that are (about to be) EMITTED, which is what makes
|
|
196
|
-
* rollback-free v1 correct: rejected DRAFTS never touch grammar state.
|
|
197
|
-
* Appends the sampled token to the processor history. */
|
|
191
|
+
/** The speculative walk needs eager token ids and waits for each grammar
|
|
192
|
+
* mask. StepSampler keeps its processor, mask, sample, and history ordering
|
|
193
|
+
* identical to the serial and batched lanes. */
|
|
198
194
|
const grammar = options.grammar;
|
|
199
|
-
const samplePos = async (logits1V: MlxArray, step: number): Promise<number> =>
|
|
200
|
-
|
|
201
|
-
// fill reject, applyMask/toLogprobs/sampler error (2026-07-07 review:
|
|
202
|
-
// these were the one spot the header's hoist discipline missed; a
|
|
203
|
-
// grammar+penalties request whose matcher fill rejects orphaned one
|
|
204
|
-
// [1,V] row per occurrence). `cur` is owned only once it diverges from
|
|
205
|
-
// the caller's row.
|
|
206
|
-
let cur: MlxArray = logits1V;
|
|
207
|
-
let lp: MlxArray | null = null;
|
|
208
|
-
let tokArr: MlxArray | null = null;
|
|
209
|
-
try {
|
|
210
|
-
for (const p of processors) {
|
|
211
|
-
const next = p(history, cur);
|
|
212
|
-
if (cur !== logits1V) cur.dispose();
|
|
213
|
-
cur = next;
|
|
214
|
-
}
|
|
215
|
-
if (grammar && !grammar.isTerminated) {
|
|
216
|
-
await grammar.ready();
|
|
217
|
-
const masked = grammar.applyMask(cur);
|
|
218
|
-
if (cur !== logits1V) cur.dispose();
|
|
219
|
-
cur = masked;
|
|
220
|
-
}
|
|
221
|
-
lp = toLogprobs(cur);
|
|
222
|
-
if (cur !== logits1V) cur.dispose();
|
|
223
|
-
cur = logits1V; // consumed — nothing owned under this name now
|
|
224
|
-
tokArr = sampler(lp, step);
|
|
225
|
-
lp.dispose();
|
|
226
|
-
lp = null;
|
|
227
|
-
const tok = ops.itemUint32(tokArr);
|
|
228
|
-
tokArr.dispose();
|
|
229
|
-
tokArr = null;
|
|
230
|
-
// Advance the matcher on the emitted token (fires the next async fill).
|
|
231
|
-
// EOS is never content and never grammar-valid — don't feed it.
|
|
232
|
-
if (grammar && !eos.includes(tok)) grammar.accept(tok);
|
|
233
|
-
if (history) {
|
|
234
|
-
const t1 = ops.fromInt32([tok], [1]);
|
|
235
|
-
const prev = history;
|
|
236
|
-
history = ops.concatAxis([prev, t1], 0);
|
|
237
|
-
prev.dispose();
|
|
238
|
-
t1.dispose();
|
|
239
|
-
}
|
|
240
|
-
return tok;
|
|
241
|
-
} finally {
|
|
242
|
-
if (cur !== logits1V) cur.dispose();
|
|
243
|
-
lp?.dispose();
|
|
244
|
-
tokArr?.dispose();
|
|
245
|
-
}
|
|
246
|
-
};
|
|
195
|
+
const samplePos = async (logits1V: MlxArray, step: number): Promise<number> =>
|
|
196
|
+
(await stepSampler.sample(logits1V, step)).token;
|
|
247
197
|
|
|
248
198
|
/** The [1,V] logits row at position `pos` of a hidden window — batched
|
|
249
199
|
* lm-head is applied by the caller ONCE; this slices its output. */
|
|
@@ -433,10 +383,14 @@ export async function specServeRun(
|
|
|
433
383
|
// KV would be woven into the live window (unrecoverable, upstream
|
|
434
384
|
// RAISES here, generate.py:529-533). Degrade to plain decode while the
|
|
435
385
|
// caches are still clean instead of 500ing a stream.
|
|
386
|
+
// Non-trimmable recurrent caches (SSMCache — gated-DeltaNet state) are
|
|
387
|
+
// spec-eligible through the round snapshot/replay contract instead:
|
|
388
|
+
// specRoundBegin before the verify forward, then commit or a bit-exact
|
|
389
|
+
// rollback(keep) after the accept walk.
|
|
436
390
|
const roundFits = caches.every((c) =>
|
|
437
391
|
"maxSize" in c && typeof (c as { maxSize?: number }).maxSize === "number"
|
|
438
392
|
? c.offset + n + 1 < (c as { maxSize: number }).maxSize
|
|
439
|
-
: c.isTrimmable(),
|
|
393
|
+
: c.isTrimmable() || typeof c.specRoundBegin === "function",
|
|
440
394
|
);
|
|
441
395
|
if (!roundFits) {
|
|
442
396
|
console.warn(
|
|
@@ -476,6 +430,9 @@ export async function specServeRun(
|
|
|
476
430
|
// (b) ONE target forward over [pending, ...drafts] (optionally tapped for
|
|
477
431
|
// DSpark's H_ctx). vHidden is retained past the accept walk: its slice at
|
|
478
432
|
// the emitted position is the next round's anchor hidden.
|
|
433
|
+
// Arm recurrent caches' spec round around the verify forward (no-op for
|
|
434
|
+
// trimmable caches, which roll back via trim() below).
|
|
435
|
+
for (const c of caches) c.specRoundBegin?.();
|
|
479
436
|
const vIds = ops.fromInt32([pending, ...drafts], [1, d + 1]);
|
|
480
437
|
const pinTarget = src.pinTargetKernelFamily === true &&
|
|
481
438
|
"setSpecKernelPinned" in model;
|
|
@@ -565,7 +522,18 @@ export async function specServeRun(
|
|
|
565
522
|
// position (the assistant borrows it; generate.ts:230 pattern — the slice
|
|
566
523
|
// outlives its parent's dispose).
|
|
567
524
|
if (!stop) {
|
|
568
|
-
|
|
525
|
+
// The verify window kept positions 0..kAccept (pending + accepted
|
|
526
|
+
// drafts). Trimmable caches drop the rejected tail; recurrent caches
|
|
527
|
+
// restore their pre-round snapshot and bit-exactly replay those
|
|
528
|
+
// kAccept+1 kept tokens. On full accept the round just commits.
|
|
529
|
+
if (kAccept < d) {
|
|
530
|
+
for (const c of caches) {
|
|
531
|
+
if (c.specRoundRollback) c.specRoundRollback(kAccept + 1);
|
|
532
|
+
else c.trim(d - kAccept);
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
for (const c of caches) c.specRoundCommit?.();
|
|
536
|
+
}
|
|
569
537
|
await src.commit(
|
|
570
538
|
d,
|
|
571
539
|
kAccept,
|
|
@@ -610,7 +578,7 @@ export async function specServeRun(
|
|
|
610
578
|
vCtxML?.dispose();
|
|
611
579
|
for (const c of caches) c.dispose();
|
|
612
580
|
source?.dispose();
|
|
613
|
-
|
|
581
|
+
stepSampler.dispose();
|
|
614
582
|
options.grammar?.dispose(); // Phase C will consume it; never leak either way
|
|
615
583
|
clearCache();
|
|
616
584
|
}
|