mlx-bun 0.0.11 → 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 +43 -13
- package/THIRD_PARTY_LICENSES.md +9 -0
- package/docs/design/colibri-glm52-port.md +1479 -0
- package/docs/design/pre-colibri-stabilization.md +329 -0
- package/docs/investigations/colibri-oracle-pin.md +1284 -0
- package/docs/investigations/indexshare-performance-spike.md +357 -0
- package/docs/planning/release-notes-v0.0.12.md +80 -0
- package/docs/reference/cli.md +28 -2
- package/docs/reference/distribution.md +25 -1
- package/docs/reference/embedding.md +5 -2
- package/docs/reference/features-matrix.md +4 -2
- package/docs/reference/library-api.md +41 -13
- package/docs/reference/models.md +49 -2
- package/docs/reference/server-api.md +90 -9
- package/docs/reference/server-config.md +21 -14
- package/docs/reference/training.md +10 -1
- package/package.json +1 -1
- package/src/anthropic.ts +54 -44
- package/src/chat-template.ts +155 -10
- package/src/cli.ts +127 -8
- package/src/config.ts +71 -26
- package/src/diffusion/diffusion-generate.ts +14 -8
- package/src/download.ts +103 -12
- package/src/eval/ifeval.ts +63 -120
- package/src/eval/runner.ts +14 -5
- package/src/eval/tasks/ifeval.ts +167 -31
- package/src/expert-io.ts +391 -0
- package/src/expert-residency.ts +1134 -0
- package/src/expert-usage.ts +415 -0
- package/src/generate.ts +114 -17
- package/src/hf-push.ts +1 -1
- package/src/index.ts +14 -2
- package/src/jobs/runner.ts +37 -18
- package/src/kv-store.ts +147 -8
- package/src/lora.ts +77 -14
- package/src/media-fetch.ts +198 -56
- package/src/mlx/array.ts +43 -0
- package/src/mlx/autograd.ts +51 -14
- package/src/mlx/checkpoint.ts +46 -13
- package/src/mlx/custom-vjp.ts +83 -35
- package/src/model/factory.ts +96 -5
- package/src/model/gemma4-base.ts +32 -0
- 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/support.ts +8 -1
- package/src/native/expert_io.c +523 -0
- package/src/native-pack.ts +6 -4
- package/src/responses.ts +62 -49
- package/src/serve/batch-scheduler.ts +95 -16
- package/src/serve/generation-gateway.ts +41 -16
- package/src/server.ts +362 -81
- package/src/spec/glm52-mtp-source.ts +392 -0
- package/src/spec/serve-loop.ts +99 -18
- package/src/spec/source.ts +31 -6
- package/src/ssd-cache.ts +6 -2
- package/src/tool-call.ts +30 -2
- package/src/train/job.ts +0 -10
- package/src/train/lora-params.ts +17 -6
- package/src/train/trainer.ts +59 -26
- package/src/web/app.html +13 -2
- package/src/web/app.js +30 -3
- package/src/web/src/status.ts +51 -3
- package/src/web-tools.ts +54 -2
- package/src/weights.ts +82 -19
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// Native GLM-5.2 MTP draft source.
|
|
2
|
+
//
|
|
3
|
+
// The target and MTP row share embeddings, the output head, and the resident
|
|
4
|
+
// dense target weights. Only the MTP layer's own dense tensors and int8 routed
|
|
5
|
+
// experts add model weight residency. The generic speculative serve loop owns
|
|
6
|
+
// verification, grammar composition, target-cache rollback, sampling, and
|
|
7
|
+
// telemetry. Prompt lookup remains the model-free alternative DraftProvider;
|
|
8
|
+
// selecting one provider per request prevents two independent draft histories
|
|
9
|
+
// from advancing the target or grammar state twice.
|
|
10
|
+
|
|
11
|
+
import { MlxArray } from "../mlx/array";
|
|
12
|
+
import * as ops from "../mlx/ops";
|
|
13
|
+
import { toLogprobs } from "../sampler";
|
|
14
|
+
import { Glm52DecoderLayer, Glm52Model } from "../model/glm52";
|
|
15
|
+
import { MLACache } from "../model/glm52-cache";
|
|
16
|
+
import { rmsNormF32Mlx } from "../model/glm52-mla";
|
|
17
|
+
import type { DraftProvider, DraftSource } from "./source";
|
|
18
|
+
|
|
19
|
+
type Sampler = (logprobs: MlxArray, step: number) => MlxArray;
|
|
20
|
+
|
|
21
|
+
function lastToken(feed: readonly number[]): number {
|
|
22
|
+
const token = feed.at(-1);
|
|
23
|
+
if (!Number.isSafeInteger(token) || token! < 0)
|
|
24
|
+
throw new Error("native MTP requires a non-empty non-negative token feed");
|
|
25
|
+
return token!;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Glm52NativeMtpProvider implements DraftProvider {
|
|
29
|
+
readonly id = "glm52-native-mtp";
|
|
30
|
+
readonly weightsBytes = 0;
|
|
31
|
+
readonly #layer: Glm52DecoderLayer;
|
|
32
|
+
|
|
33
|
+
constructor(readonly model: Glm52Model) {
|
|
34
|
+
if (!model.capabilities.mtpMetadata)
|
|
35
|
+
throw new Error("GLM-5.2 artifact does not contain a complete MTP row");
|
|
36
|
+
if (model.capabilities.mtpEnabled === false)
|
|
37
|
+
throw new Error("native GLM-5.2 MTP is disabled for this model instance");
|
|
38
|
+
if (model.glmConfig.numNextnPredictLayers !== 1) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`native GLM-5.2 MTP requires exactly one next-token layer; got ` +
|
|
41
|
+
`${model.glmConfig.numNextnPredictLayers}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const mtpBackend = model.expertRuntime?.mtpExecutor ?? null;
|
|
45
|
+
if (model.expertBackend && !mtpBackend) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"streamed GLM-5.2 MTP requires the bounded int8 MTP expert tier",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
this.#layer = new Glm52DecoderLayer(
|
|
51
|
+
model.glmConfig,
|
|
52
|
+
model.weights,
|
|
53
|
+
model.glmConfig.numHiddenLayers,
|
|
54
|
+
false,
|
|
55
|
+
mtpBackend,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
|
|
60
|
+
if (opts.target.model !== this.model)
|
|
61
|
+
throw new Error("native MTP provider was opened for a different target");
|
|
62
|
+
return new Glm52NativeMtpSource(this.model, this.#layer, opts.sampler);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dispose(): void {
|
|
66
|
+
// The target model owns every shared/MTP weight and the expert tier.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class Glm52NativeMtpSource implements DraftSource {
|
|
71
|
+
readonly prefillMode = "full" as const;
|
|
72
|
+
readonly pinTargetKernelFamily = true;
|
|
73
|
+
readonly weightsBytes = 0;
|
|
74
|
+
#cache: MLACache;
|
|
75
|
+
#roundStart = 0;
|
|
76
|
+
#lastDraftCount = 0;
|
|
77
|
+
#closed = false;
|
|
78
|
+
|
|
79
|
+
constructor(
|
|
80
|
+
readonly model: Glm52Model,
|
|
81
|
+
readonly layer: Glm52DecoderLayer,
|
|
82
|
+
readonly sampler: Sampler,
|
|
83
|
+
) {
|
|
84
|
+
this.#cache = new MLACache({
|
|
85
|
+
kvLoraRank: model.glmConfig.kvLoraRank,
|
|
86
|
+
ropeHeadDim: model.glmConfig.qkRopeHeadDim,
|
|
87
|
+
maxTokens: model.glmConfig.maxPositionEmbeddings,
|
|
88
|
+
role: "mtp",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get cacheOffset(): number {
|
|
93
|
+
return this.#cache.offset;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get cacheBytes(): number {
|
|
97
|
+
return this.#cache.byteLength;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Caller-owned zero-copy snapshot for the v3 KV persistence writer. */
|
|
101
|
+
clonePersistentCache(): MLACache {
|
|
102
|
+
this.#checkOpen();
|
|
103
|
+
if (this.#lastDraftCount !== 0)
|
|
104
|
+
throw new Error("native MTP cache cannot snapshot an uncommitted round");
|
|
105
|
+
if (this.#cache.offset === 0)
|
|
106
|
+
throw new Error("native MTP cache cannot snapshot empty state");
|
|
107
|
+
const state = this.#cache.fetch();
|
|
108
|
+
const clone = new MLACache({
|
|
109
|
+
kvLoraRank: this.#cache.kvLoraRank,
|
|
110
|
+
ropeHeadDim: this.#cache.ropeHeadDim,
|
|
111
|
+
maxTokens: this.#cache.maxTokens,
|
|
112
|
+
role: "mtp",
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
clone.restoreCompressedState(state.latent, state.rope, null, this.#cache.offset);
|
|
116
|
+
return clone;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
state.latent.dispose();
|
|
119
|
+
state.rope.dispose();
|
|
120
|
+
clone.dispose();
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Adopt one restored `mtp-mla` cache. Ownership transfers only after every
|
|
127
|
+
* role/geometry/offset check passes; the source disposes it thereafter.
|
|
128
|
+
*/
|
|
129
|
+
restorePersistentCache(cache: MLACache): void {
|
|
130
|
+
this.#checkOpen();
|
|
131
|
+
if (this.#lastDraftCount !== 0)
|
|
132
|
+
throw new Error("native MTP cache cannot restore during a draft round");
|
|
133
|
+
if (this.#cache.offset !== 0)
|
|
134
|
+
throw new Error("native MTP cache restore requires an empty source");
|
|
135
|
+
if (cache.role !== "mtp" || cache.dsa)
|
|
136
|
+
throw new Error("native MTP restore requires one mtp-mla cache");
|
|
137
|
+
if (
|
|
138
|
+
cache.kvLoraRank !== this.#cache.kvLoraRank ||
|
|
139
|
+
cache.ropeHeadDim !== this.#cache.ropeHeadDim ||
|
|
140
|
+
cache.maxTokens !== this.#cache.maxTokens
|
|
141
|
+
) {
|
|
142
|
+
throw new Error("native MTP restored cache geometry does not match model");
|
|
143
|
+
}
|
|
144
|
+
if (cache.offset <= 0 || cache.batchSize !== 1)
|
|
145
|
+
throw new Error("native MTP restored cache must contain one non-empty row");
|
|
146
|
+
this.#cache.dispose();
|
|
147
|
+
this.#cache = cache;
|
|
148
|
+
this.#roundStart = cache.offset;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
prefill(_promptIds: number[]): void {
|
|
152
|
+
this.#checkOpen();
|
|
153
|
+
if (this.#cache.offset !== 0)
|
|
154
|
+
throw new Error("native MTP source cannot be prefilled twice");
|
|
155
|
+
// Direct Colibri intentionally leaves MTP KV empty here ("niente
|
|
156
|
+
// prefill"): its first draft opens a decode-only window from the target
|
|
157
|
+
// anchor hidden and pending token. Seeding this cache with the prompt
|
|
158
|
+
// would change both the oracle state and the first speculative round.
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async draft(
|
|
162
|
+
feed: number[],
|
|
163
|
+
n: number,
|
|
164
|
+
stepBase: number,
|
|
165
|
+
anchorHidden?: MlxArray,
|
|
166
|
+
): Promise<number[]> {
|
|
167
|
+
this.#checkOpen();
|
|
168
|
+
if (!anchorHidden)
|
|
169
|
+
throw new Error("native MTP drafting requires the target anchor hidden");
|
|
170
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
171
|
+
throw new Error(`native MTP draft length must be non-negative; got ${n}`);
|
|
172
|
+
if (this.#lastDraftCount !== 0)
|
|
173
|
+
throw new Error("native MTP draft called before the prior round committed");
|
|
174
|
+
const [batch, tokens, hidden] = anchorHidden.shape;
|
|
175
|
+
if (batch !== 1 || tokens !== 1 || hidden !== this.model.glmConfig.hiddenSize) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`native MTP anchor must be [1,1,${this.model.glmConfig.hiddenSize}], ` +
|
|
178
|
+
`got [${anchorHidden.shape.join(",")}]`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.#roundStart = this.#cache.offset;
|
|
183
|
+
let token = lastToken(feed);
|
|
184
|
+
let state: MlxArray | null = ops.contiguous(anchorHidden);
|
|
185
|
+
const drafts: number[] = [];
|
|
186
|
+
try {
|
|
187
|
+
for (let position = 0; position < n; position++) {
|
|
188
|
+
const nextState = await this.#step(token, state, stepBase + position);
|
|
189
|
+
state.dispose();
|
|
190
|
+
state = nextState.hidden;
|
|
191
|
+
token = nextState.token;
|
|
192
|
+
drafts.push(token);
|
|
193
|
+
}
|
|
194
|
+
this.#lastDraftCount = drafts.length;
|
|
195
|
+
return drafts;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const appended = this.#cache.offset - this.#roundStart;
|
|
198
|
+
if (appended > 0) this.#cache.trim(appended);
|
|
199
|
+
throw error;
|
|
200
|
+
} finally {
|
|
201
|
+
state?.dispose();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async commit(
|
|
206
|
+
d: number,
|
|
207
|
+
kAccept: number,
|
|
208
|
+
vCtxML?: MlxArray,
|
|
209
|
+
verifiedHidden?: MlxArray,
|
|
210
|
+
acceptedTokens: readonly number[] = [],
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
this.#checkOpen();
|
|
213
|
+
vCtxML?.dispose();
|
|
214
|
+
if (d !== this.#lastDraftCount)
|
|
215
|
+
throw new Error(`native MTP commit d=${d}, expected ${this.#lastDraftCount}`);
|
|
216
|
+
if (!Number.isSafeInteger(kAccept) || kAccept < 0 || kAccept > d)
|
|
217
|
+
throw new Error(`native MTP accepted ${kAccept} outside [0,${d}]`);
|
|
218
|
+
if (acceptedTokens.length !== kAccept)
|
|
219
|
+
throw new Error(
|
|
220
|
+
`native MTP received ${acceptedTokens.length} accepted tokens for k=${kAccept}`,
|
|
221
|
+
);
|
|
222
|
+
if (kAccept > 0 && !verifiedHidden)
|
|
223
|
+
throw new Error("native MTP absorption requires verified target hidden rows");
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
// Only the first speculative MTP row is conditioned on a true target
|
|
227
|
+
// hidden. Later rows use earlier MTP hiddens, so accepted positions are
|
|
228
|
+
// rebuilt below from the target's verified hidden window.
|
|
229
|
+
const speculativeTail = Math.max(d - 1, 0);
|
|
230
|
+
if (speculativeTail > 0) this.#cache.trim(speculativeTail);
|
|
231
|
+
if (kAccept > 0)
|
|
232
|
+
await this.#absorb(acceptedTokens, verifiedHidden!);
|
|
233
|
+
const expected = this.#roundStart + (d > 0 ? 1 : 0) + kAccept;
|
|
234
|
+
if (this.#cache.offset !== expected) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`native MTP cache committed to ${this.#cache.offset}, expected ${expected}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const appended = this.#cache.offset - this.#roundStart;
|
|
241
|
+
if (appended > 0) this.#cache.trim(appended);
|
|
242
|
+
throw error;
|
|
243
|
+
} finally {
|
|
244
|
+
this.#lastDraftCount = 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
dispose(): void {
|
|
249
|
+
if (this.#closed) return;
|
|
250
|
+
this.#closed = true;
|
|
251
|
+
this.#cache.dispose();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async #step(
|
|
255
|
+
token: number,
|
|
256
|
+
hidden: MlxArray,
|
|
257
|
+
sampleStep: number,
|
|
258
|
+
): Promise<{ hidden: MlxArray; token: number }> {
|
|
259
|
+
const config = this.model.glmConfig;
|
|
260
|
+
const ids = ops.fromInt32([token], [1, 1]);
|
|
261
|
+
let embedded: MlxArray | null = null;
|
|
262
|
+
let embeddedNorm: MlxArray | null = null;
|
|
263
|
+
let hiddenNorm: MlxArray | null = null;
|
|
264
|
+
let joined: MlxArray | null = null;
|
|
265
|
+
let projected: MlxArray | null = null;
|
|
266
|
+
let output: MlxArray | null = null;
|
|
267
|
+
let headInput: MlxArray | null = null;
|
|
268
|
+
let logits: MlxArray | null = null;
|
|
269
|
+
let logprobs: MlxArray | null = null;
|
|
270
|
+
let sampled: MlxArray | null = null;
|
|
271
|
+
try {
|
|
272
|
+
embedded = this.model.weights.embedding(
|
|
273
|
+
ids,
|
|
274
|
+
"model.embed_tokens.weight",
|
|
275
|
+
config.vocabSize,
|
|
276
|
+
config.hiddenSize,
|
|
277
|
+
);
|
|
278
|
+
embeddedNorm = rmsNormF32Mlx(
|
|
279
|
+
embedded,
|
|
280
|
+
this.model.weights.tensor(
|
|
281
|
+
`model.layers.${config.numHiddenLayers}.enorm.weight`,
|
|
282
|
+
),
|
|
283
|
+
config.rmsNormEps,
|
|
284
|
+
);
|
|
285
|
+
hiddenNorm = rmsNormF32Mlx(
|
|
286
|
+
hidden,
|
|
287
|
+
this.model.weights.tensor(
|
|
288
|
+
`model.layers.${config.numHiddenLayers}.hnorm.weight`,
|
|
289
|
+
),
|
|
290
|
+
config.rmsNormEps,
|
|
291
|
+
);
|
|
292
|
+
joined = ops.concatAxis([embeddedNorm, hiddenNorm], 2);
|
|
293
|
+
projected = this.model.weights.linear(
|
|
294
|
+
joined,
|
|
295
|
+
`model.layers.${config.numHiddenLayers}.eh_proj.weight`,
|
|
296
|
+
config.hiddenSize,
|
|
297
|
+
2 * config.hiddenSize,
|
|
298
|
+
);
|
|
299
|
+
output = await this.layer.forwardAsync(projected, this.#cache, null);
|
|
300
|
+
headInput = rmsNormF32Mlx(
|
|
301
|
+
output,
|
|
302
|
+
this.model.weights.tensor(
|
|
303
|
+
`model.layers.${config.numHiddenLayers}.shared_head.norm.weight`,
|
|
304
|
+
),
|
|
305
|
+
config.rmsNormEps,
|
|
306
|
+
);
|
|
307
|
+
logits = this.model.logitsFromHidden(headInput);
|
|
308
|
+
logprobs = toLogprobs(logits);
|
|
309
|
+
sampled = this.sampler(logprobs, sampleStep);
|
|
310
|
+
return {
|
|
311
|
+
hidden: ops.contiguous(output),
|
|
312
|
+
token: ops.itemUint32(sampled),
|
|
313
|
+
};
|
|
314
|
+
} finally {
|
|
315
|
+
ids.dispose();
|
|
316
|
+
embedded?.dispose();
|
|
317
|
+
embeddedNorm?.dispose();
|
|
318
|
+
hiddenNorm?.dispose();
|
|
319
|
+
joined?.dispose();
|
|
320
|
+
projected?.dispose();
|
|
321
|
+
output?.dispose();
|
|
322
|
+
headInput?.dispose();
|
|
323
|
+
logits?.dispose();
|
|
324
|
+
logprobs?.dispose();
|
|
325
|
+
sampled?.dispose();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async #absorb(
|
|
330
|
+
acceptedTokens: readonly number[],
|
|
331
|
+
verifiedHidden: MlxArray,
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
const config = this.model.glmConfig;
|
|
334
|
+
const k = acceptedTokens.length;
|
|
335
|
+
const ids = ops.fromInt32([...acceptedTokens], [1, k]);
|
|
336
|
+
let embedded: MlxArray | null = null;
|
|
337
|
+
let embeddedNorm: MlxArray | null = null;
|
|
338
|
+
let trueRows: MlxArray | null = null;
|
|
339
|
+
let hiddenNorm: MlxArray | null = null;
|
|
340
|
+
let joined: MlxArray | null = null;
|
|
341
|
+
let projected: MlxArray | null = null;
|
|
342
|
+
let output: MlxArray | null = null;
|
|
343
|
+
try {
|
|
344
|
+
embedded = this.model.weights.embedding(
|
|
345
|
+
ids,
|
|
346
|
+
"model.embed_tokens.weight",
|
|
347
|
+
config.vocabSize,
|
|
348
|
+
config.hiddenSize,
|
|
349
|
+
);
|
|
350
|
+
embeddedNorm = rmsNormF32Mlx(
|
|
351
|
+
embedded,
|
|
352
|
+
this.model.weights.tensor(
|
|
353
|
+
`model.layers.${config.numHiddenLayers}.enorm.weight`,
|
|
354
|
+
),
|
|
355
|
+
config.rmsNormEps,
|
|
356
|
+
);
|
|
357
|
+
trueRows = verifiedHidden.slice(
|
|
358
|
+
[0, 0, 0],
|
|
359
|
+
[1, k, config.hiddenSize],
|
|
360
|
+
);
|
|
361
|
+
hiddenNorm = rmsNormF32Mlx(
|
|
362
|
+
trueRows,
|
|
363
|
+
this.model.weights.tensor(
|
|
364
|
+
`model.layers.${config.numHiddenLayers}.hnorm.weight`,
|
|
365
|
+
),
|
|
366
|
+
config.rmsNormEps,
|
|
367
|
+
);
|
|
368
|
+
joined = ops.concatAxis([embeddedNorm, hiddenNorm], 2);
|
|
369
|
+
projected = this.model.weights.linear(
|
|
370
|
+
joined,
|
|
371
|
+
`model.layers.${config.numHiddenLayers}.eh_proj.weight`,
|
|
372
|
+
config.hiddenSize,
|
|
373
|
+
2 * config.hiddenSize,
|
|
374
|
+
);
|
|
375
|
+
output = await this.layer.forwardAsync(projected, this.#cache, null);
|
|
376
|
+
output.eval();
|
|
377
|
+
} finally {
|
|
378
|
+
ids.dispose();
|
|
379
|
+
embedded?.dispose();
|
|
380
|
+
embeddedNorm?.dispose();
|
|
381
|
+
trueRows?.dispose();
|
|
382
|
+
hiddenNorm?.dispose();
|
|
383
|
+
joined?.dispose();
|
|
384
|
+
projected?.dispose();
|
|
385
|
+
output?.dispose();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#checkOpen(): void {
|
|
390
|
+
if (this.#closed) throw new Error("native MTP source is closed");
|
|
391
|
+
}
|
|
392
|
+
}
|
package/src/spec/serve-loop.ts
CHANGED
|
@@ -57,13 +57,24 @@ const PREFILL_CHUNK = 2048;
|
|
|
57
57
|
* [1,L,m*H] (tapLayers concatenated on the feature axis; index nLayers is the
|
|
58
58
|
* post-finalNorm sentinel). Non-tapping sources get ctxML=null and never
|
|
59
59
|
* touch model.hiddenTap. Mirrors generate-dflash.ts forwardTapped. */
|
|
60
|
-
function forwardMaybeTap(
|
|
60
|
+
async function forwardMaybeTap(
|
|
61
61
|
model: RuntimeModel,
|
|
62
62
|
ids: MlxArray,
|
|
63
63
|
caches: Cache[],
|
|
64
64
|
tapLayers: number[] | undefined,
|
|
65
|
-
): { hidden: MlxArray; ctxML: MlxArray | null } {
|
|
66
|
-
if (!tapLayers)
|
|
65
|
+
): Promise<{ hidden: MlxArray; ctxML: MlxArray | null }> {
|
|
66
|
+
if (!tapLayers) {
|
|
67
|
+
const asyncModel = model as RuntimeModel & {
|
|
68
|
+
forwardHiddenAsync?: (
|
|
69
|
+
ids: MlxArray,
|
|
70
|
+
caches: Cache[],
|
|
71
|
+
) => Promise<MlxArray>;
|
|
72
|
+
};
|
|
73
|
+
const hidden = typeof asyncModel.forwardHiddenAsync === "function"
|
|
74
|
+
? await asyncModel.forwardHiddenAsync(ids, caches)
|
|
75
|
+
: model.forwardHidden(ids, caches);
|
|
76
|
+
return { hidden, ctxML: null };
|
|
77
|
+
}
|
|
67
78
|
const m = model as unknown as {
|
|
68
79
|
hiddenTap: { layers: Set<number>; captured: Map<number, MlxArray> } | null;
|
|
69
80
|
};
|
|
@@ -96,7 +107,12 @@ function forwardMaybeTap(
|
|
|
96
107
|
export interface SpecServeExtras {
|
|
97
108
|
drafted: number;
|
|
98
109
|
accepted: number;
|
|
110
|
+
rejected: number;
|
|
99
111
|
targetCalls: number;
|
|
112
|
+
rounds: number;
|
|
113
|
+
acceptanceLengths: number[];
|
|
114
|
+
tokensPerForward: number;
|
|
115
|
+
forwardsSaved: number;
|
|
100
116
|
/** Per-draft-position counters (index = position within a round's block,
|
|
101
117
|
* 0..γ-1): how many rounds drafted/accepted at that position. Drives the
|
|
102
118
|
* Phase-1c per-position acceptance report (scripts/dspark-drafter-ab.ts)
|
|
@@ -149,7 +165,16 @@ export async function specServeRun(
|
|
|
149
165
|
processors.length > 0 ? ops.fromInt32(promptIds, [promptIds.length]) : null;
|
|
150
166
|
|
|
151
167
|
const extras: SpecServeExtras = {
|
|
152
|
-
drafted: 0,
|
|
168
|
+
drafted: 0,
|
|
169
|
+
accepted: 0,
|
|
170
|
+
rejected: 0,
|
|
171
|
+
targetCalls: 0,
|
|
172
|
+
rounds: 0,
|
|
173
|
+
acceptanceLengths: [],
|
|
174
|
+
tokensPerForward: 0,
|
|
175
|
+
forwardsSaved: 0,
|
|
176
|
+
draftedByPos: [],
|
|
177
|
+
acceptedByPos: [],
|
|
153
178
|
};
|
|
154
179
|
const stats: GenerateStats = {
|
|
155
180
|
promptTokens: promptIds.length,
|
|
@@ -263,7 +288,9 @@ export async function specServeRun(
|
|
|
263
288
|
// assistant source's first-round anchor hidden = "the hidden that
|
|
264
289
|
// produced pending" = position len-2, the last drain chunk's final row.
|
|
265
290
|
const t0 = performance.now();
|
|
266
|
-
const tailSplit =
|
|
291
|
+
const tailSplit =
|
|
292
|
+
src.prefillMode !== "full" &&
|
|
293
|
+
flagOn("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
267
294
|
const oracleShape = tailSplit && promptIds.length > 1;
|
|
268
295
|
if (oracleShape) {
|
|
269
296
|
let pos = 0;
|
|
@@ -272,7 +299,12 @@ export async function specServeRun(
|
|
|
272
299
|
const n = Math.min(PREFILL_CHUNK, end - pos);
|
|
273
300
|
const chunk = promptIds.slice(pos, pos + n);
|
|
274
301
|
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
275
|
-
const { hidden: h, ctxML } = forwardMaybeTap(
|
|
302
|
+
const { hidden: h, ctxML } = await forwardMaybeTap(
|
|
303
|
+
model,
|
|
304
|
+
ids,
|
|
305
|
+
caches,
|
|
306
|
+
tapLayers,
|
|
307
|
+
);
|
|
276
308
|
ids.dispose();
|
|
277
309
|
if (ctxML) ctxParts.push(ctxML);
|
|
278
310
|
if (pos + n >= end) {
|
|
@@ -290,7 +322,12 @@ export async function specServeRun(
|
|
|
290
322
|
for (let off = 0; off < promptIds.length; off += PREFILL_CHUNK) {
|
|
291
323
|
const chunk = promptIds.slice(off, off + PREFILL_CHUNK);
|
|
292
324
|
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
293
|
-
const { hidden: h, ctxML } = forwardMaybeTap(
|
|
325
|
+
const { hidden: h, ctxML } = await forwardMaybeTap(
|
|
326
|
+
model,
|
|
327
|
+
ids,
|
|
328
|
+
caches,
|
|
329
|
+
tapLayers,
|
|
330
|
+
);
|
|
294
331
|
ids.dispose();
|
|
295
332
|
if (ctxML) ctxParts.push(ctxML);
|
|
296
333
|
if (off + PREFILL_CHUNK >= promptIds.length) {
|
|
@@ -317,10 +354,10 @@ export async function specServeRun(
|
|
|
317
354
|
for (const p of ctxParts) p.dispose();
|
|
318
355
|
}
|
|
319
356
|
ctxParts.length = 0; // parts consumed (transferred as prefillCtx or disposed)
|
|
320
|
-
src.prefill(promptIds, prefillCtx ?? undefined); // takes ownership of prefillCtx
|
|
357
|
+
await src.prefill(promptIds, prefillCtx ?? undefined); // takes ownership of prefillCtx
|
|
321
358
|
prefillCtx = null; // ownership transferred
|
|
322
359
|
} else {
|
|
323
|
-
src.prefill(promptIds);
|
|
360
|
+
await src.prefill(promptIds);
|
|
324
361
|
}
|
|
325
362
|
stats.prefillMs = performance.now() - t0;
|
|
326
363
|
stats.prefillTps = (promptIds.length / Math.max(stats.prefillMs, 1e-6)) * 1000;
|
|
@@ -363,7 +400,12 @@ export async function specServeRun(
|
|
|
363
400
|
if (!speculating) {
|
|
364
401
|
// plain single-token continuation (post-ring-wrap fallback)
|
|
365
402
|
const ids = ops.fromInt32([pending], [1, 1]);
|
|
366
|
-
const h =
|
|
403
|
+
const { hidden: h } = await forwardMaybeTap(
|
|
404
|
+
model,
|
|
405
|
+
ids,
|
|
406
|
+
caches,
|
|
407
|
+
undefined,
|
|
408
|
+
);
|
|
367
409
|
ids.dispose();
|
|
368
410
|
const lg = model.logitsFromHidden(h);
|
|
369
411
|
h.dispose();
|
|
@@ -406,11 +448,22 @@ export async function specServeRun(
|
|
|
406
448
|
|
|
407
449
|
// (a) draft UP TO n tokens (request sampler, mlx-lm parity). The return
|
|
408
450
|
// length d is authoritative — DSpark's confidence scheduler may prune the
|
|
409
|
-
// block short
|
|
451
|
+
// block short or skip it entirely. The assistant source
|
|
410
452
|
// borrows the target's anchor hidden; two-model/dflash ignore it.
|
|
411
|
-
const drafts = src.draft(
|
|
453
|
+
const drafts = await src.draft(
|
|
454
|
+
feed,
|
|
455
|
+
n,
|
|
456
|
+
stats.generatedTokens,
|
|
457
|
+
anchorHidden ?? undefined,
|
|
458
|
+
);
|
|
412
459
|
const d = drafts.length;
|
|
413
460
|
if (d < 0 || d > n) throw new Error(`DraftSource returned ${d} drafts (contract: 0..${n})`);
|
|
461
|
+
if (Bun.env.MLX_BUN_SPEC_TRACE === "1") {
|
|
462
|
+
console.error(
|
|
463
|
+
`[SPEC_TRACE] round=${extras.rounds + 1} ` +
|
|
464
|
+
`feed=${feed.join(",")} drafts=${drafts.join(",")}`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
414
467
|
// d === 0 (a confidence scheduler skipping the round, DeepSpec ℓ=0
|
|
415
468
|
// semantics) needs NO special case: the verify window degenerates to
|
|
416
469
|
// [pending] alone, the accept walk to the single bonus position, commit
|
|
@@ -424,12 +477,24 @@ export async function specServeRun(
|
|
|
424
477
|
// DSpark's H_ctx). vHidden is retained past the accept walk: its slice at
|
|
425
478
|
// the emitted position is the next round's anchor hidden.
|
|
426
479
|
const vIds = ops.fromInt32([pending, ...drafts], [1, d + 1]);
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
480
|
+
const pinTarget = src.pinTargetKernelFamily === true &&
|
|
481
|
+
"setSpecKernelPinned" in model;
|
|
482
|
+
if (pinTarget) model.setSpecKernelPinned(true);
|
|
483
|
+
try {
|
|
484
|
+
const ft = await forwardMaybeTap(model, vIds, caches, tapLayers);
|
|
485
|
+
// Assign these before the lm-head call so the outer cleanup owns them
|
|
486
|
+
// even if logits construction throws.
|
|
487
|
+
vHidden = ft.hidden;
|
|
488
|
+
vCtxML = ft.ctxML;
|
|
489
|
+
vLogits = model.logitsFromHidden(vHidden);
|
|
490
|
+
} finally {
|
|
491
|
+
vIds.dispose();
|
|
492
|
+
if (pinTarget) model.setSpecKernelPinned(false);
|
|
493
|
+
}
|
|
431
494
|
extras.targetCalls++;
|
|
432
|
-
|
|
495
|
+
// The lm-head remains one logical batched operation. Native GLM MTP's
|
|
496
|
+
// SPEC_PIN implementation evaluates its rows through the same M=1
|
|
497
|
+
// quantized family before concatenating them.
|
|
433
498
|
|
|
434
499
|
// (c) per-position accept walk: sample the target at each position,
|
|
435
500
|
// accept while it reproduces the draft. Sampling is sequential because
|
|
@@ -471,6 +536,9 @@ export async function specServeRun(
|
|
|
471
536
|
vLogits!.dispose();
|
|
472
537
|
vLogits = null;
|
|
473
538
|
extras.accepted += kAccept;
|
|
539
|
+
extras.rejected = extras.drafted - extras.accepted;
|
|
540
|
+
extras.rounds++;
|
|
541
|
+
extras.acceptanceLengths.push(kAccept);
|
|
474
542
|
for (let i = 0; i < kAccept; i++)
|
|
475
543
|
extras.acceptedByPos[i] = (extras.acceptedByPos[i] ?? 0) + 1;
|
|
476
544
|
clearCache();
|
|
@@ -481,6 +549,13 @@ export async function specServeRun(
|
|
|
481
549
|
if ((await onToken(tok)) === false) { halted = true; break; }
|
|
482
550
|
if (stats.generatedTokens >= maxTokens) break;
|
|
483
551
|
}
|
|
552
|
+
extras.tokensPerForward = extras.rounds > 0
|
|
553
|
+
? stats.generatedTokens / extras.rounds
|
|
554
|
+
: 0;
|
|
555
|
+
extras.forwardsSaved = Math.max(
|
|
556
|
+
0,
|
|
557
|
+
Math.max(0, stats.generatedTokens - 1) - extras.rounds,
|
|
558
|
+
);
|
|
484
559
|
const stop = sawEos || halted || grammarDone || stats.generatedTokens >= maxTokens;
|
|
485
560
|
|
|
486
561
|
// (e) roll back the rejected suffix + commit to the source (skipped on
|
|
@@ -491,7 +566,13 @@ export async function specServeRun(
|
|
|
491
566
|
// outlives its parent's dispose).
|
|
492
567
|
if (!stop) {
|
|
493
568
|
if (kAccept < d) for (const c of caches) c.trim(d - kAccept);
|
|
494
|
-
src.commit(
|
|
569
|
+
await src.commit(
|
|
570
|
+
d,
|
|
571
|
+
kAccept,
|
|
572
|
+
vCtxML ?? undefined,
|
|
573
|
+
vHidden,
|
|
574
|
+
drafts.slice(0, kAccept),
|
|
575
|
+
); // takes ownership of vCtxML; verifiedHidden stays caller-owned
|
|
495
576
|
vCtxML = null;
|
|
496
577
|
const H = vHidden!.shape[2]!;
|
|
497
578
|
anchorHidden?.dispose();
|
package/src/spec/source.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// DraftSource — the seam between the serve-time speculative verify loop
|
|
2
2
|
// (src/spec/serve-loop.ts) and whatever produces draft tokens. Designed in
|
|
3
|
-
// docs/design/mlx-lm-tool-parity-plan.md §7 so
|
|
3
|
+
// docs/design/mlx-lm-tool-parity-plan.md §7 so every drafter shares ONE
|
|
4
4
|
// verify/accept executor:
|
|
5
5
|
// - TwoModelSource (src/spec/two-model.ts) — mlx-lm parity (L1 oracle:
|
|
6
6
|
// mlx_lm.server --draft-model), a full second model. Ignores `target`.
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
// - DflashSource (src/spec/dflash-source.ts) — DSpark (L3, KL/quality-gated).
|
|
11
11
|
// Taps the target's multi-layer hiddens (prefill + verify) into a growing
|
|
12
12
|
// H_ctx (docs/design/dspark-speculative-decoding.md).
|
|
13
|
+
// - NgramSource (src/spec/ngram-source.ts) — model-free prompt lookup.
|
|
14
|
+
// - Glm52NativeMtpSource (src/spec/glm52-mtp-source.ts) — the target
|
|
15
|
+
// artifact's native Colibri MTP row.
|
|
13
16
|
//
|
|
14
|
-
// The
|
|
17
|
+
// The sources differ ONLY in what fills the draft; the serve loop, admission
|
|
15
18
|
// accounting, and stats never change. The KV-borrowing sources need target
|
|
16
19
|
// state the two-model source doesn't — carried by the `target` view at open()
|
|
17
20
|
// and the optional prefill/draft/commit arguments below (all no-ops for
|
|
@@ -33,6 +36,14 @@ export interface TargetView {
|
|
|
33
36
|
/** A per-request draft-token producer. Created per generation (owns its own
|
|
34
37
|
* draft-side state), disposed by the serve loop's finally. */
|
|
35
38
|
export interface DraftSource {
|
|
39
|
+
/** Target prefill shape required by this source's oracle. Most mlx-lm
|
|
40
|
+
* sources leave the final prompt token pending; native Colibri MTP starts
|
|
41
|
+
* from a full-prompt target forward. */
|
|
42
|
+
readonly prefillMode?: "tail-split" | "full";
|
|
43
|
+
/** Request one fixed target kernel family across the speculative verify
|
|
44
|
+
* batch. Native GLM MTP uses the direct-Colibri SPEC_PIN contract. */
|
|
45
|
+
readonly pinTargetKernelFamily?: boolean;
|
|
46
|
+
|
|
36
47
|
/** Multi-layer target tap the source needs captured on the target's prefill
|
|
37
48
|
* AND every verify forward (DSpark's H_ctx; e4b {20,31,41,42}). When set,
|
|
38
49
|
* the serve loop sets model.hiddenTap around those forwards and passes the
|
|
@@ -44,7 +55,7 @@ export interface DraftSource {
|
|
|
44
55
|
* assistant/dflash: read the target's state — mostly a no-op, but DSpark
|
|
45
56
|
* seeds H_ctx from `ctxML`, the tapped prefill context [1,Lp,m*H], present
|
|
46
57
|
* iff tapLayers is set). */
|
|
47
|
-
prefill(promptIds: number[], ctxML?: MlxArray): void
|
|
58
|
+
prefill(promptIds: number[], ctxML?: MlxArray): void | Promise<void>;
|
|
48
59
|
|
|
49
60
|
/** Propose 0..n tokens (RETURN LENGTH IS AUTHORITATIVE — a source may
|
|
50
61
|
* return fewer than n, e.g. DSpark's confidence-scheduled draft-length
|
|
@@ -59,15 +70,29 @@ export interface DraftSource {
|
|
|
59
70
|
* `anchorHidden` is the target's final hidden [1,1,H] at the pending/anchor
|
|
60
71
|
* position — the assistant source borrows it for its first draft step;
|
|
61
72
|
* two-model and dflash ignore it. */
|
|
62
|
-
draft(
|
|
73
|
+
draft(
|
|
74
|
+
feed: number[],
|
|
75
|
+
n: number,
|
|
76
|
+
stepBase: number,
|
|
77
|
+
anchorHidden?: MlxArray,
|
|
78
|
+
): number[] | Promise<number[]>;
|
|
63
79
|
|
|
64
80
|
/** Verify outcome for the last round: kAccept of d accepted, where d is the
|
|
65
81
|
* length draft() actually RETURNED (≤ n). Two-model: trim the draft cache
|
|
66
82
|
* by max(d - kAccept - 1, 0) — mlx-lm's rewind rule (generate.py:589-591).
|
|
67
83
|
* DSpark: grow H_ctx by the accepted window from `vCtxML`, the verified
|
|
68
84
|
* window's tapped context [1,d+1,m*H] (present iff tapLayers is set), and
|
|
69
|
-
* drop the rejected tips.
|
|
70
|
-
|
|
85
|
+
* drop the rejected tips. Native GLM MTP receives the target's verified
|
|
86
|
+
* hidden window [1,d+1,H] in `verifiedHidden` and the kAccept accepted token
|
|
87
|
+
* ids in `acceptedTokens`; it uses them to rebuild accepted MTP KV rows
|
|
88
|
+
* from target state rather than recursively drafted hidden state. */
|
|
89
|
+
commit(
|
|
90
|
+
d: number,
|
|
91
|
+
kAccept: number,
|
|
92
|
+
vCtxML?: MlxArray,
|
|
93
|
+
verifiedHidden?: MlxArray,
|
|
94
|
+
acceptedTokens?: readonly number[],
|
|
95
|
+
): void | Promise<void>;
|
|
71
96
|
|
|
72
97
|
/** Resident per-request draft weights, for admission accounting (0 when the
|
|
73
98
|
* provider owns the weights). */
|