mlx-bun 0.0.7 → 0.0.9
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-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/diffusion-gemma-port.md +602 -0
- 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/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/spec-decode-larger-targets.md +229 -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 +262 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +92 -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.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 +40 -9
- package/docs/reference/training.md +25 -11
- 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 +1029 -86
- package/src/config.ts +54 -3
- package/src/diffusion/diffusion-generate.ts +436 -0
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +19 -7
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +196 -19
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- 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/ffi.ts +11 -0
- package/src/mlx/ops.ts +113 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/diffusion-gemma.ts +1026 -0
- package/src/model/factory.ts +35 -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 +62 -6
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3.ts +276 -0
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +45 -7
- 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/quantize/config-writer.ts +1 -1
- package/src/registry.ts +215 -7
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +272 -80
- package/src/serve/generation-gateway.ts +90 -18
- package/src/server.ts +643 -50
- 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/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/diffusion-lora.ts +216 -0
- package/src/train/flash-cce.ts +70 -15
- 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 +289 -30
- package/src/train/prefix-shared.ts +33 -5
- package/src/train/segmented.ts +67 -11
- package/src/train/trainer.ts +92 -28
- package/src/vision/diffusion-vision.ts +620 -0
- package/src/web/skills/memory/SKILL.md +40 -17
package/src/embed.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Text-embedding entry point. Wraps Qwen3Model.embedPooled (last-token hidden →
|
|
2
|
+
// L2-normalized vector) with the Qwen3-Embedding input convention so the CLI
|
|
3
|
+
// (`mlx-bun embed`), the server (`/v1/embeddings`), and in-repo experiments all
|
|
4
|
+
// produce the SAME vectors — the ones verified bit-exact vs mlx-lm in
|
|
5
|
+
// tests/qwen3-embed-parity.test.ts.
|
|
6
|
+
|
|
7
|
+
import { Qwen3Model } from "./model/qwen3";
|
|
8
|
+
import type { RuntimeModel } from "./model/factory";
|
|
9
|
+
import type { LoadedTokenizer } from "./tokenizer";
|
|
10
|
+
import * as ops from "./mlx/ops";
|
|
11
|
+
|
|
12
|
+
/** <|endoftext|> — the token Qwen3-Embedding terminates every input with and
|
|
13
|
+
* pools the last position of. */
|
|
14
|
+
export const EMBED_EOD = 151643;
|
|
15
|
+
|
|
16
|
+
/** Only plain-Qwen3 (the Qwen3-Embedding backbone) exposes a pooled-embedding
|
|
17
|
+
* path today. Narrow the served RuntimeModel before embedding. */
|
|
18
|
+
export function isEmbeddingModel(model: RuntimeModel): model is Qwen3Model {
|
|
19
|
+
return model instanceof Qwen3Model;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Qwen3-Embedding query format: a task instruction steers WHICH similarity axis
|
|
23
|
+
* the geometry reflects ("represent this for topic routing" vs "...for
|
|
24
|
+
* sentiment"). Documents are embedded raw; only queries get an instruction. */
|
|
25
|
+
export function withInstruction(text: string, instruction?: string): string {
|
|
26
|
+
return instruction ? `Instruct: ${instruction}\nQuery:${text}` : text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Tripwire: counts embedOne invocations (embedMany accrues via embedOne) so
|
|
30
|
+
* callers can assert the pipeline embeds exactly as many times as expected. */
|
|
31
|
+
let embedCallCount = 0;
|
|
32
|
+
|
|
33
|
+
/** Reset the embed tripwire counter to 0. */
|
|
34
|
+
export function resetEmbedCounter(): void {
|
|
35
|
+
embedCallCount = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Read the embed tripwire counter (total embedOne calls since last reset). */
|
|
39
|
+
export function getEmbedCounter(): number {
|
|
40
|
+
return embedCallCount;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface EmbedResult {
|
|
44
|
+
/** L2-normalized embedding (length = model hidden size). */
|
|
45
|
+
vector: Float32Array;
|
|
46
|
+
/** Token count of the embedded input (incl. the EOD pooling token). */
|
|
47
|
+
tokens: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Embed one text. `instruction` (optional) applies the query format. */
|
|
51
|
+
export function embedOne(
|
|
52
|
+
model: Qwen3Model,
|
|
53
|
+
tok: LoadedTokenizer,
|
|
54
|
+
text: string,
|
|
55
|
+
instruction?: string,
|
|
56
|
+
): EmbedResult {
|
|
57
|
+
embedCallCount++;
|
|
58
|
+
// addSpecialTokens=false matches the bit-exact parity path (Qwen3 has no BOS;
|
|
59
|
+
// we append the EOD pooling token ourselves).
|
|
60
|
+
const ids = [...tok.encode(withInstruction(text, instruction), false), EMBED_EOD];
|
|
61
|
+
const idArr = ops.fromInt32(ids, [1, ids.length]);
|
|
62
|
+
const vec = model.embedPooled(idArr);
|
|
63
|
+
idArr.dispose();
|
|
64
|
+
const out = vec.toFloat32();
|
|
65
|
+
vec.dispose();
|
|
66
|
+
return { vector: out, tokens: ids.length };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Embed many texts (one forward each — the runtime is single-sequence). */
|
|
70
|
+
export function embedMany(
|
|
71
|
+
model: Qwen3Model,
|
|
72
|
+
tok: LoadedTokenizer,
|
|
73
|
+
texts: string[],
|
|
74
|
+
instruction?: string,
|
|
75
|
+
): EmbedResult[] {
|
|
76
|
+
return texts.map((t) => embedOne(model, tok, t, instruction));
|
|
77
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Perplexity over a local text/JSONL dataset — mlx_lm.perplexity methodology.
|
|
2
|
+
//
|
|
3
|
+
// The reference (mlx_lm/perplexity.py, mlx-lm 0.31.3) does:
|
|
4
|
+
// 1. tokenize dataset samples, visit them in a seeded random permutation,
|
|
5
|
+
// 2. concatenate tokens into one stream and cut it into NON-OVERLAPPING
|
|
6
|
+
// rows of `sequence_length` (no sliding-window stride, no BOS re-seeding
|
|
7
|
+
// per row), keeping at most `num_samples` rows,
|
|
8
|
+
// 3. per batch: logits = model(batch[:, :-1]) in f32, per-token
|
|
9
|
+
// cross-entropy vs batch[:, 1:] — EVERY position counts,
|
|
10
|
+
// 4. ppl = exp(mean loss); SE via the delta method:
|
|
11
|
+
// se_ppl = ppl · std(losses, ddof=1) / √N.
|
|
12
|
+
//
|
|
13
|
+
// We reproduce 2-4 exactly. Step 1 differs only in the data source: the
|
|
14
|
+
// reference pulls a Hugging Face dataset (network); we take a LOCAL .txt or
|
|
15
|
+
// .jsonl file — never a download. The permutation RNG is a seeded
|
|
16
|
+
// deterministic shuffle (not NumPy's), so row selection is reproducible per
|
|
17
|
+
// seed but not bit-matched to the Python tool's sampling.
|
|
18
|
+
//
|
|
19
|
+
// The forward reuses the existing full-sequence machinery
|
|
20
|
+
// (src/train/forward.ts trainForward → model.forwardHidden + logitsFromHidden).
|
|
21
|
+
|
|
22
|
+
import { clearCache, Dtype } from "../mlx/ffi";
|
|
23
|
+
import * as ops from "../mlx/ops";
|
|
24
|
+
import { trainForward } from "../train/forward";
|
|
25
|
+
import type { RuntimeModel } from "../model/factory";
|
|
26
|
+
|
|
27
|
+
export interface PplDataOptions {
|
|
28
|
+
sequenceLength: number;
|
|
29
|
+
/** Max rows to keep (-1 / 0 = all available). */
|
|
30
|
+
numSamples: number;
|
|
31
|
+
/** Seed for the sample-order shuffle (reference default: 123). */
|
|
32
|
+
seed: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PplResult {
|
|
36
|
+
ppl: number;
|
|
37
|
+
/** Delta-method standard error of the perplexity. */
|
|
38
|
+
standardError: number;
|
|
39
|
+
meanLoss: number;
|
|
40
|
+
tokens: number;
|
|
41
|
+
rows: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Deterministic PRNG (mulberry32) for the seeded sample shuffle. */
|
|
45
|
+
function mulberry32(seed: number): () => number {
|
|
46
|
+
let s = seed >>> 0;
|
|
47
|
+
return () => {
|
|
48
|
+
s = (s + 0x6d2b79f5) >>> 0;
|
|
49
|
+
let t = s;
|
|
50
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
51
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
52
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Split a raw dataset file into text samples: `.jsonl` → one sample per
|
|
57
|
+
* line from the `text` field; anything else → the whole file is one sample. */
|
|
58
|
+
export function parseSamples(raw: string, path: string): string[] {
|
|
59
|
+
if (path.endsWith(".jsonl") || path.endsWith(".ndjson")) {
|
|
60
|
+
const samples: string[] = [];
|
|
61
|
+
for (const [i, line] of raw.split("\n").entries()) {
|
|
62
|
+
if (!line.trim()) continue;
|
|
63
|
+
let row: unknown;
|
|
64
|
+
try {
|
|
65
|
+
row = JSON.parse(line);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(`${path}:${i + 1}: not valid JSON`);
|
|
68
|
+
}
|
|
69
|
+
const text = (row as { text?: unknown }).text;
|
|
70
|
+
if (typeof text !== "string")
|
|
71
|
+
throw new Error(`${path}:${i + 1}: expected a {"text": "..."} row`);
|
|
72
|
+
samples.push(text);
|
|
73
|
+
}
|
|
74
|
+
return samples;
|
|
75
|
+
}
|
|
76
|
+
return raw.length > 0 ? [raw] : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Tokenize samples (seeded shuffle), concatenate, and pack into
|
|
80
|
+
* non-overlapping rows of `sequenceLength` tokens (reference load_data). */
|
|
81
|
+
export function packRows(
|
|
82
|
+
sampleTokens: number[][],
|
|
83
|
+
opts: PplDataOptions,
|
|
84
|
+
): Int32Array[] {
|
|
85
|
+
const { sequenceLength: L, numSamples, seed } = opts;
|
|
86
|
+
const order = sampleTokens.map((_, i) => i);
|
|
87
|
+
const rnd = mulberry32(seed);
|
|
88
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
89
|
+
const j = Math.floor(rnd() * (i + 1));
|
|
90
|
+
[order[i], order[j]] = [order[j]!, order[i]!];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const wanted = numSamples > 0 ? L * numSamples : Infinity;
|
|
94
|
+
const stream: number[] = [];
|
|
95
|
+
for (const idx of order) {
|
|
96
|
+
if (stream.length >= wanted) break;
|
|
97
|
+
stream.push(...sampleTokens[idx]!);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nRows = Math.min(
|
|
101
|
+
Math.floor(stream.length / L),
|
|
102
|
+
numSamples > 0 ? numSamples : Infinity,
|
|
103
|
+
);
|
|
104
|
+
const rows: Int32Array[] = [];
|
|
105
|
+
for (let r = 0; r < nRows; r++)
|
|
106
|
+
rows.push(Int32Array.from(stream.slice(r * L, (r + 1) * L)));
|
|
107
|
+
return rows;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Evaluate perplexity over packed rows. Per batch (all rows are the same
|
|
111
|
+
* length, so the stock unpadded forward applies): f32 logits over
|
|
112
|
+
* batch[:, :-1], per-token CE vs batch[:, 1:], every position counted. */
|
|
113
|
+
export function evalPpl(
|
|
114
|
+
model: RuntimeModel,
|
|
115
|
+
rows: Int32Array[],
|
|
116
|
+
batchSize: number,
|
|
117
|
+
onBatch?: (done: number, total: number) => void,
|
|
118
|
+
): PplResult {
|
|
119
|
+
const L = rows[0]!.length;
|
|
120
|
+
const allLosses: number[] = [];
|
|
121
|
+
const numBatches = Math.ceil(rows.length / batchSize);
|
|
122
|
+
|
|
123
|
+
for (let s = 0, bi = 0; s < rows.length; s += batchSize, bi++) {
|
|
124
|
+
const batch = rows.slice(s, s + batchSize);
|
|
125
|
+
const B = batch.length;
|
|
126
|
+
const inputs: number[] = [];
|
|
127
|
+
const targets: number[] = [];
|
|
128
|
+
for (const row of batch) {
|
|
129
|
+
for (let i = 0; i < L - 1; i++) inputs.push(row[i]!);
|
|
130
|
+
for (let i = 1; i < L; i++) targets.push(row[i]!);
|
|
131
|
+
}
|
|
132
|
+
const ids = ops.fromInt32(inputs, [B, L - 1]);
|
|
133
|
+
const tgt = ops.fromInt32(targets, [B, L - 1, 1]);
|
|
134
|
+
const logits = trainForward(model, ids);
|
|
135
|
+
const f32 = logits.astype(Dtype.float32);
|
|
136
|
+
logits.dispose();
|
|
137
|
+
// CE[t] = logsumexp(logits[t]) − logits[t, target] (f32, like nn.losses)
|
|
138
|
+
const lse = ops.logsumexpAxis(f32, 2, false); // [B, L-1]
|
|
139
|
+
const picked = ops.takeAlongAxis(f32, tgt, 2); // [B, L-1, 1]
|
|
140
|
+
f32.dispose();
|
|
141
|
+
const picked2d = ops.reshape(picked, [B, L - 1]);
|
|
142
|
+
picked.dispose();
|
|
143
|
+
const losses = ops.sub(lse, picked2d);
|
|
144
|
+
lse.dispose();
|
|
145
|
+
picked2d.dispose();
|
|
146
|
+
const vals = losses.toFloat32();
|
|
147
|
+
losses.dispose();
|
|
148
|
+
ids.dispose();
|
|
149
|
+
tgt.dispose();
|
|
150
|
+
for (const v of vals) allLosses.push(v);
|
|
151
|
+
clearCache();
|
|
152
|
+
onBatch?.(bi + 1, numBatches);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const n = allLosses.length;
|
|
156
|
+
const mean = allLosses.reduce((a, b) => a + b, 0) / n;
|
|
157
|
+
const ppl = Math.exp(mean);
|
|
158
|
+
// Sample std (ddof=1) → delta-method SE of the perplexity.
|
|
159
|
+
const varSum = allLosses.reduce((a, b) => a + (b - mean) ** 2, 0);
|
|
160
|
+
const std = n > 1 ? Math.sqrt(varSum / (n - 1)) : 0;
|
|
161
|
+
const standardError = ppl * (std / Math.sqrt(n));
|
|
162
|
+
|
|
163
|
+
return { ppl, standardError, meanLoss: mean, tokens: n, rows: rows.length };
|
|
164
|
+
}
|
package/src/eval/runner.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { ChatTemplate } from "../chat-template";
|
|
|
11
11
|
import { loadModelConfig, type ModelConfig } from "../config";
|
|
12
12
|
import { Weights } from "../weights";
|
|
13
13
|
import { createModel, type RuntimeModel } from "../model/factory";
|
|
14
|
+
import { DiffusionGemmaModel } from "../model/diffusion-gemma";
|
|
14
15
|
import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
|
|
15
16
|
import { resolveModelDir } from "./kl";
|
|
16
17
|
|
|
@@ -98,6 +99,11 @@ export interface GenOpts {
|
|
|
98
99
|
* (its capability numbers are non-thinking; e.g. MiniCPM5 IFEval 64.7 is
|
|
99
100
|
* non-thinking). Set MLX_BUN_EVAL_THINK=1 or pass true to evaluate thinking mode. */
|
|
100
101
|
enableThinking?: boolean;
|
|
102
|
+
/** Explicit KV-quant scheme (cli `generate --l2/--l3/--kv-quant`, resolved with
|
|
103
|
+
* serve's semantics). Overrides the MLX_BUN_EVAL_KV_QUANT env default; an empty
|
|
104
|
+
* object forces bf16. When it names quantized KV, generation runs the product
|
|
105
|
+
* generate() path (same tokens as `serve` with the same scheme). */
|
|
106
|
+
kvScheme?: Pick<GenerateOptions, "kvBits" | "kvConfig" | "quantizedKvStart">;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
/** Bit-exact greedy decode: raw `model.forward` + argmax loop, which matches mlx-lm
|
|
@@ -105,7 +111,7 @@ export interface GenOpts {
|
|
|
105
111
|
* (forwardHidden/logitsFromHidden + pipelined sampler) diverges from the raw forward
|
|
106
112
|
* on near-ties past ~32 tokens, so the EVAL decodes bit-exactly itself rather than
|
|
107
113
|
* alter the serving path. Greedy + full-precision KV only. */
|
|
108
|
-
function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: number): string {
|
|
114
|
+
export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: number): string {
|
|
109
115
|
if (tm.activeAdapters) tm.model.loraState.active = tm.activeAdapters; // apply the mounted adapter
|
|
110
116
|
const cache = tm.model.makeCache();
|
|
111
117
|
try {
|
|
@@ -147,21 +153,27 @@ export async function generateText(tm: TaskModel, body: string, opts: GenOpts =
|
|
|
147
153
|
// plain mlx_lm.generate (kv-quant lives in the serving runtime, not the eval). So
|
|
148
154
|
// for like-for-like parity we default the eval to UNQUANTIZED KV; MMLU's argmax
|
|
149
155
|
// path was already unquantized, which is why it matched while generation didn't.
|
|
150
|
-
// MLX_BUN_EVAL_KV_QUANT=1 generates through the model's quantized KV (serving
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
// MLX_BUN_EVAL_KV_QUANT=1 generates through the model's quantized KV (serving
|
|
157
|
+
// config); an explicit opts.kvScheme (cli generate's resolved tier route) wins.
|
|
158
|
+
const envKv: GenOpts["kvScheme"] | undefined =
|
|
159
|
+
(process.env.MLX_BUN_EVAL_KV_QUANT === "1" && tm.config.kvQuant?.length)
|
|
160
|
+
? { kvConfig: tm.config.kvQuant, quantizedKvStart: 0 } : undefined;
|
|
161
|
+
const kvScheme = opts.kvScheme ?? envKv;
|
|
162
|
+
const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length));
|
|
153
163
|
|
|
154
164
|
// Parity default — greedy + full-precision KV + no sampler arm: decode bit-exactly
|
|
155
165
|
// via the raw forward (matches mlx-lm token-for-token). Sampler arms / kv-quant fall
|
|
156
|
-
// through to the product generate().
|
|
157
|
-
|
|
166
|
+
// through to the product generate(). DiffusionGemma is non-autoregressive (no AR
|
|
167
|
+
// forward) — always go through generate(), which routes it to the denoising engine.
|
|
168
|
+
const isDiffusion = tm.model instanceof DiffusionGemmaModel;
|
|
169
|
+
if (!isDiffusion && !kvActive && !opts.sampler && !tm.samplerOverride) {
|
|
158
170
|
return greedyDecodeBitExact(tm, ids, maxTokens);
|
|
159
171
|
}
|
|
160
172
|
const gen = generate(tm.model, ids, {
|
|
161
173
|
maxTokens,
|
|
162
174
|
temperature: 0, // greedy default — deterministic head-to-head arms
|
|
163
175
|
adapters: tm.activeAdapters ?? [], // per-request activation of the mounted eval-adapter
|
|
164
|
-
...(
|
|
176
|
+
...(kvActive ? kvScheme : {}),
|
|
165
177
|
...(opts.sampler ?? {}), // overrides temperature when provided
|
|
166
178
|
...(tm.samplerOverride ?? {}), // arm override wins over the task's own sampler
|
|
167
179
|
});
|
package/src/eval/tasks/bfcl.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// of the task — is ported exactly. (See summary for this one note.)
|
|
16
16
|
|
|
17
17
|
import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
18
|
-
import { type ToolDefinition } from "
|
|
18
|
+
import { type ToolDefinition } from "../../chat-template";
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Data shapes (the merged jsonl: each line = {query, answer}).
|
package/src/generate.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import * as ops from "./mlx/ops";
|
|
17
17
|
import { CompiledDecode } from "./model/compiled-decode";
|
|
18
18
|
import { Gemma4Model, KVCache, RotatingKVCache, type Cache } from "./model/gemma4";
|
|
19
|
+
import { DiffusionGemmaModel } from "./model/diffusion-gemma";
|
|
20
|
+
import { diffusionGenerate } from "./diffusion/diffusion-generate";
|
|
19
21
|
import type { RuntimeModel } from "./model/factory";
|
|
20
22
|
import type { KvQuantSpec } from "./config";
|
|
21
23
|
import {
|
|
@@ -55,15 +57,47 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
55
57
|
* model's LoraState for exactly the duration of this generation —
|
|
56
58
|
* a plain field, safe because the generation queue is serialized. */
|
|
57
59
|
adapters?: string[];
|
|
60
|
+
/** DiffusionGemma image-text-to-text: channel-first pixel_values [1,3,H,W].
|
|
61
|
+
* When set, `promptTokens` are the spliced (<|image|>-expanded) ids and the
|
|
62
|
+
* denoising engine prefills the merged vision features. Caller keeps ownership. */
|
|
63
|
+
visionPixels?: MlxArray;
|
|
64
|
+
/** Capture each emitted token's log-probability (mlx_lm.server `logprobs`).
|
|
65
|
+
* The distribution matches mlx-lm generate_step exactly: full-vocab
|
|
66
|
+
* log-softmax of the logits AFTER logits processors, BEFORE the sampler's
|
|
67
|
+
* temperature/top-p/top-k/min-p/XTC (generate.py L409-422). Off by default —
|
|
68
|
+
* the hot path pays nothing when unset. */
|
|
69
|
+
logprobs?: boolean;
|
|
70
|
+
/** Capture the top-k (token id, logprob) pairs per emitted token from the
|
|
71
|
+
* same distribution (mlx_lm.server `top_logprobs`). 0/unset = off. */
|
|
72
|
+
topLogprobs?: number;
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
/** Per-token logprob info (only present when GenerateOptions requested it). */
|
|
76
|
+
export interface TokenLogprobs {
|
|
77
|
+
/** logprob of the emitted token (requires options.logprobs). */
|
|
78
|
+
logprob?: number;
|
|
79
|
+
/** top-k (id, logprob) pairs, sorted by logprob descending (requires
|
|
80
|
+
* options.topLogprobs > 0). mlx-lm leaves argpartition order unspecified;
|
|
81
|
+
* sorting is the deterministic reading of the same set. */
|
|
82
|
+
top?: { id: number; logprob: number }[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Port of mlx-lm maybe_quantize_kv_cache + BOTH halves of optiq serve's
|
|
86
|
+
* per-layer patched variant (incl. patch_rotating_to_quantized: rotating
|
|
87
|
+
* caches convert too — Phase 9):
|
|
88
|
+
* - per-layer bits/group_size selection (kvConfig overrides kvBits,
|
|
89
|
+
* matching optiq's --kv-config precedence; shipped kv_config.json
|
|
90
|
+
* files list full-attention layers only, so rotating quantization
|
|
91
|
+
* engages through uniform kvBits — like optiq --kv-bits — or a config
|
|
92
|
+
* that names sliding layers), and
|
|
93
|
+
* - STREAMING conversion (optiq streaming_kv_quant / serve.py
|
|
94
|
+
* patched_maybe_quantize): eval each layer's quantized triples and
|
|
95
|
+
* clear the buffer pool before building the next layer's conversion.
|
|
96
|
+
* Lazily batching every layer's toQuantized into one eval pins ALL
|
|
97
|
+
* layers' bf16 K/V as graph inputs alongside ALL quantized outputs —
|
|
98
|
+
* the exact transient optiq's fix kills (16.35 → 7.60 GB at 32k on a
|
|
99
|
+
* 24 GB Mac). Numerics untouched: same quantize math, only the eval
|
|
100
|
+
* ordering is forced (tests/kv-quant.test.ts, tests/rotating-kvq.test.ts). */
|
|
67
101
|
export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void {
|
|
68
102
|
const { kvBits, kvConfig } = options;
|
|
69
103
|
if (!kvBits && !kvConfig?.length) return;
|
|
@@ -81,10 +115,16 @@ export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void
|
|
|
81
115
|
if (c.offset === 0) continue;
|
|
82
116
|
if (byLayer) {
|
|
83
117
|
const e = byLayer.get(i);
|
|
84
|
-
if (e)
|
|
118
|
+
if (!e) continue;
|
|
119
|
+
cache[i] = c.toQuantized(e.groupSize, e.bits);
|
|
85
120
|
} else {
|
|
86
121
|
cache[i] = c.toQuantized(options.kvGroupSize ?? 64, kvBits!);
|
|
87
122
|
}
|
|
123
|
+
// Streaming half: materialize THIS layer's conversion now so its bf16
|
|
124
|
+
// source (already released by toQuantized) frees before the next layer
|
|
125
|
+
// converts — the transient stays ~one layer, not the whole cache.
|
|
126
|
+
ops.evalAll(cache[i]!.state());
|
|
127
|
+
clearCache();
|
|
88
128
|
}
|
|
89
129
|
}
|
|
90
130
|
|
|
@@ -106,6 +146,8 @@ export interface GenerateStats {
|
|
|
106
146
|
export interface GeneratedToken {
|
|
107
147
|
token: number;
|
|
108
148
|
index: number;
|
|
149
|
+
/** Present only when GenerateOptions.logprobs / topLogprobs requested it. */
|
|
150
|
+
logprobs?: TokenLogprobs;
|
|
109
151
|
}
|
|
110
152
|
|
|
111
153
|
export class Generation implements AsyncIterable<GeneratedToken> {
|
|
@@ -170,7 +212,13 @@ export function generate(
|
|
|
170
212
|
promptTokens: number[],
|
|
171
213
|
options: GenerateOptions = {},
|
|
172
214
|
): Generation {
|
|
173
|
-
|
|
215
|
+
// DiffusionGemma is non-autoregressive: route to the denoising engine instead
|
|
216
|
+
// of the AR decode loop. Same Generation/GenerateStats contract so the CLI and
|
|
217
|
+
// server stream it through the existing token machinery.
|
|
218
|
+
let inner =
|
|
219
|
+
model instanceof DiffusionGemmaModel
|
|
220
|
+
? generateDiffusionInner(model, promptTokens, options)
|
|
221
|
+
: generateInner(model, promptTokens, options);
|
|
174
222
|
if (options.adapters?.length) inner = adapterScoped(model, options.adapters, inner);
|
|
175
223
|
const wire =
|
|
176
224
|
process.env.MLX_BUN_FORCE_WIRE === "1" ||
|
|
@@ -178,6 +226,48 @@ export function generate(
|
|
|
178
226
|
return new Generation(wire ? wiredScoped(inner) : inner);
|
|
179
227
|
}
|
|
180
228
|
|
|
229
|
+
/** Non-autoregressive diffusion generation, adapted to the AR Generation
|
|
230
|
+
* contract. Runs the denoising engine (its own prefill + canvas loop) and
|
|
231
|
+
* streams the emitted tokens. v1: greedy (temperature 0, confidence-threshold
|
|
232
|
+
* sampler — the OptiQ default); per-block intra-stream + temperature>0
|
|
233
|
+
* (categorical) are follow-ups. */
|
|
234
|
+
async function* generateDiffusionInner(
|
|
235
|
+
model: DiffusionGemmaModel,
|
|
236
|
+
promptTokens: number[],
|
|
237
|
+
options: GenerateOptions,
|
|
238
|
+
): AsyncGenerator<GeneratedToken, GenerateStats> {
|
|
239
|
+
const t0 = performance.now();
|
|
240
|
+
const maxTokens = options.maxTokens ?? 256;
|
|
241
|
+
// A fresh random canvas seed per request unless the caller pins one.
|
|
242
|
+
const seed =
|
|
243
|
+
options.seed !== undefined
|
|
244
|
+
? BigInt(options.seed)
|
|
245
|
+
: BigInt(Math.floor(Math.random() * 0x7fffffff));
|
|
246
|
+
// The shipped checkpoint's tokenizer stops on {1, 106}; union any caller eos.
|
|
247
|
+
const eos = [...new Set([1, 106, ...(options.eosTokenIds ?? [])])];
|
|
248
|
+
const result = diffusionGenerate(model, promptTokens, {
|
|
249
|
+
maxTokens,
|
|
250
|
+
sampler: "confidence-threshold",
|
|
251
|
+
temperature: 0,
|
|
252
|
+
eosTokenIds: eos,
|
|
253
|
+
seed,
|
|
254
|
+
visionPixels: options.visionPixels,
|
|
255
|
+
});
|
|
256
|
+
const decodeMs = performance.now() - t0;
|
|
257
|
+
let index = 0;
|
|
258
|
+
for (const token of result.tokens) yield { token, index: index++ };
|
|
259
|
+
return {
|
|
260
|
+
promptTokens: promptTokens.length,
|
|
261
|
+
cachedTokens: 0,
|
|
262
|
+
generatedTokens: result.tokens.length,
|
|
263
|
+
prefillTps: 0,
|
|
264
|
+
decodeTps: result.tokens.length / Math.max(decodeMs / 1000, 1e-9),
|
|
265
|
+
prefillMs: 0,
|
|
266
|
+
decodeMs,
|
|
267
|
+
cacheTokens: [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
181
271
|
/** Hold the model's active-adapter list for exactly this generation. */
|
|
182
272
|
async function* adapterScoped(
|
|
183
273
|
model: RuntimeModel,
|
|
@@ -219,6 +309,14 @@ async function* generateInner(
|
|
|
219
309
|
const processors = makeLogitsProcessors(options);
|
|
220
310
|
const needsTokenHistory = processors.length > 0;
|
|
221
311
|
|
|
312
|
+
// logprobs capture (mlx_lm.server semantics — see GenerateOptions.logprobs).
|
|
313
|
+
// Everything below is gated: when neither flag is set, no extra ops, evals,
|
|
314
|
+
// or readbacks happen on the hot path.
|
|
315
|
+
const captureSelLp = options.logprobs === true;
|
|
316
|
+
const captureTopK =
|
|
317
|
+
options.topLogprobs && options.topLogprobs > 0 ? options.topLogprobs : 0;
|
|
318
|
+
const capture = captureSelLp || captureTopK > 0;
|
|
319
|
+
|
|
222
320
|
const ownsCache = !options.cache;
|
|
223
321
|
const cache = options.cache ?? model.makeCache();
|
|
224
322
|
const cachedTokens = cache[0]!.offset;
|
|
@@ -229,16 +327,73 @@ async function* generateInner(
|
|
|
229
327
|
/** device-side token history (only maintained when processors need it) */
|
|
230
328
|
let history: MlxArray | null = null;
|
|
231
329
|
|
|
232
|
-
|
|
233
|
-
|
|
330
|
+
/** Device-side logprob capture for one step, computed from the SAME lp the
|
|
331
|
+
* sampler saw (post-processors, pre-truncation) — read back lazily with the
|
|
332
|
+
* token so decode pipelining is preserved. */
|
|
333
|
+
interface StepExtras {
|
|
334
|
+
sel: MlxArray | null; // [1,1] emitted token's logprob
|
|
335
|
+
topIdx: MlxArray | null; // [1,k] top-k token ids
|
|
336
|
+
topVals: MlxArray | null; // [1,k] their logprobs
|
|
337
|
+
}
|
|
338
|
+
const extrasArrays = (e: StepExtras | null): MlxArray[] =>
|
|
339
|
+
e ? [e.sel, e.topIdx, e.topVals].filter((a): a is MlxArray => a !== null) : [];
|
|
340
|
+
const disposeExtras = (e: StepExtras | null): void => {
|
|
341
|
+
if (e) for (const a of [e.sel, e.topIdx, e.topVals]) a?.dispose();
|
|
342
|
+
};
|
|
343
|
+
/** Read extras back to JS (forces eval — they were async-dispatched with the
|
|
344
|
+
* token, so this normally just copies) and dispose the device arrays. */
|
|
345
|
+
const readExtras = (e: StepExtras | null): TokenLogprobs | undefined => {
|
|
346
|
+
if (!e) return undefined;
|
|
347
|
+
const out: TokenLogprobs = {};
|
|
348
|
+
if (e.sel) out.logprob = e.sel.toFloat32()[0]!;
|
|
349
|
+
if (e.topIdx && e.topVals) {
|
|
350
|
+
// uint32 ids read via the f32 cast: exact for ids < 2^24 (vocab ≪ 16M)
|
|
351
|
+
const ids = e.topIdx.toFloat32();
|
|
352
|
+
const vals = e.topVals.toFloat32();
|
|
353
|
+
out.top = Array.from(ids, (id, i) => ({ id, logprob: vals[i]! })).sort(
|
|
354
|
+
(a, b) => b.logprob - a.logprob,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
disposeExtras(e);
|
|
358
|
+
return out;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// logits [1,1,V] → sampled token array [1] (+ optional logprob capture,
|
|
362
|
+
// all on-device)
|
|
363
|
+
const sampleStep = (
|
|
364
|
+
logits3d: MlxArray,
|
|
365
|
+
step: number,
|
|
366
|
+
): { tok: MlxArray; extras: StepExtras | null } => {
|
|
234
367
|
const V = logits3d.shape[2]!;
|
|
235
368
|
let logits = ops.reshape(logits3d, [1, V]);
|
|
236
369
|
for (const p of processors) logits = disposing(logits, p(history, logits));
|
|
237
370
|
const lp = toLogprobs(logits);
|
|
238
371
|
logits.dispose();
|
|
239
372
|
const tok = sampler(lp, step);
|
|
373
|
+
let extras: StepExtras | null = null;
|
|
374
|
+
if (capture) {
|
|
375
|
+
let sel: MlxArray | null = null;
|
|
376
|
+
let topIdx: MlxArray | null = null;
|
|
377
|
+
let topVals: MlxArray | null = null;
|
|
378
|
+
if (captureSelLp) {
|
|
379
|
+
const idx = ops.reshape(tok, [1, 1]);
|
|
380
|
+
sel = ops.takeAlongAxis(lp, idx, -1);
|
|
381
|
+
idx.dispose();
|
|
382
|
+
}
|
|
383
|
+
if (captureTopK > 0) {
|
|
384
|
+
// mlx-lm _format_top_logprobs: argpartition(-logprobs, kth=k-1)[:k]
|
|
385
|
+
const k = Math.min(captureTopK, V);
|
|
386
|
+
const negLp = ops.neg(lp);
|
|
387
|
+
const part = ops.argpartitionAxis(negLp, k - 1, -1);
|
|
388
|
+
const idxView = part.slice([0, 0], [1, k]);
|
|
389
|
+
topIdx = ops.contiguous(idxView); // slices are strided views; raw readback needs contiguous
|
|
390
|
+
topVals = ops.takeAlongAxis(lp, topIdx, -1);
|
|
391
|
+
for (const a of [negLp, part, idxView]) a.dispose();
|
|
392
|
+
}
|
|
393
|
+
extras = { sel, topIdx, topVals };
|
|
394
|
+
}
|
|
240
395
|
lp.dispose();
|
|
241
|
-
return tok;
|
|
396
|
+
return { tok, extras };
|
|
242
397
|
};
|
|
243
398
|
|
|
244
399
|
const pushHistory = (tok: MlxArray) => {
|
|
@@ -263,6 +418,8 @@ async function* generateInner(
|
|
|
263
418
|
const forwarded: number[] = [];
|
|
264
419
|
let pending: MlxArray | null = null;
|
|
265
420
|
let nextPending: MlxArray | null = null;
|
|
421
|
+
let pendingExtras: StepExtras | null = null;
|
|
422
|
+
let nextExtras: StepExtras | null = null;
|
|
266
423
|
let finished = false;
|
|
267
424
|
let threw = false;
|
|
268
425
|
const makeStats = (): GenerateStats => ({
|
|
@@ -325,7 +482,9 @@ async function* generateInner(
|
|
|
325
482
|
h0.dispose();
|
|
326
483
|
const logits0 = model.logitsFromHidden(hLast);
|
|
327
484
|
hLast.dispose();
|
|
328
|
-
|
|
485
|
+
const s0 = sampleStep(logits0, 0); // token array [1] (+ optional extras)
|
|
486
|
+
pending = s0.tok;
|
|
487
|
+
pendingExtras = s0.extras;
|
|
329
488
|
logits0.dispose();
|
|
330
489
|
// mirror mlx-lm generate_step: async-dispatch the first token's
|
|
331
490
|
// compute; the prefill clock keeps running until the token ARRIVES
|
|
@@ -335,7 +494,7 @@ async function* generateInner(
|
|
|
335
494
|
// to prompt_time, not decode — replicated so cross-stack decode
|
|
336
495
|
// tok/s measure the same quantity. The boundary cost is real and
|
|
337
496
|
// scales with prompt length; it belongs to "having prefilled".
|
|
338
|
-
ops.asyncEvalAll([pending]);
|
|
497
|
+
ops.asyncEvalAll([pending, ...extrasArrays(pendingExtras)]);
|
|
339
498
|
|
|
340
499
|
// ---- decode (pipelined) ----
|
|
341
500
|
// Compiled decode (docs/design/optimization_plan.md Phase A): replay the per-step
|
|
@@ -359,8 +518,11 @@ async function* generateInner(
|
|
|
359
518
|
let stop = false;
|
|
360
519
|
while (!stop) {
|
|
361
520
|
const cur = pending!;
|
|
521
|
+
const curExtras = pendingExtras;
|
|
522
|
+
pendingExtras = null;
|
|
362
523
|
// build step n+1's graph from the *unread* pending token
|
|
363
524
|
nextPending = null;
|
|
525
|
+
nextExtras = null;
|
|
364
526
|
if (generated + 1 < maxTokens) {
|
|
365
527
|
maybeQuantizeKv(cache, options);
|
|
366
528
|
pushHistory(cur);
|
|
@@ -372,8 +534,11 @@ async function* generateInner(
|
|
|
372
534
|
logits = r.logits;
|
|
373
535
|
evalWith = r.evalWith;
|
|
374
536
|
} catch (e) {
|
|
375
|
-
//
|
|
376
|
-
//
|
|
537
|
+
// Safe to re-forward the SAME token below: a failed step is
|
|
538
|
+
// transactional — segmented mode stages ring adopts and rolls
|
|
539
|
+
// back committed js-layer writes on throw (see #stepSegmented),
|
|
540
|
+
// and buffer growth done by a partial prepare is benign for the
|
|
541
|
+
// uncompiled path (updateAndFetch re-checks capacity).
|
|
377
542
|
compiled = null;
|
|
378
543
|
console.warn(`compiled decode disabled for this generation: ${e}`);
|
|
379
544
|
}
|
|
@@ -385,9 +550,11 @@ async function* generateInner(
|
|
|
385
550
|
logits = model.logitsFromHidden(h);
|
|
386
551
|
h.dispose();
|
|
387
552
|
}
|
|
388
|
-
|
|
553
|
+
const sn = sampleStep(logits, generated + 1);
|
|
554
|
+
nextPending = sn.tok;
|
|
555
|
+
nextExtras = sn.extras;
|
|
389
556
|
logits.dispose();
|
|
390
|
-
ops.asyncEvalAll([nextPending, ...evalWith]);
|
|
557
|
+
ops.asyncEvalAll([nextPending, ...extrasArrays(nextExtras), ...evalWith]);
|
|
391
558
|
}
|
|
392
559
|
|
|
393
560
|
// sync-read step n's token while n+1 computes
|
|
@@ -406,11 +573,17 @@ async function* generateInner(
|
|
|
406
573
|
if (nextPending !== null) forwarded.push(token);
|
|
407
574
|
|
|
408
575
|
if (eosTokenIds.includes(token)) {
|
|
576
|
+
disposeExtras(curExtras);
|
|
409
577
|
nextPending?.dispose();
|
|
410
578
|
nextPending = null;
|
|
579
|
+
disposeExtras(nextExtras);
|
|
580
|
+
nextExtras = null;
|
|
411
581
|
stop = true;
|
|
412
582
|
} else {
|
|
413
|
-
|
|
583
|
+
// readExtras before the yield: if the consumer breaks at this yield,
|
|
584
|
+
// the extras are already read and disposed.
|
|
585
|
+
const logprobs = readExtras(curExtras);
|
|
586
|
+
yield { token, index: generated - 1, ...(logprobs ? { logprobs } : {}) };
|
|
414
587
|
// mlx-lm generate_step: clear_cache after token 0 (drops the
|
|
415
588
|
// remaining prefill transients) and every 256 tokens after
|
|
416
589
|
if ((generated - 1) % 256 === 0) clearCache();
|
|
@@ -419,6 +592,8 @@ async function* generateInner(
|
|
|
419
592
|
} else {
|
|
420
593
|
pending = nextPending;
|
|
421
594
|
nextPending = null;
|
|
595
|
+
pendingExtras = nextExtras;
|
|
596
|
+
nextExtras = null;
|
|
422
597
|
}
|
|
423
598
|
}
|
|
424
599
|
}
|
|
@@ -432,6 +607,8 @@ async function* generateInner(
|
|
|
432
607
|
if (!finished) {
|
|
433
608
|
pending?.dispose();
|
|
434
609
|
nextPending?.dispose();
|
|
610
|
+
disposeExtras(pendingExtras);
|
|
611
|
+
disposeExtras(nextExtras);
|
|
435
612
|
}
|
|
436
613
|
if (ownsCache) for (const c of cache) c.dispose();
|
|
437
614
|
history?.dispose();
|