mlx-bun 0.0.8 → 0.0.10
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 +112 -20
- package/docs/design/batching-perf-path.md +232 -0
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/omlx-adoption-map.md +90 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/segmented-backward-training.md +15 -0
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/ssd-kv-cold-tier.md +158 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +348 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +43 -9
- package/docs/reference/training.md +31 -12
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1077 -90
- package/src/config.ts +4 -1
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +15 -6
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +149 -18
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/kv-store.ts +240 -98
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/array.ts +21 -0
- package/src/mlx/ffi.ts +3 -0
- package/src/mlx/ops.ts +72 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/factory.ts +29 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +96 -7
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3-delta.ts +48 -1
- package/src/model/qwen3.ts +276 -0
- package/src/model/qwen3_5.ts +11 -2
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +38 -8
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/prompt-cache.ts +55 -9
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +219 -8
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +307 -83
- package/src/serve/generation-gateway.ts +156 -29
- package/src/server.ts +746 -73
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/ssd-cache.ts +213 -0
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/flash-cce.ts +81 -23
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +341 -40
- package/src/train/prefix-shared.ts +62 -11
- package/src/train/segmented.ts +215 -55
- package/src/train/trainer.ts +112 -38
- package/src/web/skills/memory/SKILL.md +40 -17
package/src/model/qwen3-delta.ts
CHANGED
|
@@ -192,7 +192,10 @@ export class SSMCache implements Cache {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
makeMask(_N: number, _windowSize: number | null): Mask {
|
|
195
|
-
//
|
|
195
|
+
// ssm_mask is None both single-stream and in the batch lane: rows
|
|
196
|
+
// solo-prefill UNPADDED at B=1 (state never sees pad tokens), and
|
|
197
|
+
// batched decode feeds one real token per row — so unlike mlx-lm's
|
|
198
|
+
// left-padded batch-prefill there is never a pad position to mask.
|
|
196
199
|
return { mode: "", arr: null };
|
|
197
200
|
}
|
|
198
201
|
|
|
@@ -221,4 +224,48 @@ export class SSMCache implements Cache {
|
|
|
221
224
|
this.conv = null;
|
|
222
225
|
this.recurrent = null;
|
|
223
226
|
}
|
|
227
|
+
|
|
228
|
+
// --- batch-lane dynamic-B ops (BatchScheduler only) ----------------------
|
|
229
|
+
// Both state slots are plain [B, ...] tensors with no temporal axis and no
|
|
230
|
+
// left-padding, so the batched twins of mergeKVRows/filterKVRows collapse
|
|
231
|
+
// to B-axis concat/take. `offset` is bookkeeping only for SSM layers (RoPE
|
|
232
|
+
// reads the FULL-attention caches; the linear-attn forward never reads it),
|
|
233
|
+
// so merged rows carrying different token counts share the max.
|
|
234
|
+
|
|
235
|
+
/** Merge a fully-prefilled solo row into a running batched cache (`prev`
|
|
236
|
+
* null ⇒ the solo row becomes the batch). The returned cache owns fresh
|
|
237
|
+
* arrays — or, when there is nothing to concat, arrays STOLEN from `solo`
|
|
238
|
+
* (nulled there so the scheduler's unconditional dispose of the solo
|
|
239
|
+
* caches stays safe). `prev`/`solo` disposal remains the caller's job. */
|
|
240
|
+
static mergeRows(prev: SSMCache | null, solo: SSMCache): SSMCache {
|
|
241
|
+
if (!solo.conv || !solo.recurrent)
|
|
242
|
+
throw new Error("SSMCache.mergeRows: solo row has no state (prefill first)");
|
|
243
|
+
const out = new SSMCache();
|
|
244
|
+
if (prev) {
|
|
245
|
+
if (!prev.conv || !prev.recurrent)
|
|
246
|
+
throw new Error("SSMCache.mergeRows: batched cache has no state");
|
|
247
|
+
out.conv = ops.concatAxis([prev.conv, solo.conv], 0);
|
|
248
|
+
out.recurrent = ops.concatAxis([prev.recurrent, solo.recurrent], 0);
|
|
249
|
+
} else {
|
|
250
|
+
out.conv = solo.conv;
|
|
251
|
+
out.recurrent = solo.recurrent;
|
|
252
|
+
solo.conv = null;
|
|
253
|
+
solo.recurrent = null;
|
|
254
|
+
}
|
|
255
|
+
out.offset = Math.max(prev?.offset ?? 0, solo.offset);
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Evict rows not in `keep` (ascending row indices), in place. */
|
|
260
|
+
filter(keep: number[]): void {
|
|
261
|
+
if (!this.conv || !this.recurrent) return;
|
|
262
|
+
const idx = ops.fromInt32(keep, [keep.length]);
|
|
263
|
+
const conv = ops.takeAxis(this.conv, idx, 0);
|
|
264
|
+
const recurrent = ops.takeAxis(this.recurrent, idx, 0);
|
|
265
|
+
idx.dispose();
|
|
266
|
+
this.conv.dispose();
|
|
267
|
+
this.recurrent.dispose();
|
|
268
|
+
this.conv = conv;
|
|
269
|
+
this.recurrent = recurrent;
|
|
270
|
+
}
|
|
224
271
|
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Concrete model graph for plain Qwen3 (Qwen3ForCausalLM).
|
|
2
|
+
// Port target: mlx_lm.models.qwen3. A standard dense decoder — GQA softmax
|
|
3
|
+
// attention with per-head q/k RMSNorm (applied over head_dim BEFORE RoPE),
|
|
4
|
+
// full-head RoPE (theta 1e6), SwiGLU MLP, RMSNorm, tied embeddings.
|
|
5
|
+
//
|
|
6
|
+
// Primary use here is as a TEXT-EMBEDDING backbone (mlx-community Qwen3-Embedding-*):
|
|
7
|
+
// `embedPooled` returns the last-token hidden, L2-normalized — the pooling
|
|
8
|
+
// Qwen3-Embedding was trained with (an <|endoftext|> token is appended by the
|
|
9
|
+
// caller; its hidden is the sentence vector). The full causal-LM surface
|
|
10
|
+
// (forward/generate via the tied head) is wired too so it conforms to RuntimeModel.
|
|
11
|
+
|
|
12
|
+
import type { ModelConfig } from "../config";
|
|
13
|
+
import type { Weights } from "../weights";
|
|
14
|
+
import { MlxArray } from "../mlx/array";
|
|
15
|
+
import * as ops from "../mlx/ops";
|
|
16
|
+
import {
|
|
17
|
+
argmaxLastPosition,
|
|
18
|
+
disposing,
|
|
19
|
+
KVCache,
|
|
20
|
+
LoraState,
|
|
21
|
+
QuantizedEmbedding,
|
|
22
|
+
QuantizedLinear,
|
|
23
|
+
RMSNorm,
|
|
24
|
+
type Cache,
|
|
25
|
+
type Mask,
|
|
26
|
+
} from "./gemma4-base";
|
|
27
|
+
|
|
28
|
+
class Qwen3Attention {
|
|
29
|
+
readonly qProj: QuantizedLinear;
|
|
30
|
+
readonly kProj: QuantizedLinear;
|
|
31
|
+
readonly vProj: QuantizedLinear;
|
|
32
|
+
readonly oProj: QuantizedLinear;
|
|
33
|
+
readonly qNorm: RMSNorm;
|
|
34
|
+
readonly kNorm: RMSNorm;
|
|
35
|
+
readonly nHeads: number;
|
|
36
|
+
readonly nKvHeads: number;
|
|
37
|
+
readonly headDim: number;
|
|
38
|
+
readonly scale: number;
|
|
39
|
+
readonly ropeBase: number;
|
|
40
|
+
|
|
41
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
42
|
+
const t = config.text;
|
|
43
|
+
this.nHeads = t.numAttentionHeads;
|
|
44
|
+
this.nKvHeads = t.numKeyValueHeads;
|
|
45
|
+
this.headDim = t.headDim;
|
|
46
|
+
this.scale = Math.pow(this.headDim, -0.5);
|
|
47
|
+
this.ropeBase = t.ropeParameters.full_attention?.ropeTheta ?? 1000000;
|
|
48
|
+
this.qProj = QuantizedLinear.load(weights, `${prefix}.q_proj`, config);
|
|
49
|
+
this.kProj = QuantizedLinear.load(weights, `${prefix}.k_proj`, config);
|
|
50
|
+
this.vProj = QuantizedLinear.load(weights, `${prefix}.v_proj`, config);
|
|
51
|
+
this.oProj = QuantizedLinear.load(weights, `${prefix}.o_proj`, config);
|
|
52
|
+
this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps);
|
|
53
|
+
this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
|
|
57
|
+
const [B, L] = x.shape as [number, number, number];
|
|
58
|
+
let q = this.qProj.forward(x);
|
|
59
|
+
let k = this.kProj.forward(x);
|
|
60
|
+
let v = this.vProj.forward(x);
|
|
61
|
+
|
|
62
|
+
// Reshape to heads, then q/k RMSNorm over head_dim BEFORE transpose+RoPE
|
|
63
|
+
// (mlx-lm qwen3 reference order).
|
|
64
|
+
q = disposing(q, ops.reshape(q, [B, L, this.nHeads, this.headDim]));
|
|
65
|
+
k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
|
|
66
|
+
v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
|
|
67
|
+
q = disposing(q, this.qNorm.forward(q));
|
|
68
|
+
k = disposing(k, this.kNorm.forward(k));
|
|
69
|
+
|
|
70
|
+
q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
|
|
71
|
+
k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
|
|
72
|
+
v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
|
|
73
|
+
|
|
74
|
+
q = disposing(q, ops.rope(q, this.headDim, this.ropeBase, cache.offset, null));
|
|
75
|
+
k = disposing(k, ops.rope(k, this.headDim, this.ropeBase, cache.offset, null));
|
|
76
|
+
|
|
77
|
+
const [keys, values] = cache.updateAndFetch(k, v);
|
|
78
|
+
k.dispose();
|
|
79
|
+
v.dispose();
|
|
80
|
+
const attn = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
|
|
81
|
+
keys.dispose();
|
|
82
|
+
values.dispose();
|
|
83
|
+
q.dispose();
|
|
84
|
+
|
|
85
|
+
const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
86
|
+
attn.dispose();
|
|
87
|
+
const merged = ops.reshape(attnT, [B, L, -1]);
|
|
88
|
+
attnT.dispose();
|
|
89
|
+
const out = this.oProj.forward(merged);
|
|
90
|
+
merged.dispose();
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class Qwen3MLP {
|
|
96
|
+
readonly gate: QuantizedLinear;
|
|
97
|
+
readonly up: QuantizedLinear;
|
|
98
|
+
readonly down: QuantizedLinear;
|
|
99
|
+
|
|
100
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
101
|
+
this.gate = QuantizedLinear.load(weights, `${prefix}.gate_proj`, config);
|
|
102
|
+
this.up = QuantizedLinear.load(weights, `${prefix}.up_proj`, config);
|
|
103
|
+
this.down = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
forward(x: MlxArray): MlxArray {
|
|
107
|
+
const gate = this.gate.forward(x);
|
|
108
|
+
const up = this.up.forward(x);
|
|
109
|
+
const sig = ops.sigmoid(gate);
|
|
110
|
+
const silu = ops.mul(gate, sig);
|
|
111
|
+
gate.dispose();
|
|
112
|
+
sig.dispose();
|
|
113
|
+
const hidden = ops.mul(silu, up);
|
|
114
|
+
silu.dispose();
|
|
115
|
+
up.dispose();
|
|
116
|
+
const out = this.down.forward(hidden);
|
|
117
|
+
hidden.dispose();
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class Qwen3Layer {
|
|
123
|
+
readonly attn: Qwen3Attention;
|
|
124
|
+
readonly mlp: Qwen3MLP;
|
|
125
|
+
readonly inputNorm: RMSNorm;
|
|
126
|
+
readonly postAttnNorm: RMSNorm;
|
|
127
|
+
|
|
128
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
129
|
+
this.attn = new Qwen3Attention(weights, config, `${prefix}.self_attn`);
|
|
130
|
+
this.mlp = new Qwen3MLP(weights, config, `${prefix}.mlp`);
|
|
131
|
+
this.inputNorm = new RMSNorm(weights.tensor(`${prefix}.input_layernorm.weight`), config.text.rmsNormEps);
|
|
132
|
+
this.postAttnNorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), config.text.rmsNormEps);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
|
|
136
|
+
const xn = this.inputNorm.forward(x);
|
|
137
|
+
const a = this.attn.forward(xn, mask, cache);
|
|
138
|
+
xn.dispose();
|
|
139
|
+
const h = ops.add(x, a);
|
|
140
|
+
a.dispose();
|
|
141
|
+
const hn = this.postAttnNorm.forward(h);
|
|
142
|
+
const m = this.mlp.forward(hn);
|
|
143
|
+
hn.dispose();
|
|
144
|
+
const out = ops.add(h, m);
|
|
145
|
+
h.dispose();
|
|
146
|
+
m.dispose();
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class Qwen3Model {
|
|
152
|
+
readonly config: ModelConfig;
|
|
153
|
+
readonly weightsBytes: number;
|
|
154
|
+
readonly prefixBase = "model";
|
|
155
|
+
readonly loraState = new LoraState();
|
|
156
|
+
readonly embed: QuantizedEmbedding;
|
|
157
|
+
readonly layers: Qwen3Layer[];
|
|
158
|
+
readonly finalNorm: RMSNorm;
|
|
159
|
+
/** Separate lm_head when NOT tied; otherwise the tied embedding's asLinear. */
|
|
160
|
+
readonly lmHead: QuantizedLinear | null;
|
|
161
|
+
|
|
162
|
+
constructor(weights: Weights, config: ModelConfig) {
|
|
163
|
+
this.config = config;
|
|
164
|
+
this.weightsBytes = [...weights.shards.files.values()]
|
|
165
|
+
.reduce((a, f) => a + f.mmap.size, 0);
|
|
166
|
+
this.embed = QuantizedEmbedding.load(weights, "model.embed_tokens", config);
|
|
167
|
+
this.layers = Array.from(
|
|
168
|
+
{ length: config.text.numHiddenLayers },
|
|
169
|
+
(_, i) => new Qwen3Layer(weights, config, `model.layers.${i}`),
|
|
170
|
+
);
|
|
171
|
+
this.finalNorm = new RMSNorm(weights.tensor("model.norm.weight"), config.text.rmsNormEps);
|
|
172
|
+
this.lmHead = config.text.tieWordEmbeddings
|
|
173
|
+
? null
|
|
174
|
+
: QuantizedLinear.load(weights, "lm_head", config);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
loraTargets(): Map<string, QuantizedLinear> {
|
|
178
|
+
const out = new Map<string, QuantizedLinear>();
|
|
179
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
180
|
+
const l = this.layers[i]!;
|
|
181
|
+
const p = `model.layers.${i}`;
|
|
182
|
+
out.set(`${p}.self_attn.q_proj`, l.attn.qProj);
|
|
183
|
+
out.set(`${p}.self_attn.k_proj`, l.attn.kProj);
|
|
184
|
+
out.set(`${p}.self_attn.v_proj`, l.attn.vProj);
|
|
185
|
+
out.set(`${p}.self_attn.o_proj`, l.attn.oProj);
|
|
186
|
+
out.set(`${p}.mlp.gate_proj`, l.mlp.gate);
|
|
187
|
+
out.set(`${p}.mlp.up_proj`, l.mlp.up);
|
|
188
|
+
out.set(`${p}.mlp.down_proj`, l.mlp.down);
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
makeCache(): Cache[] {
|
|
194
|
+
return this.layers.map(() => new KVCache());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
forwardHidden(ids: MlxArray, cache: Cache[]): MlxArray {
|
|
198
|
+
const h = this.embed.encode(ids);
|
|
199
|
+
return this.forwardLayers(h, cache);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
forwardEmbeddings(_embeds: MlxArray, _cache: Cache[], _bidir: MlxArray | null): MlxArray {
|
|
203
|
+
throw new Error("qwen3 input-embedding path is not supported");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
|
|
207
|
+
const L = h0.shape[1]!;
|
|
208
|
+
const mask = cache[0]!.makeMask(L, null);
|
|
209
|
+
let cur = h0;
|
|
210
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
211
|
+
const next = this.layers[i]!.forward(cur, mask, cache[i]!);
|
|
212
|
+
cur.dispose();
|
|
213
|
+
cur = next;
|
|
214
|
+
}
|
|
215
|
+
mask.arr?.dispose();
|
|
216
|
+
return disposing(cur, this.finalNorm.forward(cur));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Last-token pooled, L2-normalized sentence embedding for one sequence.
|
|
220
|
+
* ids [1, L] → vector [1, hidden]. The caller appends the EOS/eod token
|
|
221
|
+
* (Qwen3-Embedding's pooling token) to `ids`. */
|
|
222
|
+
embedPooled(ids: MlxArray): MlxArray {
|
|
223
|
+
const cache = this.makeCache();
|
|
224
|
+
try {
|
|
225
|
+
const h = this.forwardHidden(ids, cache); // [1, L, hidden]
|
|
226
|
+
const [B, L, H] = h.shape as [number, number, number];
|
|
227
|
+
const last = h.slice([0, L - 1, 0], [B, L, H]); // [1, 1, hidden]
|
|
228
|
+
h.dispose();
|
|
229
|
+
const vec = disposing(last, ops.reshape(last, [B, H])); // [1, hidden]
|
|
230
|
+
// L2 normalize: x / max(||x||_2, eps)
|
|
231
|
+
const sq = ops.square(vec);
|
|
232
|
+
const ss = disposing(sq, ops.sumAxis(sq, -1, true));
|
|
233
|
+
const norm = disposing(ss, ops.sqrt(ss));
|
|
234
|
+
const out = ops.div(vec, norm);
|
|
235
|
+
vec.dispose();
|
|
236
|
+
norm.dispose();
|
|
237
|
+
return out;
|
|
238
|
+
} finally {
|
|
239
|
+
for (const c of cache) c.dispose();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
logitsFromHidden(h: MlxArray): MlxArray {
|
|
244
|
+
return this.lmHead ? this.lmHead.forward(h) : this.embed.asLinear(h);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
forward(tokens: number[] | MlxArray, cache: Cache[]): MlxArray {
|
|
248
|
+
const ids = Array.isArray(tokens)
|
|
249
|
+
? ops.fromInt32(tokens, [1, tokens.length])
|
|
250
|
+
: tokens;
|
|
251
|
+
const h = this.forwardHidden(ids, cache);
|
|
252
|
+
if (Array.isArray(tokens)) ids.dispose();
|
|
253
|
+
const logits = this.logitsFromHidden(h);
|
|
254
|
+
h.dispose();
|
|
255
|
+
return logits;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
generate(promptTokens: number[], maxTokens: number, eosIds: number[] = []): number[] {
|
|
259
|
+
const cache = this.makeCache();
|
|
260
|
+
const out: number[] = [];
|
|
261
|
+
try {
|
|
262
|
+
let tokens = promptTokens;
|
|
263
|
+
for (let step = 0; step < maxTokens; step++) {
|
|
264
|
+
const logits = this.forward(tokens, cache);
|
|
265
|
+
const next = argmaxLastPosition(logits);
|
|
266
|
+
logits.dispose();
|
|
267
|
+
if (eosIds.includes(next)) break;
|
|
268
|
+
out.push(next);
|
|
269
|
+
tokens = [next];
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
for (const c of cache) c.dispose();
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/model/qwen3_5.ts
CHANGED
|
@@ -212,8 +212,17 @@ class Qwen3Attention {
|
|
|
212
212
|
v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
|
|
213
213
|
v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
// Batched decode: the scheduler's mask wrapper exposes each row's REAL
|
|
216
|
+
// position as ropeOffsetArr (rows have different prompt lengths); the
|
|
217
|
+
// dynamic-offset kernel is the same fast::rope, bit-exact vs the static
|
|
218
|
+
// form (tests/compile.test.ts). Serial lane: scalar offset, unchanged.
|
|
219
|
+
const offArr = (cache as { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
|
|
220
|
+
q = disposing(q, offArr
|
|
221
|
+
? ops.ropeDynamic(q, this.ropeDims, this.ropeBase, offArr, null)
|
|
222
|
+
: ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
|
|
223
|
+
k = disposing(k, offArr
|
|
224
|
+
? ops.ropeDynamic(k, this.ropeDims, this.ropeBase, offArr, null)
|
|
225
|
+
: ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
|
|
217
226
|
|
|
218
227
|
let attn: MlxArray;
|
|
219
228
|
if (cache instanceof QuantizedKVCache) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Plain quantized linear y = x @ Wᵀ as OUR dispatch, composed from the steel
|
|
2
|
+
// building blocks already in src/train/steel-qmm-header.ts (BlockMMA + Block/
|
|
3
|
+
// QuantizedBlockLoader — MLX's verbatim GEMM, the same code qmm_t_impl wraps).
|
|
4
|
+
// This is the projection primitive for the controlled forward (q/k/v/o/down):
|
|
5
|
+
// we own the dispatch and the eval boundary instead of emitting an ops.* node
|
|
6
|
+
// into MLX's lazy graph. N (out) and H (in/K) are baked as template literals;
|
|
7
|
+
// M comes from the x_shape helper. Output bit-matches the BlockMMA store epilogue
|
|
8
|
+
// (TransformNone → cast to T), the same store qmm_t_impl uses.
|
|
9
|
+
|
|
10
|
+
import { MlxArray } from "../mlx/array";
|
|
11
|
+
import { MetalKernel } from "../mlx/metal-kernel";
|
|
12
|
+
import { STEEL_QMM_HEADER } from "../train/steel-qmm-header";
|
|
13
|
+
import * as ops from "../mlx/ops";
|
|
14
|
+
import type { QuantSpec } from "../mlx/ops";
|
|
15
|
+
|
|
16
|
+
const BM = 32, BN = 32, BK = 32, WM = 2, WN = 2;
|
|
17
|
+
const TGN = WM * WN * 32;
|
|
18
|
+
|
|
19
|
+
// One BM×BN output tile per threadgroup (grid.y = N-tiles, grid.z = M-tiles), the
|
|
20
|
+
// K=H reduction looped inside — the qmm_t_impl body, dims via templateInts.
|
|
21
|
+
const SOURCE = String.raw`
|
|
22
|
+
const uint M=x_shape[0]; constexpr int N=N_T, H=H_T;
|
|
23
|
+
constexpr int gs=GS_T, bts=BITS_T;
|
|
24
|
+
constexpr int pf=get_pack_factor<bts,8>(), bpp=get_bytes_per_pack<bts>();
|
|
25
|
+
constexpr int BKp=BK_T+16/sizeof(T);
|
|
26
|
+
threadgroup T Xs[BM_T*BKp];
|
|
27
|
+
threadgroup T Ws[BN_T*BKp];
|
|
28
|
+
using mma_t=mlx::steel::BlockMMA<T,T,BM_T,BN_T,BK_T,WM_T,WN_T,false,true,BKp,BKp>;
|
|
29
|
+
using lx_t=mlx::steel::BlockLoader<T,BM_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE>;
|
|
30
|
+
using lw_t=QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gs,bts>;
|
|
31
|
+
const uint sg=simdgroup_index_in_threadgroup, sl=thread_index_in_simdgroup;
|
|
32
|
+
const int Hh=(int)H, K_w=Hh*bpp/pf, K_g=Hh/gs;
|
|
33
|
+
const int m0=(int)threadgroup_position_in_grid.z*BM_T;
|
|
34
|
+
const int n0=(int)threadgroup_position_in_grid.y*BN_T;
|
|
35
|
+
const short num_els=(short)min(BM_T,(int)M-m0);
|
|
36
|
+
const short num_outs=(short)min(BN_T,(int)N-n0);
|
|
37
|
+
|
|
38
|
+
mma_t mma_op(sg,sl);
|
|
39
|
+
lx_t lx(x + (int64_t)m0*Hh, Hh, Xs, sg, sl);
|
|
40
|
+
lw_t lw((const device uint8_t*)w + (int64_t)n0*K_w, scales + (int64_t)n0*K_g, biases + (int64_t)n0*K_g, Hh, Ws, sg, sl);
|
|
41
|
+
|
|
42
|
+
for (int k=0; k<Hh; k+=BK_T){
|
|
43
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
44
|
+
if (num_els<BM_T) lx.load_safe(short2(BK_T,num_els)); else lx.load_unsafe();
|
|
45
|
+
if (num_outs<BN_T) lw.load_safe(short2(BK_T,num_outs)); else lw.load_unsafe();
|
|
46
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
47
|
+
mma_op.mma(Xs,Ws);
|
|
48
|
+
lx.next(); lw.next();
|
|
49
|
+
}
|
|
50
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
51
|
+
// Store straight to device y[m0.., n0..] via the BlockMMA epilogue (float accum
|
|
52
|
+
// → T, the SAME rounding MLX's quantized_matmul applies).
|
|
53
|
+
device T* yp = y + (int64_t)m0*(int64_t)N + n0;
|
|
54
|
+
if (num_els<BM_T || num_outs<BN_T) mma_op.store_result_safe(yp, N, short2(num_outs,num_els));
|
|
55
|
+
else mma_op.store_result(yp, N);
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const _kernels = new Map<string, MetalKernel>();
|
|
59
|
+
function getKernel(gs: number, bits: number): MetalKernel {
|
|
60
|
+
const key = `${gs}:${bits}`;
|
|
61
|
+
let k = _kernels.get(key);
|
|
62
|
+
if (!k) {
|
|
63
|
+
k = new MetalKernel({
|
|
64
|
+
name: "mlx_bun_steel_linear",
|
|
65
|
+
inputNames: ["x", "w", "scales", "biases"],
|
|
66
|
+
outputNames: ["y"],
|
|
67
|
+
source: SOURCE,
|
|
68
|
+
header:
|
|
69
|
+
`${STEEL_QMM_HEADER}\n` +
|
|
70
|
+
`#define GS_T ${gs}\n#define BITS_T ${bits}\n` +
|
|
71
|
+
`#define BM_T ${BM}\n#define BN_T ${BN}\n#define BK_T ${BK}\n` +
|
|
72
|
+
`#define WM_T ${WM}\n#define WN_T ${WN}\n#define TGN_T ${TGN}\n`,
|
|
73
|
+
ensureRowContiguous: true,
|
|
74
|
+
});
|
|
75
|
+
_kernels.set(key, k);
|
|
76
|
+
}
|
|
77
|
+
return k;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** y = x @ Wᵀ, quantized, as our own dispatch. `x` is [M, H] (bf16/f16); `w` is
|
|
81
|
+
* the packed weight [N, H*bits/32] with per-group scales/biases [N, H/groupSize].
|
|
82
|
+
* Returns [M, N] in x.dtype. H % BK == 0 and groupSize % BK == 0 required. */
|
|
83
|
+
export function steelLinear(
|
|
84
|
+
x: MlxArray, w: MlxArray, scales: MlxArray, biases: MlxArray, spec: QuantSpec,
|
|
85
|
+
): MlxArray {
|
|
86
|
+
const M = x.shape[0]!, H = x.shape[1]!, N = w.shape[0]!;
|
|
87
|
+
const [y] = getKernel(spec.groupSize, spec.bits).apply([x, w, scales, biases], {
|
|
88
|
+
outputShapeFn: (ins) => [{ shape: [ins[0]!.shape[0]!, N], dtype: x.dtype }],
|
|
89
|
+
grid: (ins) => [TGN, Math.ceil(N / BN), Math.ceil(ins[0]!.shape[0]! / BM)],
|
|
90
|
+
threadGroup: [TGN, 1, 1],
|
|
91
|
+
templateInts: { N_T: N, H_T: H },
|
|
92
|
+
templateDtypes: { T: x.dtype },
|
|
93
|
+
});
|
|
94
|
+
void M;
|
|
95
|
+
return y!;
|
|
96
|
+
}
|
package/src/model/support.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelConfig } from "../config";
|
|
2
|
+
import { GENERIC_MODEL_TYPES, genericArgsFor, remapModelType } from "./universal/archs";
|
|
2
3
|
|
|
3
4
|
export function isMiniCPM5Config(config: ModelConfig): boolean {
|
|
4
5
|
const t = config.text;
|
|
@@ -22,6 +23,13 @@ export function isQwen35Config(config: ModelConfig): boolean {
|
|
|
22
23
|
config.text.fullAttentionInterval > 0;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** Plain Qwen3 (model_type `qwen3`, Qwen3ForCausalLM) — a standard dense decoder
|
|
27
|
+
* with per-head q/k norm and tied embeddings. Used here as the text-embedding
|
|
28
|
+
* backbone (mlx-community Qwen3-Embedding-*). Distinct from the qwen3_5 hybrid. */
|
|
29
|
+
export function isQwen3Config(config: ModelConfig): boolean {
|
|
30
|
+
return config.modelType === "qwen3";
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
/** Speculative-decoding drafters (e.g. `gemma4_assistant`) are companion
|
|
26
34
|
* artifacts to a target model — Q-only, centroid-head, no standalone LM
|
|
27
35
|
* head. They are never servable/selectable on their own (the spec path
|
|
@@ -38,17 +46,39 @@ export function isDiffusionGemmaConfig(config: ModelConfig): boolean {
|
|
|
38
46
|
return config.modelType === "diffusion_gemma";
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
/** Support tier for a registry record (docs/design/generic-model-support.md):
|
|
50
|
+
* "targeted" = dedicated/generated forward + L2/L3 paths;
|
|
51
|
+
* "generic" = the Tier-0 universal module (L1 monolith only);
|
|
52
|
+
* null = unsupported. Generic never shadows targeted. */
|
|
53
|
+
export function supportTier(modelType: string, repoId = ""): "targeted" | "generic" | null {
|
|
54
|
+
if (isDrafterModelType(modelType)) return null;
|
|
55
|
+
if (modelType.startsWith("gemma4")) return "targeted";
|
|
56
|
+
if (modelType === "diffusion_gemma") return "targeted";
|
|
45
57
|
// qwen3_5 / qwen3_5_text (dense hybrid). The MoE variant is a separate type.
|
|
46
|
-
if (modelType === "qwen3_5" || modelType === "qwen3_5_text") return
|
|
47
|
-
|
|
58
|
+
if (modelType === "qwen3_5" || modelType === "qwen3_5_text") return "targeted";
|
|
59
|
+
if (modelType === "qwen3") return "targeted"; // plain Qwen3 (embedding backbone)
|
|
60
|
+
if (modelType === "llama" && repoId.toLowerCase().includes("minicpm5-1b-optiq-4bit"))
|
|
61
|
+
return "targeted";
|
|
62
|
+
// Tier-0 generic fallback: the universal-dense descriptor table.
|
|
63
|
+
if (GENERIC_MODEL_TYPES.has(remapModelType(modelType))) return "generic";
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isSupportedModelRecord(modelType: string, repoId = ""): boolean {
|
|
68
|
+
return supportTier(modelType, repoId) !== null;
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
export function isSupportedModelConfig(config: ModelConfig): boolean {
|
|
51
72
|
if (isDrafterModelType(config.modelType)) return false;
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
if (
|
|
74
|
+
config.modelType.startsWith("gemma4") || isMiniCPM5Config(config) ||
|
|
75
|
+
isQwen35Config(config) || isQwen3Config(config) || isDiffusionGemmaConfig(config)
|
|
76
|
+
) return true;
|
|
77
|
+
// Generic tier: an arch descriptor exists for this model_type (and its
|
|
78
|
+
// config parses — a malformed config is unsupported, not a crash).
|
|
79
|
+
try {
|
|
80
|
+
return genericArgsFor(config) !== null;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
54
84
|
}
|