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/generate.ts
CHANGED
|
@@ -61,15 +61,43 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
61
61
|
* When set, `promptTokens` are the spliced (<|image|>-expanded) ids and the
|
|
62
62
|
* denoising engine prefills the merged vision features. Caller keeps ownership. */
|
|
63
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;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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). */
|
|
73
101
|
export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void {
|
|
74
102
|
const { kvBits, kvConfig } = options;
|
|
75
103
|
if (!kvBits && !kvConfig?.length) return;
|
|
@@ -87,10 +115,16 @@ export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void
|
|
|
87
115
|
if (c.offset === 0) continue;
|
|
88
116
|
if (byLayer) {
|
|
89
117
|
const e = byLayer.get(i);
|
|
90
|
-
if (e)
|
|
118
|
+
if (!e) continue;
|
|
119
|
+
cache[i] = c.toQuantized(e.groupSize, e.bits);
|
|
91
120
|
} else {
|
|
92
121
|
cache[i] = c.toQuantized(options.kvGroupSize ?? 64, kvBits!);
|
|
93
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();
|
|
94
128
|
}
|
|
95
129
|
}
|
|
96
130
|
|
|
@@ -112,6 +146,8 @@ export interface GenerateStats {
|
|
|
112
146
|
export interface GeneratedToken {
|
|
113
147
|
token: number;
|
|
114
148
|
index: number;
|
|
149
|
+
/** Present only when GenerateOptions.logprobs / topLogprobs requested it. */
|
|
150
|
+
logprobs?: TokenLogprobs;
|
|
115
151
|
}
|
|
116
152
|
|
|
117
153
|
export class Generation implements AsyncIterable<GeneratedToken> {
|
|
@@ -273,6 +309,14 @@ async function* generateInner(
|
|
|
273
309
|
const processors = makeLogitsProcessors(options);
|
|
274
310
|
const needsTokenHistory = processors.length > 0;
|
|
275
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
|
+
|
|
276
320
|
const ownsCache = !options.cache;
|
|
277
321
|
const cache = options.cache ?? model.makeCache();
|
|
278
322
|
const cachedTokens = cache[0]!.offset;
|
|
@@ -283,16 +327,73 @@ async function* generateInner(
|
|
|
283
327
|
/** device-side token history (only maintained when processors need it) */
|
|
284
328
|
let history: MlxArray | null = null;
|
|
285
329
|
|
|
286
|
-
|
|
287
|
-
|
|
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 } => {
|
|
288
367
|
const V = logits3d.shape[2]!;
|
|
289
368
|
let logits = ops.reshape(logits3d, [1, V]);
|
|
290
369
|
for (const p of processors) logits = disposing(logits, p(history, logits));
|
|
291
370
|
const lp = toLogprobs(logits);
|
|
292
371
|
logits.dispose();
|
|
293
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
|
+
}
|
|
294
395
|
lp.dispose();
|
|
295
|
-
return tok;
|
|
396
|
+
return { tok, extras };
|
|
296
397
|
};
|
|
297
398
|
|
|
298
399
|
const pushHistory = (tok: MlxArray) => {
|
|
@@ -317,6 +418,8 @@ async function* generateInner(
|
|
|
317
418
|
const forwarded: number[] = [];
|
|
318
419
|
let pending: MlxArray | null = null;
|
|
319
420
|
let nextPending: MlxArray | null = null;
|
|
421
|
+
let pendingExtras: StepExtras | null = null;
|
|
422
|
+
let nextExtras: StepExtras | null = null;
|
|
320
423
|
let finished = false;
|
|
321
424
|
let threw = false;
|
|
322
425
|
const makeStats = (): GenerateStats => ({
|
|
@@ -379,7 +482,9 @@ async function* generateInner(
|
|
|
379
482
|
h0.dispose();
|
|
380
483
|
const logits0 = model.logitsFromHidden(hLast);
|
|
381
484
|
hLast.dispose();
|
|
382
|
-
|
|
485
|
+
const s0 = sampleStep(logits0, 0); // token array [1] (+ optional extras)
|
|
486
|
+
pending = s0.tok;
|
|
487
|
+
pendingExtras = s0.extras;
|
|
383
488
|
logits0.dispose();
|
|
384
489
|
// mirror mlx-lm generate_step: async-dispatch the first token's
|
|
385
490
|
// compute; the prefill clock keeps running until the token ARRIVES
|
|
@@ -389,7 +494,7 @@ async function* generateInner(
|
|
|
389
494
|
// to prompt_time, not decode — replicated so cross-stack decode
|
|
390
495
|
// tok/s measure the same quantity. The boundary cost is real and
|
|
391
496
|
// scales with prompt length; it belongs to "having prefilled".
|
|
392
|
-
ops.asyncEvalAll([pending]);
|
|
497
|
+
ops.asyncEvalAll([pending, ...extrasArrays(pendingExtras)]);
|
|
393
498
|
|
|
394
499
|
// ---- decode (pipelined) ----
|
|
395
500
|
// Compiled decode (docs/design/optimization_plan.md Phase A): replay the per-step
|
|
@@ -403,8 +508,16 @@ async function* generateInner(
|
|
|
403
508
|
// 0.6.0, and shapeless replay re-infers the whole tape whenever the
|
|
404
509
|
// growing attention windows change shape (= every step). Remove this
|
|
405
510
|
// when upstream implements GatherQMM::output_shapes.
|
|
511
|
+
// MLX_BUN_FUSED_DECODE=1 (the N-tiled fused decode experiment) cannot
|
|
512
|
+
// compose with compiled decode: the tile loop's bounds bake at trace-time
|
|
513
|
+
// N, so growing quantized caches silently stop attending their newest
|
|
514
|
+
// rows under shapeless replay (kernel-perf-review 2026-07; reproduced on
|
|
515
|
+
// e4b). The explicit opt-in wins over the default optimization — same
|
|
516
|
+
// policy as LoRA and MoE below. quantizedSdpa also throws on the combo
|
|
517
|
+
// as a backstop.
|
|
406
518
|
let compiled =
|
|
407
519
|
process.env.MLX_BUN_COMPILED_DECODE !== "0" &&
|
|
520
|
+
process.env.MLX_BUN_FUSED_DECODE !== "1" &&
|
|
408
521
|
!options.adapters?.length &&
|
|
409
522
|
model.config.modelType.startsWith("gemma4") &&
|
|
410
523
|
!model.config.text.enableMoeBlock
|
|
@@ -413,8 +526,11 @@ async function* generateInner(
|
|
|
413
526
|
let stop = false;
|
|
414
527
|
while (!stop) {
|
|
415
528
|
const cur = pending!;
|
|
529
|
+
const curExtras = pendingExtras;
|
|
530
|
+
pendingExtras = null;
|
|
416
531
|
// build step n+1's graph from the *unread* pending token
|
|
417
532
|
nextPending = null;
|
|
533
|
+
nextExtras = null;
|
|
418
534
|
if (generated + 1 < maxTokens) {
|
|
419
535
|
maybeQuantizeKv(cache, options);
|
|
420
536
|
pushHistory(cur);
|
|
@@ -426,8 +542,11 @@ async function* generateInner(
|
|
|
426
542
|
logits = r.logits;
|
|
427
543
|
evalWith = r.evalWith;
|
|
428
544
|
} catch (e) {
|
|
429
|
-
//
|
|
430
|
-
//
|
|
545
|
+
// Safe to re-forward the SAME token below: a failed step is
|
|
546
|
+
// transactional — segmented mode stages ring adopts and rolls
|
|
547
|
+
// back committed js-layer writes on throw (see #stepSegmented),
|
|
548
|
+
// and buffer growth done by a partial prepare is benign for the
|
|
549
|
+
// uncompiled path (updateAndFetch re-checks capacity).
|
|
431
550
|
compiled = null;
|
|
432
551
|
console.warn(`compiled decode disabled for this generation: ${e}`);
|
|
433
552
|
}
|
|
@@ -439,9 +558,11 @@ async function* generateInner(
|
|
|
439
558
|
logits = model.logitsFromHidden(h);
|
|
440
559
|
h.dispose();
|
|
441
560
|
}
|
|
442
|
-
|
|
561
|
+
const sn = sampleStep(logits, generated + 1);
|
|
562
|
+
nextPending = sn.tok;
|
|
563
|
+
nextExtras = sn.extras;
|
|
443
564
|
logits.dispose();
|
|
444
|
-
ops.asyncEvalAll([nextPending, ...evalWith]);
|
|
565
|
+
ops.asyncEvalAll([nextPending, ...extrasArrays(nextExtras), ...evalWith]);
|
|
445
566
|
}
|
|
446
567
|
|
|
447
568
|
// sync-read step n's token while n+1 computes
|
|
@@ -460,11 +581,17 @@ async function* generateInner(
|
|
|
460
581
|
if (nextPending !== null) forwarded.push(token);
|
|
461
582
|
|
|
462
583
|
if (eosTokenIds.includes(token)) {
|
|
584
|
+
disposeExtras(curExtras);
|
|
463
585
|
nextPending?.dispose();
|
|
464
586
|
nextPending = null;
|
|
587
|
+
disposeExtras(nextExtras);
|
|
588
|
+
nextExtras = null;
|
|
465
589
|
stop = true;
|
|
466
590
|
} else {
|
|
467
|
-
|
|
591
|
+
// readExtras before the yield: if the consumer breaks at this yield,
|
|
592
|
+
// the extras are already read and disposed.
|
|
593
|
+
const logprobs = readExtras(curExtras);
|
|
594
|
+
yield { token, index: generated - 1, ...(logprobs ? { logprobs } : {}) };
|
|
468
595
|
// mlx-lm generate_step: clear_cache after token 0 (drops the
|
|
469
596
|
// remaining prefill transients) and every 256 tokens after
|
|
470
597
|
if ((generated - 1) % 256 === 0) clearCache();
|
|
@@ -473,6 +600,8 @@ async function* generateInner(
|
|
|
473
600
|
} else {
|
|
474
601
|
pending = nextPending;
|
|
475
602
|
nextPending = null;
|
|
603
|
+
pendingExtras = nextExtras;
|
|
604
|
+
nextExtras = null;
|
|
476
605
|
}
|
|
477
606
|
}
|
|
478
607
|
}
|
|
@@ -486,6 +615,8 @@ async function* generateInner(
|
|
|
486
615
|
if (!finished) {
|
|
487
616
|
pending?.dispose();
|
|
488
617
|
nextPending?.dispose();
|
|
618
|
+
disposeExtras(pendingExtras);
|
|
619
|
+
disposeExtras(nextExtras);
|
|
489
620
|
}
|
|
490
621
|
if (ownsCache) for (const c of cache) c.dispose();
|
|
491
622
|
history?.dispose();
|
package/src/harness-pi.ts
CHANGED
|
@@ -22,7 +22,10 @@ export const PI_PROVIDER_ID = "mlx-bun";
|
|
|
22
22
|
* server still reports the true id in /v1/models and responses. Gives a
|
|
23
23
|
* clean `mlx-bun/local` handle instead of `mlx-bun/mlx-community/…`. */
|
|
24
24
|
export const PI_LOCAL_MODEL_ID = "local";
|
|
25
|
-
|
|
25
|
+
// 127.0.0.1 (the server's actual default bind), not "localhost": the generated
|
|
26
|
+
// extension runs under the USER'S pi (node/jiti, not Bun), so don't depend on
|
|
27
|
+
// how that runtime's resolver maps localhost.
|
|
28
|
+
export const DEFAULT_BASE_URL = "http://127.0.0.1:8080/v1";
|
|
26
29
|
|
|
27
30
|
export function defaultPiExtensionsDir(): string {
|
|
28
31
|
return join(homedir(), ".pi", "agent", "extensions");
|
|
@@ -63,22 +66,34 @@ export interface ServerModel {
|
|
|
63
66
|
contextWindow: number;
|
|
64
67
|
maxTokens: number;
|
|
65
68
|
reasoning: boolean;
|
|
69
|
+
vision: boolean;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
/** Discover
|
|
72
|
+
/** Discover the SERVED model from a running mlx-bun server; [] when
|
|
73
|
+
* unreachable. /v1/models lists the served model first (the only row
|
|
74
|
+
* carrying the capability extras: context_window/reasoning/vision) and then
|
|
75
|
+
* every other servable model in the registry — those are things `mlx-bun
|
|
76
|
+
* serve` COULD run, not models this server answers for, so they must not be
|
|
77
|
+
* registered with pi. */
|
|
69
78
|
export async function fetchServerModels(baseUrl: string, timeoutMs = 3000): Promise<ServerModel[]> {
|
|
70
79
|
try {
|
|
71
80
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
|
|
72
81
|
signal: AbortSignal.timeout(timeoutMs),
|
|
73
82
|
});
|
|
74
83
|
if (!res.ok) return [];
|
|
75
|
-
const body = (await res.json()) as {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
const body = (await res.json()) as {
|
|
85
|
+
data?: Array<{ id: string; context_window?: number; reasoning?: boolean; vision?: boolean }>;
|
|
86
|
+
};
|
|
87
|
+
const rows = body.data ?? [];
|
|
88
|
+
const served = rows.find((m) => m.context_window !== undefined) ?? rows[0];
|
|
89
|
+
if (!served) return [];
|
|
90
|
+
return [{
|
|
91
|
+
id: served.id,
|
|
92
|
+
contextWindow: served.context_window ?? 32768,
|
|
79
93
|
maxTokens: 8192,
|
|
80
|
-
reasoning: !!
|
|
81
|
-
|
|
94
|
+
reasoning: !!served.reasoning,
|
|
95
|
+
vision: !!served.vision,
|
|
96
|
+
}];
|
|
82
97
|
} catch {
|
|
83
98
|
return [];
|
|
84
99
|
}
|
|
@@ -113,16 +128,22 @@ export default async function (pi: any) {
|
|
|
113
128
|
if (res.ok) {
|
|
114
129
|
const { data } = await res.json();
|
|
115
130
|
if (Array.isArray(data) && data.length > 0) {
|
|
116
|
-
|
|
131
|
+
// The SERVED model is the row with the capability extras (first);
|
|
132
|
+
// the rest are servable-but-not-served registry entries.
|
|
133
|
+
const served = data.find((m: any) => m.context_window !== undefined) ?? data[0];
|
|
134
|
+
models = [{
|
|
117
135
|
id: ${JSON.stringify(PI_LOCAL_MODEL_ID)},
|
|
118
|
-
name: \`\${
|
|
119
|
-
reasoning: !!
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
name: \`\${served.id} (mlx-bun local)\`,
|
|
137
|
+
reasoning: !!served.reasoning,
|
|
138
|
+
compat: {
|
|
139
|
+
supportsDeveloperRole: false,
|
|
140
|
+
...(served.reasoning ? { thinkingFormat: "qwen-chat-template" } : {}),
|
|
141
|
+
},
|
|
142
|
+
input: served.vision ? ["text", "image"] : ["text"],
|
|
122
143
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
123
|
-
contextWindow:
|
|
144
|
+
contextWindow: served.context_window ?? 32768,
|
|
124
145
|
maxTokens: 8192,
|
|
125
|
-
}
|
|
146
|
+
}];
|
|
126
147
|
}
|
|
127
148
|
}
|
|
128
149
|
} catch {}
|
|
@@ -142,8 +163,13 @@ function modelEntry(m: ServerModel) {
|
|
|
142
163
|
id: PI_LOCAL_MODEL_ID,
|
|
143
164
|
name: `${m.id} (mlx-bun local)`,
|
|
144
165
|
reasoning: !!m.reasoning,
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
// Kept aligned with buildPiProvider (src/pi-provider.ts): the system
|
|
167
|
+
// prompt must arrive as `system`, not `developer`.
|
|
168
|
+
compat: {
|
|
169
|
+
supportsDeveloperRole: false,
|
|
170
|
+
...(m.reasoning ? { thinkingFormat: "qwen-chat-template" } : {}),
|
|
171
|
+
},
|
|
172
|
+
input: m.vision ? ["text", "image"] : ["text"],
|
|
147
173
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
148
174
|
contextWindow: m.contextWindow,
|
|
149
175
|
maxTokens: m.maxTokens,
|
package/src/index.ts
CHANGED
|
@@ -13,8 +13,10 @@ export { Weights } from "./weights";
|
|
|
13
13
|
export { Gemma4Model } from "./model/gemma4";
|
|
14
14
|
export { MiniCPM5Model } from "./model/minicpm5";
|
|
15
15
|
export { createModel, type RuntimeModel } from "./model/factory";
|
|
16
|
+
export { Qwen3Model } from "./model/qwen3";
|
|
16
17
|
export { generate } from "./generate";
|
|
17
18
|
export { loadTokenizer } from "./tokenizer";
|
|
19
|
+
export { embedOne, embedMany, isEmbeddingModel, withInstruction, type EmbedResult } from "./embed";
|
|
18
20
|
export { ChatTemplate } from "./chat-template";
|
|
19
21
|
export { chooseAutoModel, COEXIST_FRACTION, DEFAULT_REPO_ID, fit, largestRecommendedRepoId, recommendedRepoId, skuMatrix, thisMachine } from "./fit";
|
|
20
22
|
export { downloadModel } from "./download";
|