mlx-bun 0.0.12 → 0.2.0
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 +40 -24
- package/bin/mlx-bun.mjs +1 -1
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/archive/planning/release-notes-v0.0.13.md +77 -0
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/colibri-glm52-port.md +8 -1
- package/docs/design/compat-cli-surface-design.md +1 -1
- package/docs/design/decode-speed-program.md +1 -1
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +1 -1
- package/docs/design/dspark-speculative-decoding.md +53 -3
- package/docs/design/grammar-spec-batching-integration.md +1 -1
- package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
- package/docs/design/orpo-dynamic-lambda.md +1 -1
- package/docs/design/orpo-future-enhancements.md +1 -1
- package/docs/design/orpo-training.md +2 -2
- package/docs/design/parity-tier-dag.md +1 -1
- package/docs/design/repo-cleanup-plan.md +46 -4
- package/docs/design/segmented-backward-training.md +2 -2
- package/docs/design/ssd-kv-cold-tier.md +127 -0
- package/docs/design/turboquant-weights.md +467 -0
- package/docs/design/w4a16-compute-precision-spike.md +1 -1
- package/docs/design/web-chat-redesign.md +5 -5
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/curve-sampler-research-plan.md +4 -4
- package/docs/planning/memory-docs-and-dag-plan.md +1 -1
- package/docs/planning/release-notes-v0.2.0.md +49 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/cli.md +15 -4
- package/docs/reference/distribution.md +1 -0
- package/docs/reference/features-matrix.md +3 -1
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +86 -19
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/chat-template.ts +34 -1
- package/src/cli.ts +131 -36
- package/src/config.ts +10 -0
- package/src/eval/kl.ts +27 -20
- package/src/eval/runner.ts +23 -4
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/expert-trace.ts +3 -1
- package/src/fit.ts +24 -43
- package/src/flags.ts +7 -9
- package/src/generate.ts +107 -92
- package/src/grammar.ts +2 -1
- package/src/harness-pi.ts +1 -1
- package/src/index.ts +20 -0
- package/src/jobs/job-entry.ts +3 -2
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +370 -205
- package/src/media-fetch.ts +19 -3
- package/src/memory/model.ts +2 -1
- package/src/memory/vault.ts +2 -1
- package/src/memory-plan.ts +18 -0
- package/src/mlx/ffi.ts +8 -11
- package/src/mlx/ops.ts +31 -6
- package/src/model/batched-mask.ts +1 -1
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +43 -97
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +56 -105
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +3 -2
- package/src/model/factory.ts +44 -49
- package/src/model/gemma4-base.ts +163 -10
- package/src/model/generated/gemma4-12b.ts +50 -146
- package/src/model/generated/gemma4-26b.ts +32 -92
- package/src/model/generated/gemma4-e4b.ts +26 -74
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/minicpm5.ts +2 -1
- package/src/model/profile.ts +433 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5.ts +217 -25
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +13 -6
- package/src/pi-web.ts +6 -5
- package/src/prompt-cache.ts +2 -2
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +6 -0
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +102 -2
- package/src/runtime-config.ts +67 -0
- package/src/sampler.ts +272 -0
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +200 -87
- package/src/serve/completion-executor.ts +346 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +205 -0
- package/src/serve/generation-gateway.ts +116 -109
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +338 -0
- package/src/serve/model-admin-routes.ts +153 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +158 -0
- package/src/serve/static-routes.ts +101 -0
- package/src/server.ts +768 -1367
- package/src/spec/dflash-source.ts +2 -1
- package/src/spec/dspark/module-dflash.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +8 -0
- package/src/spec/qwen-mtp-source.ts +431 -0
- package/src/spec/serve-loop.ts +36 -68
- package/src/ssd-cache.ts +17 -14
- package/src/ssd-durability.ts +195 -0
- package/src/train/flash-cce.ts +12 -11
- package/src/train/forward.ts +32 -1
- package/src/train/loss.ts +2 -1
- package/src/train/segmented.ts +4 -3
- package/src/train/trainer.ts +3 -2
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +47 -1
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +513 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.js +10 -3
- package/src/web/src/chat.ts +12 -5
- package/src/weights.ts +81 -26
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
package/src/ssd-cache.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { randomUUID } from "node:crypto";
|
|
|
22
22
|
import { commonPrefixLength } from "./prompt-cache";
|
|
23
23
|
import {
|
|
24
24
|
saveKvCache, saveKvCacheAsync, loadKvCache, readKvHeader,
|
|
25
|
+
cacheHeadersTrimmable,
|
|
25
26
|
type KvSaveMeta, type KvLoadExpect, type LoadedKvCache,
|
|
26
27
|
} from "./kv-store";
|
|
27
28
|
import type { Cache } from "./model/gemma4-base";
|
|
@@ -40,19 +41,6 @@ export interface SsdIndexEntry {
|
|
|
40
41
|
trimmable: boolean;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/** Trimmability from a file header alone (mirror of the live caches'
|
|
44
|
-
* isTrimmable(): kv/qkv/turboquant always trim; rotating kinds only pre-wrap
|
|
45
|
-
* (offset < maxSize); GLM compressed kinds always trim; ssm never). Keep in
|
|
46
|
-
* sync with gemma4-base/qwen3-delta/glm52-cache. */
|
|
47
|
-
const headerTrimmable = (caches: { kind: string; offset: number; maxSize?: number }[]): boolean =>
|
|
48
|
-
caches.every((c) =>
|
|
49
|
-
c.kind === "kv" || c.kind === "qkv" || c.kind === "turboquant" ||
|
|
50
|
-
c.kind === "mla" || c.kind === "mla-dsa" || c.kind === "mtp-mla"
|
|
51
|
-
? true
|
|
52
|
-
: c.kind === "rotating" || c.kind === "rotating-qkv"
|
|
53
|
-
? c.offset < (c.maxSize ?? 0)
|
|
54
|
-
: false);
|
|
55
|
-
|
|
56
44
|
export interface SsdStoreOptions {
|
|
57
45
|
dir: string;
|
|
58
46
|
maxBytes: number;
|
|
@@ -91,6 +79,21 @@ export class SsdCacheStore {
|
|
|
91
79
|
return this.#opts.maxBytes;
|
|
92
80
|
}
|
|
93
81
|
|
|
82
|
+
/** Longest token prefix whose atomic cache file is present in the index. */
|
|
83
|
+
get longestDurablePrefixTokens(): number {
|
|
84
|
+
return this.#index.reduce((best, entry) => Math.max(best, entry.tokens.length), 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** True when the SSD index can seed every token in this prefix. A longer
|
|
88
|
+
* trimmable descendant is equivalent to an exact boundary snapshot. */
|
|
89
|
+
hasDurablePrefix(tokens: number[], ns = ""): boolean {
|
|
90
|
+
return this.#index.some((entry) => {
|
|
91
|
+
if (entry.ns !== ns || entry.tokens.length < tokens.length) return false;
|
|
92
|
+
if (commonPrefixLength(entry.tokens, tokens) !== tokens.length) return false;
|
|
93
|
+
return entry.tokens.length === tokens.length || entry.trimmable;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
94
97
|
/** Startup recovery: rebuild the in-memory index from disk. Corrupt
|
|
95
98
|
* headers and metadata mismatches are unlinked; `.tmp` orphans reaped.
|
|
96
99
|
* Only OUR fingerprint dir is touched. Returns entries indexed. */
|
|
@@ -121,7 +124,7 @@ export class SsdCacheStore {
|
|
|
121
124
|
const st = statSync(path);
|
|
122
125
|
this.#index.push({
|
|
123
126
|
path, ns: h.ns ?? "", tokens: h.tokens, bytes: st.size, mtimeMs: st.mtimeMs,
|
|
124
|
-
trimmable:
|
|
127
|
+
trimmable: cacheHeadersTrimmable(h.caches),
|
|
125
128
|
});
|
|
126
129
|
} catch {
|
|
127
130
|
try { rmSync(path, { force: true }); } catch {}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Cache } from "./model/gemma4";
|
|
2
|
+
import type { PromptCacheEntry } from "./prompt-cache";
|
|
3
|
+
import type { SpillItem, SpillQueue } from "./kv-store";
|
|
4
|
+
|
|
5
|
+
export interface DurabilityGateway {
|
|
6
|
+
readonly busy: boolean;
|
|
7
|
+
runExclusive<T>(fn: () => Promise<T>): Promise<T>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DurabilityPromptCache {
|
|
11
|
+
findExact(tokens: number[], ns?: string): PromptCacheEntry | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DurabilitySnapshotStats {
|
|
15
|
+
pendingSnapshots: number;
|
|
16
|
+
pendingSpills: number;
|
|
17
|
+
pendingSpillBytes: number;
|
|
18
|
+
droppedSpills: number;
|
|
19
|
+
failedSpills: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DurabilityFlushResult extends DurabilitySnapshotStats {
|
|
23
|
+
durable: boolean;
|
|
24
|
+
flushedSnapshots: number;
|
|
25
|
+
missingSnapshots: number;
|
|
26
|
+
elapsedMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface DirtySnapshot {
|
|
30
|
+
key: string;
|
|
31
|
+
tokens: number[];
|
|
32
|
+
ns: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type StoreOutcome = "stored" | "missing" | "failed";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Turns debounced prompt-cache puts into a durability boundary.
|
|
39
|
+
*
|
|
40
|
+
* A dirty record stays present until SpillQueue reports that its atomic store
|
|
41
|
+
* completed. Queue drops and write failures therefore remain retryable during
|
|
42
|
+
* an explicit flush or graceful shutdown.
|
|
43
|
+
*/
|
|
44
|
+
export class SsdDurabilityCoordinator {
|
|
45
|
+
readonly #dirty = new Map<string, DirtySnapshot>();
|
|
46
|
+
readonly #timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
47
|
+
readonly #attempts = new Map<string, Promise<StoreOutcome>>();
|
|
48
|
+
#flush: Promise<DurabilityFlushResult> | null = null;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
readonly gateway: DurabilityGateway,
|
|
52
|
+
readonly promptCache: DurabilityPromptCache,
|
|
53
|
+
readonly spillQueue: SpillQueue,
|
|
54
|
+
readonly cloneCaches: (caches: Cache[]) => Cache[],
|
|
55
|
+
readonly isAlreadyDurable: (tokens: number[], ns: string) => boolean = () => false,
|
|
56
|
+
readonly debounceMs = 1_000,
|
|
57
|
+
readonly busyRetryMs = 5_000,
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
schedule(tokens: number[], ns = ""): void {
|
|
61
|
+
if (tokens.length === 0) return;
|
|
62
|
+
// PromptCache can hold unrelated entries with the same namespace and
|
|
63
|
+
// length. Include the tokens so one conversation cannot cancel another
|
|
64
|
+
// conversation's pending durability record.
|
|
65
|
+
const key = `${ns.length}:${ns}:${tokens.join(",")}`;
|
|
66
|
+
const rec: DirtySnapshot = { key, tokens: [...tokens], ns };
|
|
67
|
+
this.#dirty.set(key, rec);
|
|
68
|
+
this.#arm(key, this.debounceMs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get stats(): DurabilitySnapshotStats {
|
|
72
|
+
return {
|
|
73
|
+
pendingSnapshots: this.#dirty.size,
|
|
74
|
+
pendingSpills: this.spillQueue.pendingCount,
|
|
75
|
+
pendingSpillBytes: this.spillQueue.pendingBytes,
|
|
76
|
+
droppedSpills: this.spillQueue.droppedCount,
|
|
77
|
+
failedSpills: this.spillQueue.failedCount,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Force every dirty RAM snapshot through the queue and wait for storage. */
|
|
82
|
+
flush(): Promise<DurabilityFlushResult> {
|
|
83
|
+
if (this.#flush) return this.#flush;
|
|
84
|
+
const started = performance.now();
|
|
85
|
+
this.#flush = this.#flushInner(started).finally(() => { this.#flush = null; });
|
|
86
|
+
return this.#flush;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#arm(key: string, delayMs: number): void {
|
|
90
|
+
const old = this.#timers.get(key);
|
|
91
|
+
if (old) clearTimeout(old);
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
this.#timers.delete(key);
|
|
94
|
+
void this.#attempt(key, false);
|
|
95
|
+
}, delayMs);
|
|
96
|
+
timer.unref?.();
|
|
97
|
+
this.#timers.set(key, timer);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async #attempt(key: string, force: boolean): Promise<StoreOutcome> {
|
|
101
|
+
const running = this.#attempts.get(key);
|
|
102
|
+
if (running) {
|
|
103
|
+
const outcome = await running;
|
|
104
|
+
if (!force || !this.#dirty.has(key)) return outcome;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const rec = this.#dirty.get(key);
|
|
108
|
+
if (!rec) return "stored";
|
|
109
|
+
if (!force && this.gateway.busy) {
|
|
110
|
+
this.#arm(key, this.busyRetryMs);
|
|
111
|
+
return "failed";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const task = this.#store(rec);
|
|
115
|
+
this.#attempts.set(key, task);
|
|
116
|
+
try {
|
|
117
|
+
const outcome = await task;
|
|
118
|
+
if (outcome !== "failed" && this.#dirty.get(key) === rec)
|
|
119
|
+
this.#dirty.delete(key);
|
|
120
|
+
if (outcome === "failed" && !force && this.#dirty.get(key) === rec)
|
|
121
|
+
this.#arm(key, this.busyRetryMs);
|
|
122
|
+
return outcome;
|
|
123
|
+
} finally {
|
|
124
|
+
if (this.#attempts.get(key) === task) this.#attempts.delete(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async #store(rec: DirtySnapshot): Promise<StoreOutcome> {
|
|
129
|
+
let snap: SpillItem | null = null;
|
|
130
|
+
try {
|
|
131
|
+
snap = await this.gateway.runExclusive(async () => {
|
|
132
|
+
const entry = this.promptCache.findExact(rec.tokens, rec.ns);
|
|
133
|
+
if (!entry) return null;
|
|
134
|
+
return {
|
|
135
|
+
tokens: [...entry.tokens],
|
|
136
|
+
caches: this.cloneCaches(entry.caches),
|
|
137
|
+
ns: rec.ns,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
return "failed";
|
|
142
|
+
}
|
|
143
|
+
if (!snap)
|
|
144
|
+
return this.isAlreadyDurable(rec.tokens, rec.ns) ? "stored" : "missing";
|
|
145
|
+
return await this.spillQueue.enqueue(snap) ? "stored" : "failed";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async #flushInner(started: number): Promise<DurabilityFlushResult> {
|
|
149
|
+
for (const timer of this.#timers.values()) clearTimeout(timer);
|
|
150
|
+
this.#timers.clear();
|
|
151
|
+
|
|
152
|
+
let flushedSnapshots = 0;
|
|
153
|
+
let missingSnapshots = 0;
|
|
154
|
+
const droppedBefore = this.spillQueue.droppedCount;
|
|
155
|
+
const failedBefore = this.spillQueue.failedCount;
|
|
156
|
+
|
|
157
|
+
if (this.#attempts.size > 0)
|
|
158
|
+
await Promise.allSettled([...this.#attempts.values()]);
|
|
159
|
+
await this.spillQueue.drain();
|
|
160
|
+
|
|
161
|
+
// Flush one entry at a time. This prevents the queue cap from dropping a
|
|
162
|
+
// boundary snapshot while a large final snapshot is already in flight.
|
|
163
|
+
while (this.#dirty.size > 0) {
|
|
164
|
+
const keys = [...this.#dirty.keys()];
|
|
165
|
+
let progressed = false;
|
|
166
|
+
for (const key of keys) {
|
|
167
|
+
if (!this.#dirty.has(key)) continue;
|
|
168
|
+
const outcome = await this.#attempt(key, true);
|
|
169
|
+
await this.spillQueue.drain();
|
|
170
|
+
if (outcome === "stored") {
|
|
171
|
+
flushedSnapshots++;
|
|
172
|
+
progressed = true;
|
|
173
|
+
} else if (outcome === "missing") {
|
|
174
|
+
missingSnapshots++;
|
|
175
|
+
progressed = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!progressed) break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const stats = this.stats;
|
|
182
|
+
return {
|
|
183
|
+
...stats,
|
|
184
|
+
durable:
|
|
185
|
+
stats.pendingSnapshots === 0 &&
|
|
186
|
+
stats.pendingSpills === 0 &&
|
|
187
|
+
stats.droppedSpills === droppedBefore &&
|
|
188
|
+
stats.failedSpills === failedBefore &&
|
|
189
|
+
missingSnapshots === 0,
|
|
190
|
+
flushedSnapshots,
|
|
191
|
+
missingSnapshots,
|
|
192
|
+
elapsedMs: performance.now() - started,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/train/flash-cce.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { MlxArray } from "../mlx/array";
|
|
|
37
37
|
import { Dtype } from "../mlx/ffi";
|
|
38
38
|
import * as ops from "../mlx/ops";
|
|
39
39
|
import { STEEL_QMM_HEADER } from "./steel-qmm-header";
|
|
40
|
+
import { runtimeValue } from "../runtime-config";
|
|
40
41
|
|
|
41
42
|
/** The quantized head pieces the kernel needs (packed weight + per-group affine
|
|
42
43
|
* scale/bias + bits/group_size). Mirrors HeadQuant in loss.ts. */
|
|
@@ -392,7 +393,7 @@ function fwdSgKernel(): MetalKernel {
|
|
|
392
393
|
inputNames: ["h", "wp", "scales", "biases", "targets", "shape", "capv"],
|
|
393
394
|
outputNames: ["blockMax_out", "blockSum_out", "blockTgt_out"],
|
|
394
395
|
source: FWD_SG_SOURCE,
|
|
395
|
-
header: `#include <metal_simdgroup_matrix>\n#define TG ${TG}\n#define BB ${SG_BB}\n#define BV ${SG_BV}\n#define BD ${SG_BD}\n#define BDX ${SG_BD + 4}\n#define BVX ${SG_BV + 4}\n#define CCE_SG_SKIPDQ ${
|
|
396
|
+
header: `#include <metal_simdgroup_matrix>\n#define TG ${TG}\n#define BB ${SG_BB}\n#define BV ${SG_BV}\n#define BD ${SG_BD}\n#define BDX ${SG_BD + 4}\n#define BVX ${SG_BV + 4}\n#define CCE_SG_SKIPDQ ${runtimeValue("MLX_BUN_CCE_SG_SKIPDQ") === "1" ? 1 : 0}\n`,
|
|
396
397
|
ensureRowContiguous: true,
|
|
397
398
|
});
|
|
398
399
|
}
|
|
@@ -404,7 +405,7 @@ function fwdSgKernel(): MetalKernel {
|
|
|
404
405
|
// compute-bound regime. Removing the staging redundancy (one thread per packed
|
|
405
406
|
// word; scale/bias once per group) cut it 1629→848 ms, beating the scalar
|
|
406
407
|
// kernel's 1097 ms. Fall back to scalar (MLX_BUN_CCE_SCALAR=1) when H % SG_BD != 0.
|
|
407
|
-
const USE_SIMDGROUP_FWD =
|
|
408
|
+
const USE_SIMDGROUP_FWD = runtimeValue("MLX_BUN_CCE_SCALAR") !== "1" && runtimeValue("MLX_BUN_CCE_LANE") !== "1";
|
|
408
409
|
|
|
409
410
|
// Backward: dh[t,d] = Σ_v coeff_v · dequant(W_v)[d], coeff_v = g_t·(onehot_v −
|
|
410
411
|
// softmax_v)·sech²_v, softmax_v = exp(softcap(logit_v) − lse_t). Two-phase per
|
|
@@ -549,7 +550,7 @@ function bwdKernel(daccN: number): MetalKernel {
|
|
|
549
550
|
inputNames: ["h", "wp", "scales", "biases", "targets", "lse", "gv", "shape", "capv"],
|
|
550
551
|
outputNames: ["dh"],
|
|
551
552
|
source: BWD_SOURCE,
|
|
552
|
-
header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B_BWD}\n#define DACC_N ${daccN}\n#define CCE_BWD_SKIP_P2DQ ${
|
|
553
|
+
header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B_BWD}\n#define DACC_N ${daccN}\n#define CCE_BWD_SKIP_P2DQ ${runtimeValue("MLX_BUN_CCE_BWD_SKIP_P2DQ") === "1" ? 1 : 0}\n#define CCE_BWD_SKIP_P2 ${runtimeValue("MLX_BUN_CCE_BWD_SKIP_P2") === "1" ? 1 : 0}\n`,
|
|
553
554
|
ensureRowContiguous: true,
|
|
554
555
|
atomicOutputs: true, // dh is summed across the NBLK vocab-block programs
|
|
555
556
|
});
|
|
@@ -735,7 +736,7 @@ const BWD_SG_SOURCE = String.raw`
|
|
|
735
736
|
// (The ~3.5× figure floating around older notes was the retired SG kernel, not
|
|
736
737
|
// steel.) MLX_BUN_CCE_BWD_FILTER_EPS=0 restores exact gradients — eps "0"
|
|
737
738
|
// compiles the filter OUT (#if CCE_BWD_FILTER 0), the identical pre-flip kernel.
|
|
738
|
-
const BWD_FILTER_EPS =
|
|
739
|
+
const BWD_FILTER_EPS = runtimeValue("MLX_BUN_CCE_BWD_FILTER_EPS") ?? "1e-5";
|
|
739
740
|
// blockMax vocab-block early-exit eps (skip whole cold (token-block,vocab-block) programs
|
|
740
741
|
// — attacks BOTH phases). DEFAULT ON at 1e-5 (flipped 2026-07-02). It is ~LOSSLESS when
|
|
741
742
|
// it fires (only skips blocks whose max prob < eps AND which lack the target → ~0
|
|
@@ -744,7 +745,7 @@ const BWD_FILTER_EPS = process.env.MLX_BUN_CCE_BWD_FILTER_EPS ?? "1e-5";
|
|
|
744
745
|
// at ≤0.004% dh; COMBINED with the coeff filter at 1e-5/1e-5 the backward is 1.71×
|
|
745
746
|
// (CPM5) / 3.16× (e4b) vs exact. Fires only when the caller supplies blockMax (the
|
|
746
747
|
// production head does); MLX_BUN_CCE_BWD_BLOCK_EPS=0 disables (compiled out).
|
|
747
|
-
const BWD_BLOCK_EPS =
|
|
748
|
+
const BWD_BLOCK_EPS = runtimeValue("MLX_BUN_CCE_BWD_BLOCK_EPS") ?? "1e-5";
|
|
748
749
|
const _bwdSgKernels = new Map<string, MetalKernel>();
|
|
749
750
|
function bwdSgKernel(dimTiles: number, filterEps: string, blockEps: string): MetalKernel {
|
|
750
751
|
const filterOn = Number.parseFloat(filterEps) > 0 ? 1 : 0;
|
|
@@ -1061,9 +1062,9 @@ export function flashCceForward(
|
|
|
1061
1062
|
// tiling is clean (H,V,blockV all % 32). MLX_BUN_CCE_NOSTEEL=1 falls back to the
|
|
1062
1063
|
// old kernels (simdgroup / scalar / lane).
|
|
1063
1064
|
const blockV0 = Math.ceil(V / NBLK);
|
|
1064
|
-
const useSteel =
|
|
1065
|
+
const useSteel = runtimeValue("MLX_BUN_CCE_NOSTEEL") !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0;
|
|
1065
1066
|
const useSimd = !useSteel && USE_SIMDGROUP_FWD && H % SG_BD === 0;
|
|
1066
|
-
if (!useSteel &&
|
|
1067
|
+
if (!useSteel && runtimeValue("MLX_BUN_CCE_NOSTEEL") !== "1") {
|
|
1067
1068
|
// NOTE blockV0 = ceil(V/NBLK) can violate %32 even when V%32==0 (e.g. V=50304)
|
|
1068
1069
|
const fails = [
|
|
1069
1070
|
H % 32 !== 0 ? `H%32!=0 (H=${H})` : null,
|
|
@@ -1072,7 +1073,7 @@ export function flashCceForward(
|
|
|
1072
1073
|
].filter(Boolean).join(", ");
|
|
1073
1074
|
warnSteelFallback("forward", fails, useSimd ? "simdgroup kernel" : "scalar/lane kernel");
|
|
1074
1075
|
}
|
|
1075
|
-
const useLane = !useSteel && !useSimd &&
|
|
1076
|
+
const useLane = !useSteel && !useSimd && runtimeValue("MLX_BUN_CCE_LANE") === "1" && WPR % 32 === 0;
|
|
1076
1077
|
const kernel = useSteel ? fwdSteelKernel(GS, head.bits) : useSimd ? fwdSgKernel() : useLane ? fwdLaneKernel() : fwdKernel();
|
|
1077
1078
|
const tokTile = useSteel ? STEEL_BM : useSimd ? SG_BB : BLOCK_B;
|
|
1078
1079
|
// steel + simdgroup read h as [M, H] (M on axis 0); lane + scalar read h TRANSPOSED
|
|
@@ -1178,11 +1179,11 @@ export function flashCceBackward(
|
|
|
1178
1179
|
const blockV0 = Math.ceil(V / NBLK);
|
|
1179
1180
|
// HTw == group_size for the phase-2 QuantizedBlockLoader (one group per H-tile); needs
|
|
1180
1181
|
// H % GS == 0 (always, GR*GS=H) and GS % 32 == 0 (TNv = GS/32 frags per H-tile integral).
|
|
1181
|
-
const useSteel =
|
|
1182
|
+
const useSteel = runtimeValue("MLX_BUN_CCE_BWD_NOSTEEL") !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0 && H % GS === 0 && GS % 32 === 0;
|
|
1182
1183
|
// simdgroup_matrix phase-2 GEMM is the next tier (5512→3913 ms, 1.41×) when the H
|
|
1183
1184
|
// tiling is clean; MLX_BUN_CCE_BWD_SCALAR=1 forces the float4 scalar kernel.
|
|
1184
|
-
const useSg = !useSteel &&
|
|
1185
|
-
if (!useSteel &&
|
|
1185
|
+
const useSg = !useSteel && runtimeValue("MLX_BUN_CCE_BWD_SCALAR") !== "1" && H % (SGN * 8) === 0 && H % SG_BD === 0;
|
|
1186
|
+
if (!useSteel && runtimeValue("MLX_BUN_CCE_BWD_NOSTEEL") !== "1") {
|
|
1186
1187
|
// NOTE blockV0 = ceil(V/NBLK) can violate %32 even when V%32==0 (e.g. V=50304)
|
|
1187
1188
|
const fails = [
|
|
1188
1189
|
H % 32 !== 0 ? `H%32!=0 (H=${H})` : null,
|
package/src/train/forward.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { MlxArray } from "../mlx/array";
|
|
|
20
20
|
import { Dtype } from "../mlx/ffi";
|
|
21
21
|
import * as ops from "../mlx/ops";
|
|
22
22
|
import { createCausalMask, type Cache, type Mask } from "../model/gemma4-base";
|
|
23
|
+
import { SSMCache } from "../model/qwen3-delta";
|
|
23
24
|
import type { RuntimeModel } from "../model/factory";
|
|
24
25
|
|
|
25
26
|
/** Stateless cache for the training forward. Training is always a single
|
|
@@ -57,6 +58,35 @@ export class TrainingCache implements Cache {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/** Stateless DeltaNet stand-in (same re-runnability contract as
|
|
62
|
+
* TrainingCache): state WRITES are discarded immediately, so a gradient-
|
|
63
|
+
* checkpoint recompute re-enters with the identical null-state conditions;
|
|
64
|
+
* reads always yield null (zeros inside the kernel) — a full-sequence
|
|
65
|
+
* forward from t=0, exactly the training/perplexity semantics. Before this
|
|
66
|
+
* existed, qwen3_5 routed its DeltaNet layers into plain TrainingCache and
|
|
67
|
+
* threw on the missing conv/recurrent/advance surface (the recorded
|
|
68
|
+
* "mlx-bun perplexity cannot score qwen3_5" gap). */
|
|
69
|
+
export class TrainingSSMCache implements Cache {
|
|
70
|
+
offset = 0;
|
|
71
|
+
specRound: null = null;
|
|
72
|
+
get conv(): MlxArray | null { return null; }
|
|
73
|
+
set conv(v: MlxArray | null) { v?.dispose(); }
|
|
74
|
+
get recurrent(): MlxArray | null { return null; }
|
|
75
|
+
set recurrent(v: MlxArray | null) { v?.dispose(); }
|
|
76
|
+
advance(_n: number): void { /* offset pinned at 0 */ }
|
|
77
|
+
rowOffset(_i: number): number { return 0; }
|
|
78
|
+
updateAndFetch(): [MlxArray, MlxArray] {
|
|
79
|
+
throw new Error("TrainingSSMCache: DeltaNet layers do not use the KV path");
|
|
80
|
+
}
|
|
81
|
+
makeMask(_N: number, _w: number | null): Mask {
|
|
82
|
+
return { mode: "", arr: null }; // ssm_mask is None at B=1 (SSMCache parity)
|
|
83
|
+
}
|
|
84
|
+
state(): MlxArray[] { return []; }
|
|
85
|
+
isTrimmable(): boolean { return true; }
|
|
86
|
+
trim(_n: number): void { /* stateless */ }
|
|
87
|
+
dispose(): void { /* owns no arrays */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
60
90
|
/** Run a full-sequence forward for training.
|
|
61
91
|
* @param ids int32 array [B, L].
|
|
62
92
|
* @returns logits [B, L, V] (caller owns; dispose when done).
|
|
@@ -90,7 +120,8 @@ export function trainForwardHidden(
|
|
|
90
120
|
// B=1 / no padding: stateless offset-0 caches (one per donor layer), so the
|
|
91
121
|
// forward is pure and safe to recompute under gradient checkpointing.
|
|
92
122
|
const probe = model.makeCache();
|
|
93
|
-
const cache: Cache[] = probe.map(() =>
|
|
123
|
+
const cache: Cache[] = probe.map((c) =>
|
|
124
|
+
c instanceof SSMCache ? new TrainingSSMCache() : new TrainingCache());
|
|
94
125
|
for (const c of probe) c.dispose();
|
|
95
126
|
try {
|
|
96
127
|
return model.forwardHidden(ids, cache); // [B, L, hidden]
|
package/src/train/loss.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { logitSoftcap } from "../model/gemma4-base";
|
|
|
27
27
|
import { trainForward, trainForwardHidden } from "./forward";
|
|
28
28
|
import { setLoraScale, type TrainableLora } from "./lora-params";
|
|
29
29
|
import { flashCceForward, flashCceBackward, type FlashCceHead } from "./flash-cce";
|
|
30
|
+
import { runtimeValue } from "../runtime-config";
|
|
30
31
|
|
|
31
32
|
/** The quantized LM-head weights + final-logit softcap, accessed uniformly across
|
|
32
33
|
* models for the fused linear-CE head: Gemma is tied (`embed.asLinear`, softcap
|
|
@@ -1197,7 +1198,7 @@ export function fusedRespLogpMean(
|
|
|
1197
1198
|
// e4b time-tie point where flash's memory win takes over) takes the EXACT
|
|
1198
1199
|
// fused head instead — short rows get the faster exact head for free.
|
|
1199
1200
|
// MLX_BUN_FLASH_MIN_M=0 always honors flash (the pre-dispatch behavior).
|
|
1200
|
-
const flashMinM = Number(
|
|
1201
|
+
const flashMinM = Number(runtimeValue("MLX_BUN_FLASH_MIN_M") ?? "1024");
|
|
1201
1202
|
const useFlash = flash && (flashMinM <= 0 || M >= flashMinM);
|
|
1202
1203
|
let op: CustomVjp;
|
|
1203
1204
|
if (useFlash) {
|
package/src/train/segmented.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { MlxArray } from "../mlx/array";
|
|
|
38
38
|
import * as ops from "../mlx/ops";
|
|
39
39
|
import { peakMemory } from "../mlx/ffi";
|
|
40
40
|
import { Vjp } from "../mlx/autograd";
|
|
41
|
+
import { runtimeValue } from "../runtime-config";
|
|
41
42
|
|
|
42
43
|
// MLX_BUN_SEG_MEM_LOG=1: log the within-step peak after each phase of the
|
|
43
44
|
// segmented step (forward+boundaries / head vjp / each segment backward).
|
|
@@ -46,7 +47,7 @@ import { Vjp } from "../mlx/autograd";
|
|
|
46
47
|
// (This is how the backlog-#2 refutation below was measured. An inter-segment
|
|
47
48
|
// clearCache was also tried and had NO effect — the watermark is live memory,
|
|
48
49
|
// not allocator cache retention.)
|
|
49
|
-
const SEG_MEM =
|
|
50
|
+
const SEG_MEM = runtimeValue("MLX_BUN_SEG_MEM_LOG") === "1";
|
|
50
51
|
const segMem = (label: string): void => {
|
|
51
52
|
if (SEG_MEM) console.log(` [seg-mem] ${label}: peak=${(peakMemory() / 1e9).toFixed(2)} GB`);
|
|
52
53
|
};
|
|
@@ -68,7 +69,7 @@ import { prefixSharedCaches, prefixGatherIdx, branchLogpMeanGathered, blockSpars
|
|
|
68
69
|
// memory at the TOP level but not when nested in the segmented mlx_vjp (its
|
|
69
70
|
// per-chunk graph isn't freed incrementally there); Checkpoint does. See
|
|
70
71
|
// docs/design/orpo-training.md.
|
|
71
|
-
const SEG_HEAD =
|
|
72
|
+
const SEG_HEAD = runtimeValue("MLX_BUN_SEG_HEAD") ?? "checkpoint";
|
|
72
73
|
function boundedHeadFromHidden(
|
|
73
74
|
model: MiniCPM5Model | Gemma4Model, h: MlxArray, ids: number[], mask: number[],
|
|
74
75
|
chunkSize: number, sink: Array<{ dispose(): void }>, span?: HeadSpan,
|
|
@@ -82,7 +83,7 @@ import { rowLength, type SftBatch, type DpoBatch } from "./dataset";
|
|
|
82
83
|
|
|
83
84
|
// Token-chunk size for the bounded SFT head (the ORPO paths take theirs from
|
|
84
85
|
// cfg.orpoChunkSize; SFT has no per-run knob, 512 matches the ORPO default).
|
|
85
|
-
const SFT_HEAD_CHUNK = Number(
|
|
86
|
+
const SFT_HEAD_CHUNK = Number(runtimeValue("MLX_BUN_SEG_HEAD_CHUNK") ?? "512");
|
|
86
87
|
|
|
87
88
|
/** [M,V]-bounded response-only CE from post-finalNorm hidden — the SFT
|
|
88
89
|
* segmented head (kernel-review backlog #8). Same span arithmetic and
|
package/src/train/trainer.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import { MlxArray, pinnedBufferCount } from "../mlx/array";
|
|
12
12
|
import { clearCache, peakMemory, activeMemory, cacheMemory, resetPeakMemory } from "../mlx/ffi";
|
|
13
|
+
import { runtimeValue } from "../runtime-config";
|
|
13
14
|
|
|
14
|
-
const MEM_LOG =
|
|
15
|
+
const MEM_LOG = runtimeValue("MLX_BUN_MEM_LOG") === "1";
|
|
15
16
|
const gbStr = (b: number) => `${(b / 1e9).toFixed(2)}GB`;
|
|
16
17
|
import { ValueAndGrad } from "../mlx/autograd";
|
|
17
18
|
import * as ops from "../mlx/ops";
|
|
@@ -543,7 +544,7 @@ async function sftLoop(
|
|
|
543
544
|
// so a stale `export MLX_BUN_TRAIN_ATTN=flash` from a MiniCPM5 experiment
|
|
544
545
|
// would otherwise ride silently into a crash mid-run. Refuse it for Gemma
|
|
545
546
|
// until the >=2K re-validation lands; MiniCPM5 stays allowed.
|
|
546
|
-
if (
|
|
547
|
+
if (runtimeValue("MLX_BUN_TRAIN_ATTN") === "flash") {
|
|
547
548
|
if (model instanceof Gemma4Model)
|
|
548
549
|
throw new Error(
|
|
549
550
|
"MLX_BUN_TRAIN_ATTN=flash is disabled for Gemma models: e4b SIGTRAPs on this " +
|
package/src/vision/preprocess.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 1/255 → patchify into 48×48 patch vectors with 2D grid positions →
|
|
7
7
|
// pad to the 280 soft-token budget.
|
|
8
8
|
//
|
|
9
|
-
// Decode strategy (Bun ≥1.
|
|
9
|
+
// Decode strategy (Bun ≥1.4.0): PNG goes straight through fast-png
|
|
10
10
|
// (exact, lossless — the parity-golden path). Everything else (JPEG,
|
|
11
11
|
// HEIC, AVIF, WebP, TIFF, GIF, BMP) is transcoded to lossless PNG by
|
|
12
12
|
// Bun.Image — native OS codecs (ImageIO on macOS), EXIF auto-orient,
|
package/src/vision/prompt.ts
CHANGED
|
@@ -26,7 +26,7 @@ import type { LoadedTokenizer } from "../tokenizer";
|
|
|
26
26
|
import type { AudioTower } from "../audio/conformer";
|
|
27
27
|
import { audioSoftTokenCount, decodeAudio } from "../audio/decode";
|
|
28
28
|
import { extractMelFeatures } from "../audio/features";
|
|
29
|
-
import { fetchMediaBytes } from "../media-fetch";
|
|
29
|
+
import { fetchMediaBytes, videoMediaFetchPolicy } from "../media-fetch";
|
|
30
30
|
|
|
31
31
|
/** Common contract for both vision towers (encoder-free gemma4_unified in
|
|
32
32
|
* ./embedder.ts and the SigLIP encoder in ./siglip.ts): preprocess image
|
|
@@ -117,6 +117,52 @@ export async function extractImages(
|
|
|
117
117
|
return { messages: out, images };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** Extract video bytes from content parts, rewriting to the template's
|
|
121
|
+
* {type:"video"} form. Accepts {type:"video_url", video_url:{url}} (data:
|
|
122
|
+
* or http(s), same SSRF guard as images with the larger video body cap)
|
|
123
|
+
* and {type:"video"} with base64 `data`. Decode to frames happens later
|
|
124
|
+
* (vision/video-frames.ts — the AVFoundation sidecar). */
|
|
125
|
+
export async function extractVideos(
|
|
126
|
+
messages: ChatMessage[],
|
|
127
|
+
): Promise<{ messages: ChatMessage[]; videos: Uint8Array[] }> {
|
|
128
|
+
const videos: Uint8Array[] = [];
|
|
129
|
+
const out: ChatMessage[] = [];
|
|
130
|
+
for (const m of messages) {
|
|
131
|
+
if (!Array.isArray(m.content)) {
|
|
132
|
+
out.push(m);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const parts: Array<Record<string, unknown>> = [];
|
|
136
|
+
for (const part of m.content) {
|
|
137
|
+
if (part.type === "video_url") {
|
|
138
|
+
const url = (part.video_url as { url: string } | undefined)?.url
|
|
139
|
+
?? (part.video_url as unknown as string);
|
|
140
|
+
if (typeof url !== "string") throw new Error("video_url part missing url");
|
|
141
|
+
videos.push(await fetchMediaBytes(url, "video"));
|
|
142
|
+
parts.push({ type: "video" });
|
|
143
|
+
} else if (part.type === "video") {
|
|
144
|
+
if (typeof part.data === "string") {
|
|
145
|
+
// Same cap as fetched/data:-URL video bodies — the inline base64
|
|
146
|
+
// form must not be the uncapped route.
|
|
147
|
+
const cap = videoMediaFetchPolicy().maxBytes;
|
|
148
|
+
if (part.data.length * 0.75 > cap)
|
|
149
|
+
throw new Error(
|
|
150
|
+
`video part exceeds the ${Math.round(cap / 1024 / 1024)} MB cap`,
|
|
151
|
+
);
|
|
152
|
+
videos.push(Uint8Array.from(Buffer.from(part.data, "base64")));
|
|
153
|
+
parts.push({ type: "video" });
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error("video part requires base64 `data`");
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
parts.push(part);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
out.push({ ...m, content: parts });
|
|
162
|
+
}
|
|
163
|
+
return { messages: out, videos };
|
|
164
|
+
}
|
|
165
|
+
|
|
120
166
|
/** Extract audio bytes from OpenAI-style content parts, rewriting the parts
|
|
121
167
|
* to the template's {type:"audio"} form. Accepts the OpenAI-canonical
|
|
122
168
|
* {type:"input_audio", input_audio:{data:<b64>, format:...}} plus optiq's
|