mlx-bun 0.0.11 → 0.0.13
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 +63 -27
- package/THIRD_PARTY_LICENSES.md +9 -0
- package/docs/design/colibri-glm52-port.md +1486 -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/planning/release-notes-v0.0.13.md +77 -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 +5 -2
- package/docs/reference/library-api.md +41 -13
- package/docs/reference/models.md +49 -2
- package/docs/reference/server-api.md +113 -20
- 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 +189 -11
- package/src/cli.ts +127 -8
- package/src/config.ts +81 -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 +471 -93
- 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,786 @@
|
|
|
1
|
+
// GLM-5.2 multi-head latent attention (MLA), correctness path.
|
|
2
|
+
//
|
|
3
|
+
// The checkpoint stores a compressed KV representation:
|
|
4
|
+
// latent: [B, T, kv_lora_rank]
|
|
5
|
+
// rope: [B, T, qk_rope_head_dim]
|
|
6
|
+
//
|
|
7
|
+
// Prefill reconstructs per-head no-PE keys and values through kv_b_proj.
|
|
8
|
+
// Single-token decode absorbs the key/value projections around attention so
|
|
9
|
+
// the full per-head K/V history is never materialized.
|
|
10
|
+
|
|
11
|
+
import { MlxArray } from "../mlx/array";
|
|
12
|
+
import { Dtype } from "../mlx/ffi";
|
|
13
|
+
import * as ops from "../mlx/ops";
|
|
14
|
+
import type { Glm52Config } from "./glm52-config";
|
|
15
|
+
import type { MLACompressedState, MLACache } from "./glm52-cache";
|
|
16
|
+
import type { ColibriGlm52Weights } from "./glm52-weights";
|
|
17
|
+
|
|
18
|
+
/** Structural seam used by tiny fixtures as well as ColibriGlm52Weights. */
|
|
19
|
+
export interface Glm52MlaWeightSource {
|
|
20
|
+
tensor(name: string): MlxArray;
|
|
21
|
+
dequantized(name: string, outputRows: number, inputColumns: number): MlxArray;
|
|
22
|
+
linear(
|
|
23
|
+
x: MlxArray,
|
|
24
|
+
name: string,
|
|
25
|
+
outputRows: number,
|
|
26
|
+
inputColumns: number,
|
|
27
|
+
): MlxArray;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Glm52MlaProjection {
|
|
31
|
+
/** [B,T,H,qk_nope_head_dim]. */
|
|
32
|
+
qNope: MlxArray;
|
|
33
|
+
/** [B,T,H,qk_rope_head_dim], already rotated. */
|
|
34
|
+
qRope: MlxArray;
|
|
35
|
+
/** [B,T,kv_lora_rank], normalized compressed content state. */
|
|
36
|
+
latent: MlxArray;
|
|
37
|
+
/** [B,T,qk_rope_head_dim], already rotated. */
|
|
38
|
+
rope: MlxArray;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type Glm52MlaPath = "auto" | "reconstructed" | "absorbed";
|
|
42
|
+
export interface Glm52MlaBatchedSelection {
|
|
43
|
+
readonly rows: readonly (MlxArray | null)[];
|
|
44
|
+
}
|
|
45
|
+
export type Glm52MlaSelectedPositions =
|
|
46
|
+
| readonly number[]
|
|
47
|
+
| MlxArray
|
|
48
|
+
| Glm52MlaBatchedSelection
|
|
49
|
+
| null;
|
|
50
|
+
|
|
51
|
+
function isBatchedSelection(
|
|
52
|
+
value: Glm52MlaSelectedPositions,
|
|
53
|
+
): value is Glm52MlaBatchedSelection {
|
|
54
|
+
return value !== null &&
|
|
55
|
+
!(value instanceof MlxArray) &&
|
|
56
|
+
!Array.isArray(value) &&
|
|
57
|
+
"rows" in value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function positiveInteger(name: string, value: number): void {
|
|
61
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
62
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sameShape(actual: readonly number[], expected: readonly number[]): boolean {
|
|
66
|
+
return actual.length === expected.length &&
|
|
67
|
+
actual.every((dimension, index) => dimension === expected[index]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateShape(name: string, array: MlxArray, expected: readonly number[]): void {
|
|
71
|
+
if (!sameShape(array.shape, expected))
|
|
72
|
+
throw new Error(
|
|
73
|
+
`${name} shape ${JSON.stringify(array.shape)} != ${JSON.stringify(expected)}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizedAxis(axis: number, rank: number): number {
|
|
78
|
+
const result = axis < 0 ? rank + axis : axis;
|
|
79
|
+
if (!Number.isSafeInteger(result) || result < 0 || result >= rank)
|
|
80
|
+
throw new Error(`axis ${axis} is invalid for rank ${rank}`);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Exact GLM partial RoPE layout.
|
|
86
|
+
*
|
|
87
|
+
* The rotary prefix enters as pair-interleaved `[a0,b0,a1,b1,...]` and leaves
|
|
88
|
+
* as split real/imaginary halves `[r0,r1,...,i0,i1,...]`. A non-rotary tail is
|
|
89
|
+
* preserved. Positions advance along `tokenAxis`.
|
|
90
|
+
*/
|
|
91
|
+
export function partialInterleavedRopeMlx(
|
|
92
|
+
input: MlxArray,
|
|
93
|
+
positionOffset: number | readonly number[],
|
|
94
|
+
rotaryDimensions: number,
|
|
95
|
+
theta: number,
|
|
96
|
+
tokenAxis = 1,
|
|
97
|
+
): MlxArray {
|
|
98
|
+
const shape = input.shape;
|
|
99
|
+
if (shape.length < 2)
|
|
100
|
+
throw new Error("partial interleaved RoPE requires rank >= 2");
|
|
101
|
+
const axis = normalizedAxis(tokenAxis, shape.length);
|
|
102
|
+
if (axis === shape.length - 1)
|
|
103
|
+
throw new Error("RoPE token axis cannot be the feature axis");
|
|
104
|
+
const positionOffsets = typeof positionOffset === "number"
|
|
105
|
+
? null
|
|
106
|
+
: [...positionOffset];
|
|
107
|
+
if (positionOffsets) {
|
|
108
|
+
if (axis === 0)
|
|
109
|
+
throw new Error("per-row RoPE offsets require a separate token axis");
|
|
110
|
+
if (positionOffsets.length !== shape[0])
|
|
111
|
+
throw new Error(
|
|
112
|
+
`RoPE row offsets ${positionOffsets.length} != batch ${shape[0]}`,
|
|
113
|
+
);
|
|
114
|
+
for (const offset of positionOffsets) {
|
|
115
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
116
|
+
throw new Error("RoPE position offset must be a non-negative integer");
|
|
117
|
+
}
|
|
118
|
+
} else if (
|
|
119
|
+
!Number.isSafeInteger(positionOffset) || (positionOffset as number) < 0
|
|
120
|
+
) {
|
|
121
|
+
throw new Error("RoPE position offset must be a non-negative integer");
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
!Number.isSafeInteger(rotaryDimensions) ||
|
|
125
|
+
rotaryDimensions <= 0 ||
|
|
126
|
+
(rotaryDimensions & 1) !== 0 ||
|
|
127
|
+
rotaryDimensions > shape.at(-1)!
|
|
128
|
+
) {
|
|
129
|
+
throw new Error("RoPE dimensions must be positive, even, and fit the feature axis");
|
|
130
|
+
}
|
|
131
|
+
if (!Number.isFinite(theta) || theta <= 0)
|
|
132
|
+
throw new Error("RoPE theta must be positive");
|
|
133
|
+
|
|
134
|
+
const tokens = shape[axis]!;
|
|
135
|
+
const batches = positionOffsets ? shape[0]! : 1;
|
|
136
|
+
const half = rotaryDimensions / 2;
|
|
137
|
+
const angles = new Float32Array(batches * tokens * half);
|
|
138
|
+
for (let batch = 0; batch < batches; batch++) {
|
|
139
|
+
const offset = positionOffsets?.[batch] ?? positionOffset as number;
|
|
140
|
+
for (let token = 0; token < tokens; token++) {
|
|
141
|
+
const position = offset + token;
|
|
142
|
+
for (let pair = 0; pair < half; pair++) {
|
|
143
|
+
const exponent = Math.fround(Math.fround(-2 * pair) / rotaryDimensions);
|
|
144
|
+
const inverseFrequency = Math.fround(
|
|
145
|
+
Math.pow(Math.fround(theta), exponent),
|
|
146
|
+
);
|
|
147
|
+
angles[(batch * tokens + token) * half + pair] = Math.fround(
|
|
148
|
+
position * inverseFrequency,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const frequencyShape = new Array(shape.length).fill(1);
|
|
155
|
+
if (positionOffsets) frequencyShape[0] = batches;
|
|
156
|
+
frequencyShape[axis] = tokens;
|
|
157
|
+
frequencyShape[shape.length - 1] = half;
|
|
158
|
+
const angleArray = MlxArray.fromFloat32(angles, frequencyShape);
|
|
159
|
+
const cosine = ops.cos(angleArray);
|
|
160
|
+
const sine = ops.sin(angleArray);
|
|
161
|
+
angleArray.dispose();
|
|
162
|
+
|
|
163
|
+
const prefixStart = new Array(shape.length).fill(0);
|
|
164
|
+
const prefixStop = [...shape];
|
|
165
|
+
prefixStop[shape.length - 1] = rotaryDimensions;
|
|
166
|
+
const prefix = input.slice(prefixStart, prefixStop);
|
|
167
|
+
const pairShape = [...shape.slice(0, -1), half, 2];
|
|
168
|
+
const pairs = ops.reshape(prefix, pairShape);
|
|
169
|
+
prefix.dispose();
|
|
170
|
+
|
|
171
|
+
const evenStart = new Array(pairShape.length).fill(0);
|
|
172
|
+
const evenStop = [...pairShape];
|
|
173
|
+
evenStop[pairShape.length - 1] = 1;
|
|
174
|
+
const oddStart = [...evenStart];
|
|
175
|
+
const oddStop = [...pairShape];
|
|
176
|
+
oddStart[pairShape.length - 1] = 1;
|
|
177
|
+
oddStop[pairShape.length - 1] = 2;
|
|
178
|
+
const evenColumn = pairs.slice(evenStart, evenStop);
|
|
179
|
+
const oddColumn = pairs.slice(oddStart, oddStop);
|
|
180
|
+
pairs.dispose();
|
|
181
|
+
const halfShape = [...shape.slice(0, -1), half];
|
|
182
|
+
const even = ops.reshape(evenColumn, halfShape);
|
|
183
|
+
const odd = ops.reshape(oddColumn, halfShape);
|
|
184
|
+
evenColumn.dispose();
|
|
185
|
+
oddColumn.dispose();
|
|
186
|
+
|
|
187
|
+
const evenCos = ops.mul(even, cosine);
|
|
188
|
+
const oddSin = ops.mul(odd, sine);
|
|
189
|
+
const real = ops.sub(evenCos, oddSin);
|
|
190
|
+
const oddCos = ops.mul(odd, cosine);
|
|
191
|
+
const evenSin = ops.mul(even, sine);
|
|
192
|
+
const imaginary = ops.add(oddCos, evenSin);
|
|
193
|
+
const rotated = ops.concatAxis([real, imaginary], shape.length - 1);
|
|
194
|
+
|
|
195
|
+
for (const temporary of [
|
|
196
|
+
cosine,
|
|
197
|
+
sine,
|
|
198
|
+
even,
|
|
199
|
+
odd,
|
|
200
|
+
evenCos,
|
|
201
|
+
oddSin,
|
|
202
|
+
real,
|
|
203
|
+
oddCos,
|
|
204
|
+
evenSin,
|
|
205
|
+
imaginary,
|
|
206
|
+
]) {
|
|
207
|
+
temporary.dispose();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (rotaryDimensions === shape.at(-1)) return rotated;
|
|
211
|
+
const tailStart = new Array(shape.length).fill(0);
|
|
212
|
+
tailStart[shape.length - 1] = rotaryDimensions;
|
|
213
|
+
const tail = input.slice(tailStart, shape);
|
|
214
|
+
const output = ops.concatAxis([rotated, tail], shape.length - 1);
|
|
215
|
+
rotated.dispose();
|
|
216
|
+
tail.dispose();
|
|
217
|
+
return output;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function rmsNormF32Mlx(
|
|
221
|
+
input: MlxArray,
|
|
222
|
+
weight: MlxArray,
|
|
223
|
+
epsilon: number,
|
|
224
|
+
): MlxArray {
|
|
225
|
+
const width = input.shape.at(-1)!;
|
|
226
|
+
validateShape("RMSNorm weight", weight, [width]);
|
|
227
|
+
if (!Number.isFinite(epsilon) || epsilon < 0)
|
|
228
|
+
throw new Error("RMSNorm epsilon must be non-negative");
|
|
229
|
+
|
|
230
|
+
const inputF32 = input.dtype === Dtype.float32
|
|
231
|
+
? input
|
|
232
|
+
: input.astype(Dtype.float32);
|
|
233
|
+
const weightF32 = weight.dtype === Dtype.float32
|
|
234
|
+
? weight
|
|
235
|
+
: weight.astype(Dtype.float32);
|
|
236
|
+
const square = ops.square(inputF32);
|
|
237
|
+
const mean = ops.meanAxis(square, -1, true);
|
|
238
|
+
const epsilonArray = ops.scalarLike(epsilon, mean);
|
|
239
|
+
const shifted = ops.add(mean, epsilonArray);
|
|
240
|
+
const inverseRoot = ops.rsqrt(shifted);
|
|
241
|
+
const normalized = ops.mul(inputF32, inverseRoot);
|
|
242
|
+
const output = ops.mul(normalized, weightF32);
|
|
243
|
+
for (const temporary of [
|
|
244
|
+
square,
|
|
245
|
+
mean,
|
|
246
|
+
epsilonArray,
|
|
247
|
+
shifted,
|
|
248
|
+
inverseRoot,
|
|
249
|
+
normalized,
|
|
250
|
+
]) {
|
|
251
|
+
temporary.dispose();
|
|
252
|
+
}
|
|
253
|
+
if (inputF32 !== input) inputF32.dispose();
|
|
254
|
+
if (weightF32 !== weight) weightF32.dispose();
|
|
255
|
+
return output;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function disposeProjection(projection: Glm52MlaProjection): void {
|
|
259
|
+
projection.qNope.dispose();
|
|
260
|
+
projection.qRope.dispose();
|
|
261
|
+
projection.latent.dispose();
|
|
262
|
+
projection.rope.dispose();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function disposeState(state: MLACompressedState): void {
|
|
266
|
+
state.latent.dispose();
|
|
267
|
+
state.rope.dispose();
|
|
268
|
+
state.dsa?.dispose();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* MLX correctness implementation of GLM-5.2 MLA.
|
|
273
|
+
*
|
|
274
|
+
* This is deliberately independent from the future fused MLA kernel. It loads
|
|
275
|
+
* the existing Colibri artifact directly through ColibriGlm52Weights and pins
|
|
276
|
+
* the mathematical contract used by that optimized path.
|
|
277
|
+
*/
|
|
278
|
+
export class Glm52Mla {
|
|
279
|
+
readonly config: Glm52Config;
|
|
280
|
+
readonly weights: Glm52MlaWeightSource;
|
|
281
|
+
readonly prefix: string;
|
|
282
|
+
|
|
283
|
+
constructor(
|
|
284
|
+
config: Glm52Config,
|
|
285
|
+
weights: ColibriGlm52Weights | Glm52MlaWeightSource,
|
|
286
|
+
layerIndex: number,
|
|
287
|
+
) {
|
|
288
|
+
if (!Number.isSafeInteger(layerIndex) || layerIndex < 0 ||
|
|
289
|
+
layerIndex > config.numHiddenLayers) {
|
|
290
|
+
throw new Error(`invalid GLM-5.2 layer index ${layerIndex}`);
|
|
291
|
+
}
|
|
292
|
+
if (!config.ropeInterleave)
|
|
293
|
+
throw new Error("GLM-5.2 MLA requires pair-interleaved RoPE");
|
|
294
|
+
positiveInteger("attention heads", config.numAttentionHeads);
|
|
295
|
+
positiveInteger("q LoRA rank", config.qLoraRank);
|
|
296
|
+
positiveInteger("kv LoRA rank", config.kvLoraRank);
|
|
297
|
+
positiveInteger("no-PE head dimension", config.qkNopeHeadDim);
|
|
298
|
+
positiveInteger("RoPE head dimension", config.qkRopeHeadDim);
|
|
299
|
+
positiveInteger("value head dimension", config.vHeadDim);
|
|
300
|
+
this.config = config;
|
|
301
|
+
this.weights = weights;
|
|
302
|
+
this.prefix = `model.layers.${layerIndex}.self_attn`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Project a token block into query state and compressed cache state.
|
|
307
|
+
* Returned arrays are caller-owned.
|
|
308
|
+
*/
|
|
309
|
+
project(
|
|
310
|
+
input: MlxArray,
|
|
311
|
+
positionOffset: number | readonly number[],
|
|
312
|
+
): Glm52MlaProjection {
|
|
313
|
+
const shape = input.shape;
|
|
314
|
+
if (shape.length !== 3)
|
|
315
|
+
throw new Error(`GLM-5.2 MLA input must have rank 3 (got ${shape.length})`);
|
|
316
|
+
const [batch, tokens, hidden] = shape as [number, number, number];
|
|
317
|
+
positiveInteger("MLA batch", batch);
|
|
318
|
+
positiveInteger("MLA tokens", tokens);
|
|
319
|
+
if (hidden !== this.config.hiddenSize)
|
|
320
|
+
throw new Error(`MLA hidden width ${hidden} != ${this.config.hiddenSize}`);
|
|
321
|
+
|
|
322
|
+
const heads = this.config.numAttentionHeads;
|
|
323
|
+
const qA = this.weights.linear(
|
|
324
|
+
input,
|
|
325
|
+
`${this.prefix}.q_a_proj.weight`,
|
|
326
|
+
this.config.qLoraRank,
|
|
327
|
+
this.config.hiddenSize,
|
|
328
|
+
);
|
|
329
|
+
const qNorm = rmsNormF32Mlx(
|
|
330
|
+
qA,
|
|
331
|
+
this.weights.tensor(`${this.prefix}.q_a_layernorm.weight`),
|
|
332
|
+
this.config.rmsNormEps,
|
|
333
|
+
);
|
|
334
|
+
qA.dispose();
|
|
335
|
+
const qFlat = this.weights.linear(
|
|
336
|
+
qNorm,
|
|
337
|
+
`${this.prefix}.q_b_proj.weight`,
|
|
338
|
+
heads * this.config.qkHeadDim,
|
|
339
|
+
this.config.qLoraRank,
|
|
340
|
+
);
|
|
341
|
+
qNorm.dispose();
|
|
342
|
+
const q = ops.reshape(
|
|
343
|
+
qFlat,
|
|
344
|
+
[batch, tokens, heads, this.config.qkHeadDim],
|
|
345
|
+
);
|
|
346
|
+
qFlat.dispose();
|
|
347
|
+
const qNope = q.slice(
|
|
348
|
+
[0, 0, 0, 0],
|
|
349
|
+
[batch, tokens, heads, this.config.qkNopeHeadDim],
|
|
350
|
+
);
|
|
351
|
+
const qRopeRaw = q.slice(
|
|
352
|
+
[0, 0, 0, this.config.qkNopeHeadDim],
|
|
353
|
+
[batch, tokens, heads, this.config.qkHeadDim],
|
|
354
|
+
);
|
|
355
|
+
q.dispose();
|
|
356
|
+
const qRope = partialInterleavedRopeMlx(
|
|
357
|
+
qRopeRaw,
|
|
358
|
+
positionOffset,
|
|
359
|
+
this.config.qkRopeHeadDim,
|
|
360
|
+
this.config.ropeTheta,
|
|
361
|
+
1,
|
|
362
|
+
);
|
|
363
|
+
qRopeRaw.dispose();
|
|
364
|
+
|
|
365
|
+
const compressed = this.weights.linear(
|
|
366
|
+
input,
|
|
367
|
+
`${this.prefix}.kv_a_proj_with_mqa.weight`,
|
|
368
|
+
this.config.kvLoraRank + this.config.qkRopeHeadDim,
|
|
369
|
+
this.config.hiddenSize,
|
|
370
|
+
);
|
|
371
|
+
const latentRaw = compressed.slice(
|
|
372
|
+
[0, 0, 0],
|
|
373
|
+
[batch, tokens, this.config.kvLoraRank],
|
|
374
|
+
);
|
|
375
|
+
const ropeRaw = compressed.slice(
|
|
376
|
+
[0, 0, this.config.kvLoraRank],
|
|
377
|
+
[batch, tokens, this.config.kvLoraRank + this.config.qkRopeHeadDim],
|
|
378
|
+
);
|
|
379
|
+
compressed.dispose();
|
|
380
|
+
const latent = rmsNormF32Mlx(
|
|
381
|
+
latentRaw,
|
|
382
|
+
this.weights.tensor(`${this.prefix}.kv_a_layernorm.weight`),
|
|
383
|
+
this.config.rmsNormEps,
|
|
384
|
+
);
|
|
385
|
+
latentRaw.dispose();
|
|
386
|
+
const rope = partialInterleavedRopeMlx(
|
|
387
|
+
ropeRaw,
|
|
388
|
+
positionOffset,
|
|
389
|
+
this.config.qkRopeHeadDim,
|
|
390
|
+
this.config.ropeTheta,
|
|
391
|
+
1,
|
|
392
|
+
);
|
|
393
|
+
ropeRaw.dispose();
|
|
394
|
+
return { qNope, qRope, latent, rope };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Append the compressed state and execute attention.
|
|
399
|
+
*
|
|
400
|
+
* `auto` reconstructs K/V for a multi-token block and selects absorbed MLA
|
|
401
|
+
* for single-token decode. DSA state, when present, is cached atomically with
|
|
402
|
+
* the MLA state but DSA selection itself is owned by the indexer primitive.
|
|
403
|
+
*/
|
|
404
|
+
forward(
|
|
405
|
+
input: MlxArray,
|
|
406
|
+
cache: MLACache,
|
|
407
|
+
dsa: MlxArray | null = null,
|
|
408
|
+
path: Glm52MlaPath = "auto",
|
|
409
|
+
selectedPositions: Glm52MlaSelectedPositions = null,
|
|
410
|
+
): MlxArray {
|
|
411
|
+
if (cache.kvLoraRank !== this.config.kvLoraRank ||
|
|
412
|
+
cache.ropeHeadDim !== this.config.qkRopeHeadDim) {
|
|
413
|
+
throw new Error("MLA cache geometry does not match the model");
|
|
414
|
+
}
|
|
415
|
+
const tokens = input.shape[1]!;
|
|
416
|
+
if (path === "absorbed" && tokens !== 1)
|
|
417
|
+
throw new Error("absorbed MLA is valid only for single-token decode");
|
|
418
|
+
const batch = input.shape[0]!;
|
|
419
|
+
const positionOffset = batch > 1
|
|
420
|
+
? (cache.rowOffsets.length
|
|
421
|
+
? cache.rowOffsets
|
|
422
|
+
: new Array(batch).fill(0) as number[])
|
|
423
|
+
: cache.offset;
|
|
424
|
+
const projection = this.project(input, positionOffset);
|
|
425
|
+
const state = cache.appendCompressed(
|
|
426
|
+
projection.latent,
|
|
427
|
+
projection.rope,
|
|
428
|
+
dsa,
|
|
429
|
+
);
|
|
430
|
+
const selectedPath = path === "auto"
|
|
431
|
+
? (tokens === 1 ? "absorbed" : "reconstructed")
|
|
432
|
+
: path;
|
|
433
|
+
if (selectedPositions !== null && selectedPath !== "absorbed")
|
|
434
|
+
throw new Error("DSA-selected MLA requires single-token absorbed decode");
|
|
435
|
+
try {
|
|
436
|
+
if (selectedPath === "absorbed") {
|
|
437
|
+
if (batch > 1) {
|
|
438
|
+
const selections = isBatchedSelection(selectedPositions)
|
|
439
|
+
? selectedPositions.rows
|
|
440
|
+
: null;
|
|
441
|
+
if (selectedPositions !== null && selections === null)
|
|
442
|
+
throw new Error("batched MLA requires per-row DSA selections");
|
|
443
|
+
if (selections && selections.length !== batch)
|
|
444
|
+
throw new Error(
|
|
445
|
+
`batched MLA selections ${selections.length} != batch ${batch}`,
|
|
446
|
+
);
|
|
447
|
+
const rows: MlxArray[] = [];
|
|
448
|
+
try {
|
|
449
|
+
for (let row = 0; row < batch; row++) {
|
|
450
|
+
const start = cache.leftPad[row]!;
|
|
451
|
+
const qNope = projection.qNope.slice(
|
|
452
|
+
[row, 0, 0, 0],
|
|
453
|
+
[row + 1, 1, projection.qNope.shape[2]!, projection.qNope.shape[3]!],
|
|
454
|
+
);
|
|
455
|
+
const qRope = projection.qRope.slice(
|
|
456
|
+
[row, 0, 0, 0],
|
|
457
|
+
[row + 1, 1, projection.qRope.shape[2]!, projection.qRope.shape[3]!],
|
|
458
|
+
);
|
|
459
|
+
const latent = state.latent.slice(
|
|
460
|
+
[row, start, 0],
|
|
461
|
+
[row + 1, state.latent.shape[1]!, state.latent.shape[2]!],
|
|
462
|
+
);
|
|
463
|
+
const ropeState = state.rope.slice(
|
|
464
|
+
[row, start, 0],
|
|
465
|
+
[row + 1, state.rope.shape[1]!, state.rope.shape[2]!],
|
|
466
|
+
);
|
|
467
|
+
try {
|
|
468
|
+
rows.push(this.attendAbsorbed(
|
|
469
|
+
{ qNope, qRope },
|
|
470
|
+
{ latent, rope: ropeState },
|
|
471
|
+
selections?.[row] ?? null,
|
|
472
|
+
));
|
|
473
|
+
} finally {
|
|
474
|
+
qNope.dispose();
|
|
475
|
+
qRope.dispose();
|
|
476
|
+
latent.dispose();
|
|
477
|
+
ropeState.dispose();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return ops.concatAxis(rows, 0);
|
|
481
|
+
} finally {
|
|
482
|
+
for (const row of rows) row.dispose();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return this.attendAbsorbed(projection, state, selectedPositions);
|
|
486
|
+
}
|
|
487
|
+
return this.attendReconstructed(projection, state);
|
|
488
|
+
} finally {
|
|
489
|
+
disposeState(state);
|
|
490
|
+
disposeProjection(projection);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Reconstruct no-PE keys and values for the complete cached prefix. */
|
|
495
|
+
attendReconstructed(
|
|
496
|
+
projection: Pick<Glm52MlaProjection, "qNope" | "qRope">,
|
|
497
|
+
state: Pick<MLACompressedState, "latent" | "rope">,
|
|
498
|
+
): MlxArray {
|
|
499
|
+
const [batch, tokens] = projection.qNope.shape;
|
|
500
|
+
const keys = state.latent.shape[1]!;
|
|
501
|
+
const heads = this.config.numAttentionHeads;
|
|
502
|
+
validateShape("MLA q(no-PE)", projection.qNope, [
|
|
503
|
+
batch!,
|
|
504
|
+
tokens!,
|
|
505
|
+
heads,
|
|
506
|
+
this.config.qkNopeHeadDim,
|
|
507
|
+
]);
|
|
508
|
+
validateShape("MLA q(RoPE)", projection.qRope, [
|
|
509
|
+
batch!,
|
|
510
|
+
tokens!,
|
|
511
|
+
heads,
|
|
512
|
+
this.config.qkRopeHeadDim,
|
|
513
|
+
]);
|
|
514
|
+
validateShape("MLA latent state", state.latent, [
|
|
515
|
+
batch!,
|
|
516
|
+
keys,
|
|
517
|
+
this.config.kvLoraRank,
|
|
518
|
+
]);
|
|
519
|
+
validateShape("MLA RoPE state", state.rope, [
|
|
520
|
+
batch!,
|
|
521
|
+
keys,
|
|
522
|
+
this.config.qkRopeHeadDim,
|
|
523
|
+
]);
|
|
524
|
+
if (keys < tokens!)
|
|
525
|
+
throw new Error("MLA state cannot be shorter than the query block");
|
|
526
|
+
|
|
527
|
+
const kvWidth = this.config.qkNopeHeadDim + this.config.vHeadDim;
|
|
528
|
+
const reconstructedFlat = this.weights.linear(
|
|
529
|
+
state.latent,
|
|
530
|
+
`${this.prefix}.kv_b_proj.weight`,
|
|
531
|
+
heads * kvWidth,
|
|
532
|
+
this.config.kvLoraRank,
|
|
533
|
+
);
|
|
534
|
+
const reconstructed = ops.reshape(
|
|
535
|
+
reconstructedFlat,
|
|
536
|
+
[batch!, keys, heads, kvWidth],
|
|
537
|
+
);
|
|
538
|
+
reconstructedFlat.dispose();
|
|
539
|
+
const keyNope = reconstructed.slice(
|
|
540
|
+
[0, 0, 0, 0],
|
|
541
|
+
[batch!, keys, heads, this.config.qkNopeHeadDim],
|
|
542
|
+
);
|
|
543
|
+
const values = reconstructed.slice(
|
|
544
|
+
[0, 0, 0, this.config.qkNopeHeadDim],
|
|
545
|
+
[batch!, keys, heads, kvWidth],
|
|
546
|
+
);
|
|
547
|
+
reconstructed.dispose();
|
|
548
|
+
|
|
549
|
+
const ropeOneHead = ops.expandDims(state.rope, 2);
|
|
550
|
+
const ropeHeads = heads === 1
|
|
551
|
+
? ropeOneHead
|
|
552
|
+
: ops.concatAxis(
|
|
553
|
+
Array.from({ length: heads }, () => ropeOneHead),
|
|
554
|
+
2,
|
|
555
|
+
);
|
|
556
|
+
const query = ops.concatAxis(
|
|
557
|
+
[projection.qNope, projection.qRope],
|
|
558
|
+
3,
|
|
559
|
+
);
|
|
560
|
+
const key = ops.concatAxis([keyNope, ropeHeads], 3);
|
|
561
|
+
const queryHeads = ops.transposeAxes(query, [0, 2, 1, 3]);
|
|
562
|
+
const keyHeads = ops.transposeAxes(key, [0, 2, 3, 1]);
|
|
563
|
+
const valueHeads = ops.transposeAxes(values, [0, 2, 1, 3]);
|
|
564
|
+
const rawScores = ops.matmul(queryHeads, keyHeads);
|
|
565
|
+
const scores = ops.mulScalar(
|
|
566
|
+
rawScores,
|
|
567
|
+
1 / Math.sqrt(this.config.qkHeadDim),
|
|
568
|
+
);
|
|
569
|
+
rawScores.dispose();
|
|
570
|
+
|
|
571
|
+
const prefixTokens = keys - tokens!;
|
|
572
|
+
let maskedScores = scores;
|
|
573
|
+
if (tokens! > 1) {
|
|
574
|
+
const maskValues = new Float32Array(tokens! * keys);
|
|
575
|
+
for (let queryToken = 0; queryToken < tokens!; queryToken++) {
|
|
576
|
+
const lastVisible = prefixTokens + queryToken;
|
|
577
|
+
for (let keyToken = lastVisible + 1; keyToken < keys; keyToken++)
|
|
578
|
+
maskValues[queryToken * keys + keyToken] = -Infinity;
|
|
579
|
+
}
|
|
580
|
+
const mask = MlxArray.fromFloat32(maskValues, [1, 1, tokens!, keys]);
|
|
581
|
+
maskedScores = ops.add(scores, mask);
|
|
582
|
+
mask.dispose();
|
|
583
|
+
scores.dispose();
|
|
584
|
+
}
|
|
585
|
+
const probabilities = ops.softmaxAxis(maskedScores, -1, true);
|
|
586
|
+
const contextHeads = ops.matmul(probabilities, valueHeads);
|
|
587
|
+
const contextTokens = ops.transposeAxes(contextHeads, [0, 2, 1, 3]);
|
|
588
|
+
const context = ops.reshape(
|
|
589
|
+
contextTokens,
|
|
590
|
+
[batch!, tokens!, heads * this.config.vHeadDim],
|
|
591
|
+
);
|
|
592
|
+
const output = this.weights.linear(
|
|
593
|
+
context,
|
|
594
|
+
`${this.prefix}.o_proj.weight`,
|
|
595
|
+
this.config.hiddenSize,
|
|
596
|
+
heads * this.config.vHeadDim,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
for (const temporary of [
|
|
600
|
+
keyNope,
|
|
601
|
+
values,
|
|
602
|
+
ropeOneHead,
|
|
603
|
+
...(ropeHeads === ropeOneHead ? [] : [ropeHeads]),
|
|
604
|
+
query,
|
|
605
|
+
key,
|
|
606
|
+
queryHeads,
|
|
607
|
+
keyHeads,
|
|
608
|
+
valueHeads,
|
|
609
|
+
maskedScores,
|
|
610
|
+
probabilities,
|
|
611
|
+
contextHeads,
|
|
612
|
+
contextTokens,
|
|
613
|
+
context,
|
|
614
|
+
]) {
|
|
615
|
+
temporary.dispose();
|
|
616
|
+
}
|
|
617
|
+
return output;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Weight-absorbed single-token decode.
|
|
622
|
+
*
|
|
623
|
+
* For each head, `q_nope @ Wk` attends directly over cached latent vectors;
|
|
624
|
+
* the weighted latent is then projected through `Wv`. Only the small
|
|
625
|
+
* decoupled RoPE term is scored separately.
|
|
626
|
+
*/
|
|
627
|
+
attendAbsorbed(
|
|
628
|
+
projection: Pick<Glm52MlaProjection, "qNope" | "qRope">,
|
|
629
|
+
state: Pick<MLACompressedState, "latent" | "rope">,
|
|
630
|
+
selectedPositions: Glm52MlaSelectedPositions = null,
|
|
631
|
+
): MlxArray {
|
|
632
|
+
const [batch, tokens] = projection.qNope.shape;
|
|
633
|
+
if (tokens !== 1)
|
|
634
|
+
throw new Error("absorbed MLA is valid only for single-token decode");
|
|
635
|
+
const keys = state.latent.shape[1]!;
|
|
636
|
+
const heads = this.config.numAttentionHeads;
|
|
637
|
+
validateShape("MLA q(no-PE)", projection.qNope, [
|
|
638
|
+
batch!,
|
|
639
|
+
1,
|
|
640
|
+
heads,
|
|
641
|
+
this.config.qkNopeHeadDim,
|
|
642
|
+
]);
|
|
643
|
+
validateShape("MLA q(RoPE)", projection.qRope, [
|
|
644
|
+
batch!,
|
|
645
|
+
1,
|
|
646
|
+
heads,
|
|
647
|
+
this.config.qkRopeHeadDim,
|
|
648
|
+
]);
|
|
649
|
+
validateShape("MLA latent state", state.latent, [
|
|
650
|
+
batch!,
|
|
651
|
+
keys,
|
|
652
|
+
this.config.kvLoraRank,
|
|
653
|
+
]);
|
|
654
|
+
validateShape("MLA RoPE state", state.rope, [
|
|
655
|
+
batch!,
|
|
656
|
+
keys,
|
|
657
|
+
this.config.qkRopeHeadDim,
|
|
658
|
+
]);
|
|
659
|
+
|
|
660
|
+
let activeLatent = state.latent;
|
|
661
|
+
let activeRope = state.rope;
|
|
662
|
+
if (selectedPositions !== null) {
|
|
663
|
+
if (isBatchedSelection(selectedPositions)) {
|
|
664
|
+
throw new Error("per-row DSA selection requires batched MLA");
|
|
665
|
+
}
|
|
666
|
+
let indices: MlxArray;
|
|
667
|
+
let ownsIndices = false;
|
|
668
|
+
if (selectedPositions instanceof MlxArray) {
|
|
669
|
+
if (selectedPositions.ndim !== 1 || selectedPositions.size === 0)
|
|
670
|
+
throw new Error("DSA device selection must be a non-empty vector");
|
|
671
|
+
if (selectedPositions.dtype !== Dtype.uint32 &&
|
|
672
|
+
selectedPositions.dtype !== Dtype.int32) {
|
|
673
|
+
throw new Error("DSA device selection must use uint32 or int32 indices");
|
|
674
|
+
}
|
|
675
|
+
indices = selectedPositions;
|
|
676
|
+
} else {
|
|
677
|
+
if (selectedPositions.length === 0)
|
|
678
|
+
throw new Error("DSA selection cannot be empty");
|
|
679
|
+
for (const position of selectedPositions) {
|
|
680
|
+
if (!Number.isSafeInteger(position) || position < 0 || position >= keys)
|
|
681
|
+
throw new Error(`DSA position ${position} is outside cached prefix ${keys}`);
|
|
682
|
+
}
|
|
683
|
+
indices = ops.fromInt32(
|
|
684
|
+
[...selectedPositions],
|
|
685
|
+
[selectedPositions.length],
|
|
686
|
+
);
|
|
687
|
+
ownsIndices = true;
|
|
688
|
+
}
|
|
689
|
+
activeLatent = ops.takeAxis(state.latent, indices, 1);
|
|
690
|
+
activeRope = ops.takeAxis(state.rope, indices, 1);
|
|
691
|
+
if (ownsIndices) indices.dispose();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const kvWidth = this.config.qkNopeHeadDim + this.config.vHeadDim;
|
|
695
|
+
const kvWeights = this.weights.dequantized(
|
|
696
|
+
`${this.prefix}.kv_b_proj.weight`,
|
|
697
|
+
heads * kvWidth,
|
|
698
|
+
this.config.kvLoraRank,
|
|
699
|
+
);
|
|
700
|
+
const latentTranspose = ops.transposeAxes(activeLatent, [0, 2, 1]);
|
|
701
|
+
const ropeTranspose = ops.transposeAxes(activeRope, [0, 2, 1]);
|
|
702
|
+
const headOutputs: MlxArray[] = [];
|
|
703
|
+
try {
|
|
704
|
+
for (let head = 0; head < heads; head++) {
|
|
705
|
+
const row = head * kvWidth;
|
|
706
|
+
const qNopeHeadView = projection.qNope.slice(
|
|
707
|
+
[0, 0, head, 0],
|
|
708
|
+
[batch!, 1, head + 1, this.config.qkNopeHeadDim],
|
|
709
|
+
);
|
|
710
|
+
const qNopeHead = ops.reshape(
|
|
711
|
+
qNopeHeadView,
|
|
712
|
+
[batch!, 1, this.config.qkNopeHeadDim],
|
|
713
|
+
);
|
|
714
|
+
qNopeHeadView.dispose();
|
|
715
|
+
const qRopeHeadView = projection.qRope.slice(
|
|
716
|
+
[0, 0, head, 0],
|
|
717
|
+
[batch!, 1, head + 1, this.config.qkRopeHeadDim],
|
|
718
|
+
);
|
|
719
|
+
const qRopeHead = ops.reshape(
|
|
720
|
+
qRopeHeadView,
|
|
721
|
+
[batch!, 1, this.config.qkRopeHeadDim],
|
|
722
|
+
);
|
|
723
|
+
qRopeHeadView.dispose();
|
|
724
|
+
|
|
725
|
+
// Wk is [qk_nope, latent], so q_nope @ Wk produces latent-space q.
|
|
726
|
+
const keyWeights = kvWeights.slice(
|
|
727
|
+
[row, 0],
|
|
728
|
+
[row + this.config.qkNopeHeadDim, this.config.kvLoraRank],
|
|
729
|
+
);
|
|
730
|
+
const latentQuery = ops.matmul(qNopeHead, keyWeights);
|
|
731
|
+
const contentScores = ops.matmul(latentQuery, latentTranspose);
|
|
732
|
+
const ropeScores = ops.matmul(qRopeHead, ropeTranspose);
|
|
733
|
+
const combinedScores = ops.add(contentScores, ropeScores);
|
|
734
|
+
const scaledScores = ops.mulScalar(
|
|
735
|
+
combinedScores,
|
|
736
|
+
1 / Math.sqrt(this.config.qkHeadDim),
|
|
737
|
+
);
|
|
738
|
+
const probabilities = ops.softmaxAxis(scaledScores, -1, true);
|
|
739
|
+
const latentContext = ops.matmul(probabilities, activeLatent);
|
|
740
|
+
|
|
741
|
+
// Wv is [value, latent], so the latent context multiplies Wv^T.
|
|
742
|
+
const valueWeights = kvWeights.slice(
|
|
743
|
+
[row + this.config.qkNopeHeadDim, 0],
|
|
744
|
+
[row + kvWidth, this.config.kvLoraRank],
|
|
745
|
+
);
|
|
746
|
+
const valueWeightsTranspose = ops.transposeAxes(valueWeights, [1, 0]);
|
|
747
|
+
const headOutput = ops.matmul(latentContext, valueWeightsTranspose);
|
|
748
|
+
headOutputs.push(headOutput);
|
|
749
|
+
for (const temporary of [
|
|
750
|
+
qNopeHead,
|
|
751
|
+
qRopeHead,
|
|
752
|
+
keyWeights,
|
|
753
|
+
latentQuery,
|
|
754
|
+
contentScores,
|
|
755
|
+
ropeScores,
|
|
756
|
+
combinedScores,
|
|
757
|
+
scaledScores,
|
|
758
|
+
probabilities,
|
|
759
|
+
latentContext,
|
|
760
|
+
valueWeights,
|
|
761
|
+
valueWeightsTranspose,
|
|
762
|
+
]) {
|
|
763
|
+
temporary.dispose();
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const context = headOutputs.length === 1
|
|
767
|
+
? headOutputs[0]!
|
|
768
|
+
: ops.concatAxis(headOutputs, 2);
|
|
769
|
+
const output = this.weights.linear(
|
|
770
|
+
context,
|
|
771
|
+
`${this.prefix}.o_proj.weight`,
|
|
772
|
+
this.config.hiddenSize,
|
|
773
|
+
heads * this.config.vHeadDim,
|
|
774
|
+
);
|
|
775
|
+
if (headOutputs.length > 1) context.dispose();
|
|
776
|
+
return output;
|
|
777
|
+
} finally {
|
|
778
|
+
for (const output of headOutputs) output.dispose();
|
|
779
|
+
kvWeights.dispose();
|
|
780
|
+
latentTranspose.dispose();
|
|
781
|
+
ropeTranspose.dispose();
|
|
782
|
+
if (activeLatent !== state.latent) activeLatent.dispose();
|
|
783
|
+
if (activeRope !== state.rope) activeRope.dispose();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|