mlx-bun 0.0.8 → 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/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 +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 +141 -18
- 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 +3 -0
- package/src/mlx/ops.ts +46 -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 +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 +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/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 +86 -18
- package/src/server.ts +612 -49
- 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/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/web/skills/memory/SKILL.md +40 -17
package/src/registry.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { Database } from "bun:sqlite";
|
|
8
8
|
import {
|
|
9
|
-
closeSync, existsSync, openSync, readdirSync, readFileSync,
|
|
9
|
+
closeSync, existsSync, lstatSync, openSync, readdirSync, readFileSync,
|
|
10
|
+
readlinkSync, readSync, rmSync, statSync,
|
|
10
11
|
} from "node:fs";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { isDrafterModelType } from "./model/support";
|
|
@@ -27,6 +28,12 @@ export interface ModelRecord {
|
|
|
27
28
|
quantGroupSize: number | null;
|
|
28
29
|
quantMode: string | null;
|
|
29
30
|
hasVisionSidecar: boolean;
|
|
31
|
+
/** config.json's vision_config.model_type when it names a vision tower
|
|
32
|
+
* (e.g. "gemma4_vision" — SigLIP, needs the bf16 sidecar — or
|
|
33
|
+
* "gemma4_unified_vision" — encoder-free). null for text-only models
|
|
34
|
+
* AND for configs whose nested vision_config is not a vision tower
|
|
35
|
+
* (Qwen3.5 nests a copy of its own text config there). */
|
|
36
|
+
visionConfigType: string | null;
|
|
30
37
|
hasKvConfig: boolean;
|
|
31
38
|
hasToolTemplate: boolean;
|
|
32
39
|
numLayers: number | null;
|
|
@@ -52,6 +59,7 @@ CREATE TABLE IF NOT EXISTS models (
|
|
|
52
59
|
quant_group_size INTEGER,
|
|
53
60
|
quant_mode TEXT,
|
|
54
61
|
has_vision_sidecar INTEGER NOT NULL,
|
|
62
|
+
vision_config_type TEXT,
|
|
55
63
|
has_kv_config INTEGER NOT NULL,
|
|
56
64
|
has_tool_template INTEGER NOT NULL,
|
|
57
65
|
num_layers INTEGER,
|
|
@@ -62,7 +70,14 @@ CREATE TABLE IF NOT EXISTS models (
|
|
|
62
70
|
);
|
|
63
71
|
`;
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
/** HF hub cache root, honoring the standard env overrides the same way
|
|
74
|
+
* huggingface_hub (and our server.ts) do: HF_HUB_CACHE > HF_HOME/hub >
|
|
75
|
+
* ~/.cache/huggingface/hub. */
|
|
76
|
+
export const DEFAULT_HUB =
|
|
77
|
+
process.env.HF_HUB_CACHE ??
|
|
78
|
+
(process.env.HF_HOME
|
|
79
|
+
? join(process.env.HF_HOME, "hub")
|
|
80
|
+
: `${process.env.HOME}/.cache/huggingface/hub`);
|
|
66
81
|
export const DEFAULT_DB = `${process.env.HOME}/.cache/mlx-bun/registry.sqlite`;
|
|
67
82
|
|
|
68
83
|
export class Registry {
|
|
@@ -81,7 +96,8 @@ export class Registry {
|
|
|
81
96
|
if (
|
|
82
97
|
!cols.includes("sidecar_bytes") ||
|
|
83
98
|
!cols.includes("experts_bytes") ||
|
|
84
|
-
!cols.includes("license")
|
|
99
|
+
!cols.includes("license") ||
|
|
100
|
+
!cols.includes("vision_config_type")
|
|
85
101
|
) {
|
|
86
102
|
this.db.exec("DROP TABLE models");
|
|
87
103
|
this.db.exec(SCHEMA);
|
|
@@ -99,7 +115,7 @@ export class Registry {
|
|
|
99
115
|
const upsert = this.db.prepare(`
|
|
100
116
|
INSERT OR REPLACE INTO models VALUES
|
|
101
117
|
($path, $repo, $type, $params, $size, $sidecar, $experts, $bits, $gs, $mode,
|
|
102
|
-
$vision, $kv, $tools, $layers, $hidden, $vocab, $license, $at)
|
|
118
|
+
$vision, $vtype, $kv, $tools, $layers, $hidden, $vocab, $license, $at)
|
|
103
119
|
`);
|
|
104
120
|
for (const entry of readdirSync(hubDir)) {
|
|
105
121
|
if (!entry.startsWith("models--")) continue;
|
|
@@ -116,6 +132,7 @@ export class Registry {
|
|
|
116
132
|
$sidecar: rec.sidecarBytes, $experts: rec.expertsBytes,
|
|
117
133
|
$bits: rec.quantBits, $gs: rec.quantGroupSize, $mode: rec.quantMode,
|
|
118
134
|
$vision: rec.hasVisionSidecar ? 1 : 0,
|
|
135
|
+
$vtype: rec.visionConfigType,
|
|
119
136
|
$kv: rec.hasKvConfig ? 1 : 0,
|
|
120
137
|
$tools: rec.hasToolTemplate ? 1 : 0,
|
|
121
138
|
$layers: rec.numLayers, $hidden: rec.hiddenSize, $vocab: rec.vocabSize,
|
|
@@ -131,8 +148,12 @@ export class Registry {
|
|
|
131
148
|
const clauses: string[] = [];
|
|
132
149
|
const params: Record<string, unknown> = {};
|
|
133
150
|
if (filter.vision !== undefined) {
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
// Vision-capable = SigLIP sidecar present OR the encoder-free unified
|
|
152
|
+
// tower declared in config.json (12B ships no sidecar upstream anymore).
|
|
153
|
+
// COALESCE: NULL vision_config_type must read as "no", not SQL-NULL
|
|
154
|
+
// (a bare NULL LIKE poisons the NOT branch of the filter).
|
|
155
|
+
const cap = "(has_vision_sidecar = 1 OR COALESCE(vision_config_type,'') LIKE '%unified_vision')";
|
|
156
|
+
clauses.push(filter.vision ? cap : `NOT ${cap}`);
|
|
136
157
|
}
|
|
137
158
|
if (filter.maxBytes !== undefined) {
|
|
138
159
|
clauses.push("size_bytes <= $max");
|
|
@@ -152,6 +173,24 @@ export class Registry {
|
|
|
152
173
|
return rows.map(rowToRecord).filter((r) => existsSync(r.path));
|
|
153
174
|
}
|
|
154
175
|
|
|
176
|
+
/** list(), collapsed to ONE record per repo: the canonical snapshot
|
|
177
|
+
* (refs/main, else most recently scanned). Upstream pushes create a new
|
|
178
|
+
* snapshots/<commit> dir per revision and the old ones are never deleted,
|
|
179
|
+
* so raw list() shows a row per revision — this is the user-facing view
|
|
180
|
+
* (`ls`, /v1/models, /library). Order follows list() (size ascending). */
|
|
181
|
+
listCanonical(filter: Parameters<Registry["list"]>[0] = {}): ModelRecord[] {
|
|
182
|
+
const byRepo = new Map<string, ModelRecord[]>();
|
|
183
|
+
const order: string[] = [];
|
|
184
|
+
for (const m of this.list(filter)) {
|
|
185
|
+
if (!byRepo.has(m.repoId)) {
|
|
186
|
+
byRepo.set(m.repoId, []);
|
|
187
|
+
order.push(m.repoId);
|
|
188
|
+
}
|
|
189
|
+
byRepo.get(m.repoId)!.push(m);
|
|
190
|
+
}
|
|
191
|
+
return order.map((repo) => pickCanonicalRevision(byRepo.get(repo)!));
|
|
192
|
+
}
|
|
193
|
+
|
|
155
194
|
/** Resolve a fuzzy query to exactly one model (error listing candidates otherwise).
|
|
156
195
|
* Speculative-decoding drafters are companion artifacts, never selectable on
|
|
157
196
|
* their own, so they never count as candidates here. */
|
|
@@ -172,10 +211,17 @@ export class Registry {
|
|
|
172
211
|
}
|
|
173
212
|
}
|
|
174
213
|
|
|
214
|
+
/** True when the model can answer vision requests: it either ships the bf16
|
|
215
|
+
* SigLIP sidecar (gemma4_vision: e2b/e4b/26B/31B) or declares the
|
|
216
|
+
* encoder-free unified tower in config.json (gemma4_unified_vision: 12B). */
|
|
217
|
+
export function visionCapable(m: Pick<ModelRecord, "hasVisionSidecar" | "visionConfigType">): boolean {
|
|
218
|
+
return m.hasVisionSidecar || (m.visionConfigType?.endsWith("unified_vision") ?? false);
|
|
219
|
+
}
|
|
220
|
+
|
|
175
221
|
/** Of several cached revisions of ONE repo, pick the canonical snapshot: the
|
|
176
222
|
* revision refs/main points at, else the most recently scanned (stable
|
|
177
223
|
* tie-break on path). Paths are `<hub>/models--<repo>/snapshots/<hash>`. */
|
|
178
|
-
function pickCanonicalRevision(matches: ModelRecord[]): ModelRecord {
|
|
224
|
+
export function pickCanonicalRevision(matches: ModelRecord[]): ModelRecord {
|
|
179
225
|
if (matches.length === 1) return matches[0]!;
|
|
180
226
|
const root = matches[0]!.path.split("/snapshots/")[0]!;
|
|
181
227
|
try {
|
|
@@ -203,6 +249,7 @@ function rowToRecord(r: Record<string, unknown>): ModelRecord {
|
|
|
203
249
|
quantGroupSize: r.quant_group_size as number | null,
|
|
204
250
|
quantMode: r.quant_mode as string | null,
|
|
205
251
|
hasVisionSidecar: !!r.has_vision_sidecar,
|
|
252
|
+
visionConfigType: (r.vision_config_type as string | null) ?? null,
|
|
206
253
|
hasKvConfig: !!r.has_kv_config,
|
|
207
254
|
hasToolTemplate: !!r.has_tool_template,
|
|
208
255
|
numLayers: r.num_layers as number | null,
|
|
@@ -258,6 +305,166 @@ function expertTensorBytes(path: string): number {
|
|
|
258
305
|
}
|
|
259
306
|
}
|
|
260
307
|
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// gc: reclaim superseded snapshots + dead blobs.
|
|
310
|
+
//
|
|
311
|
+
// The HF cache layout keeps one snapshots/<commit> dir per downloaded
|
|
312
|
+
// revision and NEVER deletes old ones — every upstream push a `get` follows
|
|
313
|
+
// strands the previous snapshot (and any blobs only it references). gc keeps
|
|
314
|
+
// the snapshots refs/* point at, deletes the rest, then deletes blobs no
|
|
315
|
+
// surviving snapshot symlink targets.
|
|
316
|
+
|
|
317
|
+
export interface GcSkippedSnapshot {
|
|
318
|
+
path: string;
|
|
319
|
+
/** Files this snapshot has that NO kept snapshot has (relative names).
|
|
320
|
+
* Deleting it would delete the only copy — needs an explicit --force. */
|
|
321
|
+
extraFiles: string[];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export interface GcRepoPlan {
|
|
325
|
+
repoId: string;
|
|
326
|
+
repoDir: string;
|
|
327
|
+
/** Snapshots referenced by refs/* — never deleted. */
|
|
328
|
+
keepSnapshots: string[];
|
|
329
|
+
/** Unreferenced snapshots safe to delete (file set ⊆ kept snapshots'). */
|
|
330
|
+
pruneSnapshots: string[];
|
|
331
|
+
/** Unreferenced snapshots with files the kept set lacks — skipped unless
|
|
332
|
+
* force (their blobs count as live). */
|
|
333
|
+
skippedSnapshots: GcSkippedSnapshot[];
|
|
334
|
+
/** blobs/ entries no surviving snapshot links to. */
|
|
335
|
+
deadBlobs: string[];
|
|
336
|
+
reclaimBytes: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Relative paths of all files/symlinks under a snapshot dir. */
|
|
340
|
+
function snapshotFiles(snapDir: string): string[] {
|
|
341
|
+
const out: string[] = [];
|
|
342
|
+
const walk = (dir: string, prefix: string) => {
|
|
343
|
+
for (const name of readdirSync(dir)) {
|
|
344
|
+
const p = join(dir, name);
|
|
345
|
+
const st = lstatSync(p);
|
|
346
|
+
if (st.isDirectory()) walk(p, `${prefix}${name}/`);
|
|
347
|
+
else out.push(`${prefix}${name}`);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
try { walk(snapDir, ""); } catch {}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Blob basenames a snapshot's symlinks resolve to (within this repo). */
|
|
355
|
+
function snapshotBlobTargets(snapDir: string): Set<string> {
|
|
356
|
+
const targets = new Set<string>();
|
|
357
|
+
for (const rel of snapshotFiles(snapDir)) {
|
|
358
|
+
const p = join(snapDir, rel);
|
|
359
|
+
try {
|
|
360
|
+
const t = readlinkSync(p); // throws for regular files — those aren't blobs
|
|
361
|
+
const base = t.split("/").pop();
|
|
362
|
+
if (base && t.includes("blobs")) targets.add(base);
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
365
|
+
return targets;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Plan gc for one models--* repo dir. Conservative by construction:
|
|
369
|
+
* - no refs/ (or refs pointing at missing snapshots) → nothing is pruned;
|
|
370
|
+
* - an unreferenced snapshot carrying files the kept set lacks is only
|
|
371
|
+
* prunable under `force` (the live example: a stale snapshot whose
|
|
372
|
+
* optiq_vision.safetensors the canonical revision dropped);
|
|
373
|
+
* - blobs are dead only when NO surviving snapshot (kept or skipped)
|
|
374
|
+
* links to them. `.incomplete`/`.lock` resume artifacts are never touched. */
|
|
375
|
+
export function planRepoGc(repoDir: string, opts: { force?: boolean } = {}): GcRepoPlan {
|
|
376
|
+
const entry = repoDir.split("/").pop() ?? "";
|
|
377
|
+
const repoId = entry.slice("models--".length).replaceAll("--", "/");
|
|
378
|
+
const plan: GcRepoPlan = {
|
|
379
|
+
repoId, repoDir,
|
|
380
|
+
keepSnapshots: [], pruneSnapshots: [], skippedSnapshots: [],
|
|
381
|
+
deadBlobs: [], reclaimBytes: 0,
|
|
382
|
+
};
|
|
383
|
+
const snapsDir = join(repoDir, "snapshots");
|
|
384
|
+
const refsDir = join(repoDir, "refs");
|
|
385
|
+
if (!existsSync(snapsDir)) return plan;
|
|
386
|
+
|
|
387
|
+
const refCommits = new Set<string>();
|
|
388
|
+
if (existsSync(refsDir)) {
|
|
389
|
+
for (const r of readdirSync(refsDir)) {
|
|
390
|
+
try { refCommits.add(readFileSync(join(refsDir, r), "utf8").trim()); } catch {}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const candidates: string[] = [];
|
|
395
|
+
for (const snap of readdirSync(snapsDir)) {
|
|
396
|
+
const p = join(snapsDir, snap);
|
|
397
|
+
if (!lstatSync(p).isDirectory()) continue;
|
|
398
|
+
(refCommits.has(snap) ? plan.keepSnapshots : candidates).push(p);
|
|
399
|
+
}
|
|
400
|
+
// No ref resolves to an existing snapshot → we cannot tell what's live.
|
|
401
|
+
if (plan.keepSnapshots.length === 0) return plan;
|
|
402
|
+
|
|
403
|
+
const keptFiles = new Set<string>();
|
|
404
|
+
for (const k of plan.keepSnapshots) for (const f of snapshotFiles(k)) keptFiles.add(f);
|
|
405
|
+
for (const c of candidates) {
|
|
406
|
+
const extra = snapshotFiles(c).filter((f) => !keptFiles.has(f));
|
|
407
|
+
if (extra.length > 0 && !opts.force) plan.skippedSnapshots.push({ path: c, extraFiles: extra });
|
|
408
|
+
else plan.pruneSnapshots.push(c);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const live = new Set<string>();
|
|
412
|
+
for (const s of [...plan.keepSnapshots, ...plan.skippedSnapshots.map((x) => x.path)])
|
|
413
|
+
for (const b of snapshotBlobTargets(s)) live.add(b);
|
|
414
|
+
const blobsDir = join(repoDir, "blobs");
|
|
415
|
+
if (existsSync(blobsDir)) {
|
|
416
|
+
for (const b of readdirSync(blobsDir)) {
|
|
417
|
+
if (b.endsWith(".incomplete") || b.endsWith(".lock")) continue;
|
|
418
|
+
if (live.has(b)) continue;
|
|
419
|
+
const p = join(blobsDir, b);
|
|
420
|
+
plan.deadBlobs.push(p);
|
|
421
|
+
try { plan.reclaimBytes += lstatSync(p).size; } catch {}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return plan;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function planGc(hubDir: string = DEFAULT_HUB, opts: { force?: boolean } = {}): GcRepoPlan[] {
|
|
428
|
+
if (!existsSync(hubDir)) return [];
|
|
429
|
+
const plans: GcRepoPlan[] = [];
|
|
430
|
+
for (const entry of readdirSync(hubDir)) {
|
|
431
|
+
if (!entry.startsWith("models--")) continue;
|
|
432
|
+
plans.push(planRepoGc(join(hubDir, entry), opts));
|
|
433
|
+
}
|
|
434
|
+
return plans;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Execute a plan: snapshot dirs first, then dead blobs. Returns totals. */
|
|
438
|
+
export function executeGc(plans: GcRepoPlan[]): {
|
|
439
|
+
snapshots: number; blobs: number; reclaimedBytes: number;
|
|
440
|
+
} {
|
|
441
|
+
let snapshots = 0, blobs = 0, reclaimedBytes = 0;
|
|
442
|
+
for (const p of plans) {
|
|
443
|
+
for (const s of p.pruneSnapshots) {
|
|
444
|
+
rmSync(s, { recursive: true, force: true });
|
|
445
|
+
snapshots++;
|
|
446
|
+
}
|
|
447
|
+
for (const b of p.deadBlobs) {
|
|
448
|
+
try {
|
|
449
|
+
const size = lstatSync(b).size;
|
|
450
|
+
rmSync(b, { force: true });
|
|
451
|
+
blobs++;
|
|
452
|
+
reclaimedBytes += size;
|
|
453
|
+
} catch {}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { snapshots, blobs, reclaimedBytes };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** vision_config.model_type, but only when it actually names a vision tower
|
|
460
|
+
* (`*_vision`). Some text-only configs nest a serialized copy of themselves
|
|
461
|
+
* under vision_config (Qwen3.5 puts model_type "qwen3_5" there) — presence
|
|
462
|
+
* of the key alone is NOT a vision signal. */
|
|
463
|
+
function visionConfigTypeOf(config: Record<string, any>): string | null {
|
|
464
|
+
const vt = (config.vision_config as Record<string, any> | undefined)?.model_type;
|
|
465
|
+
return typeof vt === "string" && vt.endsWith("_vision") ? vt : null;
|
|
466
|
+
}
|
|
467
|
+
|
|
261
468
|
async function scanSnapshot(dir: string, repoId: string): Promise<ModelRecord | null> {
|
|
262
469
|
const configPath = join(dir, "config.json");
|
|
263
470
|
if (!existsSync(configPath)) return null;
|
|
@@ -325,6 +532,7 @@ async function scanSnapshot(dir: string, repoId: string): Promise<ModelRecord |
|
|
|
325
532
|
quantGroupSize: quant?.group_size ?? null,
|
|
326
533
|
quantMode: quant?.mode ?? (quant ? "affine" : null),
|
|
327
534
|
hasVisionSidecar: existsSync(join(dir, "optiq_vision.safetensors")),
|
|
535
|
+
visionConfigType: visionConfigTypeOf(config),
|
|
328
536
|
hasKvConfig: existsSync(join(dir, "kv_config.json")),
|
|
329
537
|
hasToolTemplate,
|
|
330
538
|
numLayers: text.num_hidden_layers ?? null,
|
package/src/responses.ts
CHANGED
|
@@ -121,6 +121,19 @@ export interface ResponsesRequest {
|
|
|
121
121
|
top_p?: number;
|
|
122
122
|
top_k?: number;
|
|
123
123
|
stream?: boolean;
|
|
124
|
+
/** mlx-lm sampler/penalty extensions + logit_bias — not part of the
|
|
125
|
+
* Responses protocol proper; accepted as pass-through extras with the
|
|
126
|
+
* same names as our /v1 chat surface. */
|
|
127
|
+
min_p?: number;
|
|
128
|
+
xtc_probability?: number;
|
|
129
|
+
xtc_threshold?: number;
|
|
130
|
+
logit_bias?: Record<string, number>;
|
|
131
|
+
repetition_penalty?: number;
|
|
132
|
+
repetition_context_size?: number;
|
|
133
|
+
presence_penalty?: number;
|
|
134
|
+
presence_context_size?: number;
|
|
135
|
+
frequency_penalty?: number;
|
|
136
|
+
frequency_context_size?: number;
|
|
124
137
|
tools?: Array<Record<string, unknown>>;
|
|
125
138
|
tool_choice?: unknown;
|
|
126
139
|
}
|
|
@@ -235,6 +248,16 @@ export function responsesToChatBody(body: ResponsesRequest): Record<string, unkn
|
|
|
235
248
|
if (body.top_p != null) oai.top_p = body.top_p;
|
|
236
249
|
if (body.top_k != null) oai.top_k = body.top_k;
|
|
237
250
|
if (body.stream) oai.stream = true;
|
|
251
|
+
// mlx-lm sampler/penalty extensions (+ OpenAI logit_bias): straight
|
|
252
|
+
// pass-through — same wire names on the chat surface.
|
|
253
|
+
for (const k of [
|
|
254
|
+
"min_p", "xtc_probability", "xtc_threshold", "logit_bias",
|
|
255
|
+
"repetition_penalty", "repetition_context_size",
|
|
256
|
+
"presence_penalty", "presence_context_size",
|
|
257
|
+
"frequency_penalty", "frequency_context_size",
|
|
258
|
+
] as const) {
|
|
259
|
+
if (body[k] != null) oai[k] = body[k];
|
|
260
|
+
}
|
|
238
261
|
|
|
239
262
|
// Flat Responses tool shape → nested chat shape; built-ins
|
|
240
263
|
// (web_search, file_search, mcp, computer_use) dropped silently.
|
package/src/sampler.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// Sampling — port of mlx-lm's sample_utils.make_sampler /
|
|
2
|
-
// make_logits_processors (temperature, top-p, top-k,
|
|
2
|
+
// make_logits_processors (temperature, top-p, top-k, min-p, XTC,
|
|
3
|
+
// repetition/presence/frequency penalties, logit bias).
|
|
3
4
|
// All filtering happens on-device; only the chosen token id crosses to JS.
|
|
4
5
|
// Seeded: each step derives a fresh key from (seed, step) so runs are
|
|
5
|
-
// reproducible without sharing global RNG state.
|
|
6
|
+
// reproducible without sharing global RNG state. (mlx-lm draws XTC's
|
|
7
|
+
// uniform and the categorical from one global stream; we split the
|
|
8
|
+
// per-step key into (xtc, categorical) subkeys instead — deterministic,
|
|
9
|
+
// and the XTC-off path is unchanged.)
|
|
6
10
|
|
|
7
11
|
import { Dtype } from "./mlx/ffi";
|
|
8
12
|
import { MlxArray } from "./mlx/array";
|
|
@@ -63,6 +67,18 @@ export interface SamplerOptions {
|
|
|
63
67
|
temperature?: number;
|
|
64
68
|
topP?: number;
|
|
65
69
|
topK?: number;
|
|
70
|
+
/** min-p: keep tokens whose probability ≥ minP · p(top token). mlx-lm's
|
|
71
|
+
* `min_p`. Applied after top-p, before XTC/top-k (reference chain order). */
|
|
72
|
+
minP?: number;
|
|
73
|
+
/** Minimum number of tokens min-p may never filter out. Default 1. */
|
|
74
|
+
minTokensToKeep?: number;
|
|
75
|
+
/** XTC ("exclude top choices"): with this probability per step, drop every
|
|
76
|
+
* token above the threshold EXCEPT the least likely of them. */
|
|
77
|
+
xtcProbability?: number;
|
|
78
|
+
/** Probability a token must exceed to be an XTC removal candidate. [0, 0.5]. */
|
|
79
|
+
xtcThreshold?: number;
|
|
80
|
+
/** Token ids XTC never removes (mlx-lm's server passes EOS + "\n"). */
|
|
81
|
+
xtcSpecialTokens?: number[];
|
|
66
82
|
seed?: number;
|
|
67
83
|
/** HLG tone-curve sampling. When enabled, replaces temperature's flat slope
|
|
68
84
|
* with the piecewise curve (temperature becomes the mid gain). Off/undefined
|
|
@@ -75,8 +91,19 @@ export interface SamplerOptions {
|
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
export interface LogitsProcessorOptions {
|
|
94
|
+
/** Additive per-token-id logit bias, applied before any penalty
|
|
95
|
+
* (mlx-lm's `logit_bias`: {tokenId: bias}). */
|
|
96
|
+
logitBias?: Record<number, number>;
|
|
78
97
|
repetitionPenalty?: number;
|
|
79
98
|
repetitionContextSize?: number;
|
|
99
|
+
/** OpenAI-style presence penalty: subtracted ONCE from a token's logit if it
|
|
100
|
+
* occurred at all in the recent window. May be negative. */
|
|
101
|
+
presencePenalty?: number;
|
|
102
|
+
presenceContextSize?: number;
|
|
103
|
+
/** OpenAI-style frequency penalty: subtracted once PER OCCURRENCE of the
|
|
104
|
+
* token in the recent window. May be negative. */
|
|
105
|
+
frequencyPenalty?: number;
|
|
106
|
+
frequencyContextSize?: number;
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
/** logprobs [1, V] → sampled token array (uint32, shape [1]). */
|
|
@@ -98,7 +125,7 @@ function negInfLike(a: MlxArray): MlxArray {
|
|
|
98
125
|
|
|
99
126
|
/** apply_top_p: keep the smallest set of tokens whose cumulative
|
|
100
127
|
* probability exceeds top_p; others → -inf. */
|
|
101
|
-
function applyTopP(lp: MlxArray, topP: number): MlxArray {
|
|
128
|
+
export function applyTopP(lp: MlxArray, topP: number): MlxArray {
|
|
102
129
|
const probs = ops.exp(lp);
|
|
103
130
|
const sortedIdx = ops.argsortAxis(lp, -1);
|
|
104
131
|
const sortedProbs = ops.takeAlongAxis(probs, sortedIdx, -1);
|
|
@@ -120,7 +147,7 @@ function applyTopP(lp: MlxArray, topP: number): MlxArray {
|
|
|
120
147
|
}
|
|
121
148
|
|
|
122
149
|
/** apply_top_k: all but the k highest logprobs → -inf. */
|
|
123
|
-
function applyTopK(lp: MlxArray, topK: number): MlxArray {
|
|
150
|
+
export function applyTopK(lp: MlxArray, topK: number): MlxArray {
|
|
124
151
|
const V = lp.shape[lp.shape.length - 1]!;
|
|
125
152
|
const negLp = ops.neg(lp);
|
|
126
153
|
const part = ops.argpartitionAxis(negLp, topK - 1, -1);
|
|
@@ -131,6 +158,88 @@ function applyTopK(lp: MlxArray, topK: number): MlxArray {
|
|
|
131
158
|
return out;
|
|
132
159
|
}
|
|
133
160
|
|
|
161
|
+
/** apply_min_p: mask tokens whose probability is below minP · p(top token).
|
|
162
|
+
* Faithful to the reference (mlx-lm 0.31.3 sample_utils.apply_min_p): the
|
|
163
|
+
* threshold is computed in log space directly against the max logprob —
|
|
164
|
+
* `scaled_min_p = max(lp) + log(minP)` — no full sort; min_tokens_to_keep
|
|
165
|
+
* survivors are recovered with an argpartition on the top tail. */
|
|
166
|
+
export function applyMinP(lp: MlxArray, minP: number, minTokensToKeep = 1): MlxArray {
|
|
167
|
+
if (!(minP >= 0 && minP <= 1))
|
|
168
|
+
throw new Error(`minP has to be a float in the [0, 1] interval, but is ${minP}`);
|
|
169
|
+
if (!Number.isInteger(minTokensToKeep) || minTokensToKeep < 1)
|
|
170
|
+
throw new Error(`minTokensToKeep has to be a positive integer, but is ${minTokensToKeep}`);
|
|
171
|
+
|
|
172
|
+
const owned: MlxArray[] = [];
|
|
173
|
+
const k = <T extends MlxArray>(a: T): T => { owned.push(a); return a; };
|
|
174
|
+
|
|
175
|
+
// Mask tokens that have a probability less than max(p) * min_p
|
|
176
|
+
const top = k(ops.maxAxis(lp, -1, true));
|
|
177
|
+
const scaledMinP = k(ops.add(top, k(ops.scalarLike(Math.log(minP), top))));
|
|
178
|
+
let remove = k(ops.less(lp, scaledMinP));
|
|
179
|
+
|
|
180
|
+
// Ensure at least min_tokens_to_keep survive the filter
|
|
181
|
+
if (minTokensToKeep > 1) {
|
|
182
|
+
const V = lp.shape[lp.shape.length - 1]!;
|
|
183
|
+
// mx.argpartition(lp, kth=-min_tokens_to_keep): negative kth ≡ V - k
|
|
184
|
+
const part = k(ops.argpartitionAxis(lp, V - minTokensToKeep, -1));
|
|
185
|
+
const topIdx = k(part.slice([0, V - minTokensToKeep], [1, V]));
|
|
186
|
+
const falses = k(ops.zeros([1, minTokensToKeep], Dtype.bool));
|
|
187
|
+
remove = k(ops.putAlongAxis(remove, topIdx, falses, -1));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const ninf = k(negInfLike(lp));
|
|
191
|
+
const out = ops.where(remove, ninf, lp);
|
|
192
|
+
for (const a of owned) a.dispose();
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** apply_xtc: with probability xtcProbability, remove every token whose
|
|
197
|
+
* probability exceeds the minimum above-threshold probability — i.e. drop
|
|
198
|
+
* the top choices, keeping the least likely token that still clears the
|
|
199
|
+
* threshold (plus everything below it). Special tokens are never removed.
|
|
200
|
+
* `key` seeds the per-step uniform draw (the reference uses the global RNG). */
|
|
201
|
+
export function applyXtc(
|
|
202
|
+
logits: MlxArray,
|
|
203
|
+
xtcProbability: number,
|
|
204
|
+
xtcThreshold: number,
|
|
205
|
+
xtcSpecialTokens: number[],
|
|
206
|
+
key: MlxArray | null,
|
|
207
|
+
): MlxArray {
|
|
208
|
+
if (!(xtcThreshold >= 0 && xtcThreshold <= 0.5))
|
|
209
|
+
throw new Error(`xtcThreshold has to be a float in the [0, 0.5] interval, but is ${xtcThreshold}`);
|
|
210
|
+
if (!(xtcProbability >= 0 && xtcProbability <= 1))
|
|
211
|
+
throw new Error(`xtcProbability has to be a float in the [0, 1] interval, but is ${xtcProbability}`);
|
|
212
|
+
|
|
213
|
+
const owned: MlxArray[] = [];
|
|
214
|
+
const k = <T extends MlxArray>(a: T): T => { owned.push(a); return a; };
|
|
215
|
+
|
|
216
|
+
const probs = k(ops.softmaxAxis(logits, -1, false));
|
|
217
|
+
// mask = probs > min(where(probs > threshold, probs, inf))
|
|
218
|
+
const thresh = k(ops.scalarLike(xtcThreshold, probs));
|
|
219
|
+
const above = k(ops.less(thresh, probs)); // probs > threshold
|
|
220
|
+
const inf = k(ops.scalarLike(Infinity, probs));
|
|
221
|
+
const aboveProbs = k(ops.where(above, probs, inf));
|
|
222
|
+
// min(x) = -max(-x) (no min reduction in the ops layer)
|
|
223
|
+
const minAbove = k(ops.neg(k(ops.maxAxis(k(ops.neg(aboveProbs)), -1, true))));
|
|
224
|
+
let mask = k(ops.less(minAbove, probs)); // probs > min above-threshold prob
|
|
225
|
+
if (xtcSpecialTokens.length > 0) {
|
|
226
|
+
const n = xtcSpecialTokens.length;
|
|
227
|
+
const idx = k(ops.fromInt32(xtcSpecialTokens, [1, n]));
|
|
228
|
+
const falses = k(ops.zeros([1, n], Dtype.bool));
|
|
229
|
+
mask = k(ops.putAlongAxis(mask, idx, falses, -1));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// where(uniform() > xtcProbability, logits, where(mask, -inf, logits))
|
|
233
|
+
const u = k(ops.randomUniform([1], Dtype.float32, 0, 1, key));
|
|
234
|
+
const pArr = k(ops.scalarLike(xtcProbability, u));
|
|
235
|
+
const skip = k(ops.less(pArr, u)); // u > xtcProbability → leave logits alone
|
|
236
|
+
const ninf = k(negInfLike(logits));
|
|
237
|
+
const culled = k(ops.where(mask, ninf, logits));
|
|
238
|
+
const out = ops.where(skip, logits, culled);
|
|
239
|
+
for (const a of owned) a.dispose();
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
134
243
|
/** HLG sampling parameters — a piecewise tone curve on the logprobs.
|
|
135
244
|
* See docs/design/hlg-sampling.md. Knobs are in nats (the logprob unit). */
|
|
136
245
|
export interface HlgParams {
|
|
@@ -410,7 +519,11 @@ export function applyHlgShaper(lp: MlxArray, opts: HlgShaperParams = {}): MlxArr
|
|
|
410
519
|
}
|
|
411
520
|
|
|
412
521
|
export function makeSampler(opts: SamplerOptions = {}): Sampler {
|
|
413
|
-
const {
|
|
522
|
+
const {
|
|
523
|
+
temperature = 0, topP = 0, topK = 0, minP = 0, minTokensToKeep = 1,
|
|
524
|
+
xtcProbability = 0, xtcThreshold = 0, xtcSpecialTokens = [],
|
|
525
|
+
seed = 0, hlg, curve,
|
|
526
|
+
} = opts;
|
|
414
527
|
|
|
415
528
|
// v2 curve sampler: the drawn log-prob transfer curve REPLACES temperature +
|
|
416
529
|
// softmax. Stochastic and seeded by design (the curve IS the sampling shape),
|
|
@@ -461,13 +574,29 @@ export function makeSampler(opts: SamplerOptions = {}): Sampler {
|
|
|
461
574
|
};
|
|
462
575
|
}
|
|
463
576
|
|
|
577
|
+
// Filter chain order matches mlx-lm's make_sampler: top_p → min_p → xtc →
|
|
578
|
+
// top_k, then categorical(logprobs / temperature).
|
|
464
579
|
return (lp, step) => {
|
|
465
580
|
let cur = lp;
|
|
466
581
|
const owned: MlxArray[] = [];
|
|
582
|
+
let key = stepKey(seed, step);
|
|
467
583
|
if (topP > 0 && topP < 1) { cur = applyTopP(cur, topP); owned.push(cur); }
|
|
584
|
+
if (minP !== 0) { cur = applyMinP(cur, minP, minTokensToKeep); owned.push(cur); }
|
|
585
|
+
if (xtcProbability > 0) {
|
|
586
|
+
// Split the step key: subkey 0 → XTC's uniform, subkey 1 → categorical.
|
|
587
|
+
const split = ops.randomSplitNum(key, 2);
|
|
588
|
+
const row0 = split.slice([0, 0], [1, 2]);
|
|
589
|
+
const row1 = split.slice([1, 0], [2, 2]);
|
|
590
|
+
const xtcKey = ops.reshape(row0, [2]);
|
|
591
|
+
key.dispose();
|
|
592
|
+
key = ops.reshape(row1, [2]);
|
|
593
|
+
for (const a of [split, row0, row1]) a.dispose();
|
|
594
|
+
cur = applyXtc(cur, xtcProbability, xtcThreshold, xtcSpecialTokens, xtcKey);
|
|
595
|
+
owned.push(cur);
|
|
596
|
+
xtcKey.dispose();
|
|
597
|
+
}
|
|
468
598
|
if (topK > 0) { cur = applyTopK(cur, topK); owned.push(cur); }
|
|
469
599
|
const scaled = ops.mulScalar(cur, 1 / temperature);
|
|
470
|
-
const key = stepKey(seed, step);
|
|
471
600
|
const tok = ops.randomCategorical(scaled, key);
|
|
472
601
|
scaled.dispose();
|
|
473
602
|
key.dispose();
|
|
@@ -476,18 +605,48 @@ export function makeSampler(opts: SamplerOptions = {}): Sampler {
|
|
|
476
605
|
};
|
|
477
606
|
}
|
|
478
607
|
|
|
608
|
+
/** Start of the recent-token window, matching Python's `tokens[-context_size:]`
|
|
609
|
+
* slice semantics exactly (context_size 0 ⇒ `[-0:]` ⇒ the WHOLE history). */
|
|
610
|
+
function windowStart(n: number, contextSize: number): number {
|
|
611
|
+
return contextSize === 0 ? 0 : Math.max(0, n - contextSize);
|
|
612
|
+
}
|
|
613
|
+
|
|
479
614
|
export function makeLogitsProcessors(opts: LogitsProcessorOptions = {}): LogitsProcessor[] {
|
|
480
615
|
const out: LogitsProcessor[] = [];
|
|
481
|
-
const {
|
|
616
|
+
const {
|
|
617
|
+
logitBias,
|
|
618
|
+
repetitionPenalty, repetitionContextSize = 20,
|
|
619
|
+
presencePenalty, presenceContextSize = 20,
|
|
620
|
+
frequencyPenalty, frequencyContextSize = 20,
|
|
621
|
+
} = opts;
|
|
622
|
+
|
|
623
|
+
// logit_bias first, as in mlx-lm's make_logits_processors. Reference:
|
|
624
|
+
// `logits.at[:, indices].add(values)` — dict keys are unique, so a
|
|
625
|
+
// gather → add → put round-trip is exactly equivalent to the scatter-add.
|
|
626
|
+
const biasIds = logitBias ? Object.keys(logitBias).map(Number) : [];
|
|
627
|
+
if (logitBias && biasIds.length > 0) {
|
|
628
|
+
const biasVals = Float32Array.from(biasIds.map((id) => logitBias[id]!));
|
|
629
|
+
out.push((_tokens, logits) => {
|
|
630
|
+
const K = biasIds.length;
|
|
631
|
+
const idx = ops.fromInt32(biasIds, [1, K]);
|
|
632
|
+
const vals32 = MlxArray.fromFloat32(biasVals, [1, K]);
|
|
633
|
+
const vals = vals32.astype(logits.dtype);
|
|
634
|
+
const selected = ops.takeAlongAxis(logits, idx, -1);
|
|
635
|
+
const biased = ops.add(selected, vals);
|
|
636
|
+
const updated = ops.putAlongAxis(logits, idx, biased, -1);
|
|
637
|
+
for (const a of [idx, vals32, vals, selected, biased]) a.dispose();
|
|
638
|
+
return updated;
|
|
639
|
+
});
|
|
640
|
+
}
|
|
482
641
|
|
|
483
|
-
if (repetitionPenalty && repetitionPenalty !== 0) {
|
|
642
|
+
if (repetitionPenalty !== undefined && repetitionPenalty !== 0) {
|
|
484
643
|
if (repetitionPenalty < 0)
|
|
485
644
|
throw new Error("repetitionPenalty must be non-negative");
|
|
486
645
|
out.push((tokens, logits) => {
|
|
487
646
|
if (!tokens) return logits;
|
|
488
647
|
const n = tokens.shape[0]!;
|
|
489
648
|
if (n === 0) return logits;
|
|
490
|
-
const start =
|
|
649
|
+
const start = windowStart(n, repetitionContextSize);
|
|
491
650
|
const recent = tokens.slice([start], [n]);
|
|
492
651
|
const idx = ops.reshape(recent, [1, n - start]);
|
|
493
652
|
const selected = ops.takeAlongAxis(logits, idx, -1);
|
|
@@ -503,6 +662,58 @@ export function makeLogitsProcessors(opts: LogitsProcessorOptions = {}): LogitsP
|
|
|
503
662
|
return updated;
|
|
504
663
|
});
|
|
505
664
|
}
|
|
665
|
+
|
|
666
|
+
// make_presence_penalty: `logits[:, tokens] -= penalty` — a plain
|
|
667
|
+
// fancy-index assignment, so duplicate occurrences all write the SAME
|
|
668
|
+
// value (original − penalty): the penalty applies once per distinct token.
|
|
669
|
+
if (presencePenalty !== undefined && presencePenalty !== 0) {
|
|
670
|
+
out.push((tokens, logits) => {
|
|
671
|
+
if (!tokens) return logits;
|
|
672
|
+
const n = tokens.shape[0]!;
|
|
673
|
+
if (n === 0) return logits;
|
|
674
|
+
const start = windowStart(n, presenceContextSize);
|
|
675
|
+
const recent = tokens.slice([start], [n]);
|
|
676
|
+
const idx = ops.reshape(recent, [1, n - start]);
|
|
677
|
+
const selected = ops.takeAlongAxis(logits, idx, -1);
|
|
678
|
+
const pen = ops.scalarLike(presencePenalty, selected);
|
|
679
|
+
const penalized = ops.sub(selected, pen);
|
|
680
|
+
const updated = ops.putAlongAxis(logits, idx, penalized, -1);
|
|
681
|
+
for (const a of [recent, idx, selected, pen, penalized]) a.dispose();
|
|
682
|
+
return updated;
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// make_frequency_penalty: `logits.at[:, tokens].subtract(penalty)` — a
|
|
687
|
+
// scatter-subtract that ACCUMULATES over duplicates: each token loses
|
|
688
|
+
// penalty × (occurrence count in the window). We compute the count per
|
|
689
|
+
// position ([m,m] equality matrix summed over a row — m ≤ contextSize, tiny)
|
|
690
|
+
// so every duplicate writes the identical final value and a put_along_axis
|
|
691
|
+
// reproduces the scatter-accumulate exactly.
|
|
692
|
+
if (frequencyPenalty !== undefined && frequencyPenalty !== 0) {
|
|
693
|
+
out.push((tokens, logits) => {
|
|
694
|
+
if (!tokens) return logits;
|
|
695
|
+
const n = tokens.shape[0]!;
|
|
696
|
+
if (n === 0) return logits;
|
|
697
|
+
const start = windowStart(n, frequencyContextSize);
|
|
698
|
+
const m = n - start;
|
|
699
|
+
const recent = tokens.slice([start], [n]);
|
|
700
|
+
const col = ops.reshape(recent, [m, 1]);
|
|
701
|
+
const idx = ops.reshape(recent, [1, m]);
|
|
702
|
+
const eq = ops.equal(col, idx); // [m, m]
|
|
703
|
+
const counts = ops.sumAxis(eq, -1, false); // [m] occurrences of tokens[i]
|
|
704
|
+
const countsF = counts.astype(logits.dtype);
|
|
705
|
+
const countsRow = ops.reshape(countsF, [1, m]);
|
|
706
|
+
const selected = ops.takeAlongAxis(logits, idx, -1);
|
|
707
|
+
const pen = ops.scalarLike(frequencyPenalty, selected);
|
|
708
|
+
const penTotal = ops.mul(countsRow, pen); // penalty × count
|
|
709
|
+
const penalized = ops.sub(selected, penTotal);
|
|
710
|
+
const updated = ops.putAlongAxis(logits, idx, penalized, -1);
|
|
711
|
+
for (const a of [recent, col, idx, eq, counts, countsF, countsRow, selected, pen, penTotal, penalized])
|
|
712
|
+
a.dispose();
|
|
713
|
+
return updated;
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
506
717
|
return out;
|
|
507
718
|
}
|
|
508
719
|
|