mlx-bun 0.0.13 → 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 +31 -21
- 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/{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 +1 -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 -2
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +69 -14
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/cli.ts +131 -36
- 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 +704 -1400
- 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}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
package/src/media-fetch.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// runs model-free.
|
|
22
22
|
|
|
23
23
|
import { lookup } from "node:dns/promises";
|
|
24
|
+
import { runtimeValue } from "./runtime-config";
|
|
24
25
|
|
|
25
26
|
export interface MediaFetchPolicy {
|
|
26
27
|
/** Permit private/loopback/link-local destinations (LAN hosts). */
|
|
@@ -71,7 +72,7 @@ export interface RestrictedFetchResult {
|
|
|
71
72
|
|
|
72
73
|
export function defaultMediaFetchPolicy(): MediaFetchPolicy {
|
|
73
74
|
return {
|
|
74
|
-
allowPrivate:
|
|
75
|
+
allowPrivate: runtimeValue("MLX_BUN_ALLOW_PRIVATE_MEDIA") === "1",
|
|
75
76
|
// 10 s / 64 MB: well above what a 30 s audio clip (~5 MB of 16 kHz
|
|
76
77
|
// WAV, less compressed) or any supported image needs.
|
|
77
78
|
timeoutMs: 10_000,
|
|
@@ -405,10 +406,19 @@ export async function fetchRestrictedHttpBytes(
|
|
|
405
406
|
* redirect hop re-validated, one wall-clock timeout, and a streaming
|
|
406
407
|
* size cap. Throws Error with a client-presentable message — callers
|
|
407
408
|
* surface it as a 400 (the prompt-build catch in server.ts). */
|
|
409
|
+
/** Video clips are legitimately larger than images/audio: same SSRF guard
|
|
410
|
+
* and timeout discipline, quadruple the body cap (a 30 s 1080p H.264 clip
|
|
411
|
+
* runs tens of MB; frame sampling truncates long clips server-side). */
|
|
412
|
+
export function videoMediaFetchPolicy(): MediaFetchPolicy {
|
|
413
|
+
return { ...defaultMediaFetchPolicy(), maxBytes: 256 * 1024 * 1024 };
|
|
414
|
+
}
|
|
415
|
+
|
|
408
416
|
export async function fetchMediaBytes(
|
|
409
417
|
url: string,
|
|
410
|
-
kind: "image" | "audio",
|
|
411
|
-
policy: MediaFetchPolicy =
|
|
418
|
+
kind: "image" | "audio" | "video",
|
|
419
|
+
policy: MediaFetchPolicy = kind === "video"
|
|
420
|
+
? videoMediaFetchPolicy()
|
|
421
|
+
: defaultMediaFetchPolicy(),
|
|
412
422
|
): Promise<Uint8Array> {
|
|
413
423
|
if (url.startsWith("data:")) {
|
|
414
424
|
const comma = url.indexOf(",");
|
|
@@ -416,6 +426,12 @@ export async function fetchMediaBytes(
|
|
|
416
426
|
const meta = url.slice(0, comma);
|
|
417
427
|
const body = url.slice(comma + 1);
|
|
418
428
|
if (!meta.includes("base64")) throw new Error("data: URL must be base64");
|
|
429
|
+
// The same body cap as the HTTP path — inline payloads must not become
|
|
430
|
+
// the uncapped route (base64 inflates 4/3, so check pre-decode too).
|
|
431
|
+
if (body.length * 0.75 > policy.maxBytes)
|
|
432
|
+
throw new Error(
|
|
433
|
+
`${kind} data: URL exceeds the ${Math.round(policy.maxBytes / 1024 / 1024)} MB cap`,
|
|
434
|
+
);
|
|
419
435
|
return Uint8Array.from(Buffer.from(body, "base64"));
|
|
420
436
|
}
|
|
421
437
|
return (
|
package/src/memory/model.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type { LoadedTokenizer } from "../tokenizer";
|
|
|
30
30
|
import { BatchScheduler, type RowSampler } from "../serve/batch-scheduler";
|
|
31
31
|
import * as ops from "../mlx/ops";
|
|
32
32
|
import type { MlxArray } from "../mlx/array";
|
|
33
|
+
import { runtimeValue } from "../runtime-config";
|
|
33
34
|
|
|
34
35
|
const HF_HUB = `${process.env.HOME}/.cache/huggingface/hub`;
|
|
35
36
|
const E4B_REPO = "models--mlx-community--gemma-4-e4b-it-OptiQ-4bit";
|
|
@@ -166,7 +167,7 @@ interface MemoryRuntime {
|
|
|
166
167
|
* Opt back in with MLX_BUN_MEMORY_BATCH=8 if a length-bucketed scheduler
|
|
167
168
|
* lands. (Decision: Josh, 2026-07-01.) */
|
|
168
169
|
export function memoryBatchSize(): number {
|
|
169
|
-
const n = Number(
|
|
170
|
+
const n = Number(runtimeValue("MLX_BUN_MEMORY_BATCH") ?? "1");
|
|
170
171
|
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;
|
|
171
172
|
}
|
|
172
173
|
|
package/src/memory/vault.ts
CHANGED
|
@@ -19,12 +19,13 @@ import { homedir } from "node:os";
|
|
|
19
19
|
import { isAbsolute, join } from "node:path";
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
21
|
import { isFenceLine } from "./article";
|
|
22
|
+
import { runtimeValue } from "../runtime-config";
|
|
22
23
|
|
|
23
24
|
// ---- paths -----------------------------------------------------------
|
|
24
25
|
|
|
25
26
|
/** Root of the memory vault: ~/.mlx-bun/wiki (override with MLX_BUN_WIKI). */
|
|
26
27
|
export function vaultRoot(): string {
|
|
27
|
-
const override =
|
|
28
|
+
const override = runtimeValue("MLX_BUN_WIKI")?.trim();
|
|
28
29
|
if (override) return override.replace(/^~(?=$|[/\\])/, homedir());
|
|
29
30
|
return join(homedir(), ".mlx-bun", "wiki");
|
|
30
31
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Common admission contract for every runtime. Representation-specific
|
|
2
|
+
* planners may expose richer fields, but serving consumes only this shape. */
|
|
3
|
+
export interface MemoryPlan {
|
|
4
|
+
readonly schemaVersion: 1;
|
|
5
|
+
readonly strategy: "generic-kv" | "glm52-colibri";
|
|
6
|
+
readonly fits: boolean;
|
|
7
|
+
readonly contextTokens: number;
|
|
8
|
+
readonly maxSafeContext: number;
|
|
9
|
+
readonly weightsBytes: number;
|
|
10
|
+
readonly kvBytes: number;
|
|
11
|
+
readonly transientBytes: number;
|
|
12
|
+
readonly reserveBytes: number;
|
|
13
|
+
readonly totalBytes: number;
|
|
14
|
+
readonly usableBytes: number;
|
|
15
|
+
readonly predictedDecodeTps: number | null;
|
|
16
|
+
/** mlx allocator-cache limit, distinct from the process admission limit. */
|
|
17
|
+
readonly allocatorLimitBytes?: number;
|
|
18
|
+
}
|
package/src/mlx/ffi.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { dlopen, FFIType, ptr, read } from "bun:ffi";
|
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { nativePackDir } from "../native-pack";
|
|
17
|
+
import { runtimeValue } from "../runtime-config";
|
|
17
18
|
|
|
18
19
|
const { ptr: P, i32, u64, f32, f64, cstring } = FFIType;
|
|
19
20
|
|
|
@@ -24,7 +25,7 @@ const { ptr: P, i32, u64, f32, f64, cstring } = FFIType;
|
|
|
24
25
|
* (src/native-pack.ts; populated on first run by the CLI) → homebrew
|
|
25
26
|
* (arm64, then Intel prefix). Keep in sync with nativeRuntimeDir(). */
|
|
26
27
|
function resolveLibmlxc(): string {
|
|
27
|
-
const env =
|
|
28
|
+
const env = runtimeValue("MLX_BUN_LIBMLXC");
|
|
28
29
|
if (env) return env;
|
|
29
30
|
const beside = join(dirname(process.execPath), "libmlxc.dylib");
|
|
30
31
|
if (existsSync(beside)) return beside;
|
|
@@ -99,6 +100,7 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
99
100
|
mlx_fast_layer_norm: { args: [P, u64, u64, u64, f32, u64], returns: i32 },
|
|
100
101
|
mlx_matmul: { args: [P, u64, u64, u64], returns: i32 },
|
|
101
102
|
mlx_contiguous: { args: [P, u64, FFIType.bool, u64], returns: i32 },
|
|
103
|
+
mlx_copy: { args: [P, u64, u64], returns: i32 },
|
|
102
104
|
// (res: mlx_vector_array*, w, opt group_size, opt bits, mode, global_scale, stream)
|
|
103
105
|
mlx_quantize: { args: [P, u64, u64, u64, cstring, u64, u64], returns: i32 },
|
|
104
106
|
mlx_vector_array_get: { args: [P, u64, u64], returns: i32 },
|
|
@@ -173,16 +175,11 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
173
175
|
// (res, input, weight, stride, padding, dilation, groups, stream) — depthwise
|
|
174
176
|
// causal conv1d for Qwen3.5 gated-DeltaNet (groups == channels, padding 0).
|
|
175
177
|
mlx_conv1d: { args: [P, u64, u64, i32, i32, i32, i32, u64], returns: i32 },
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// bun:ffi 1.3.14 writes one 8-byte slot per arg — the callee reads shifted
|
|
182
|
-
// garbage and mlx segfaults on a bogus stream. Workaround: declare the two
|
|
183
|
-
// stack ints as ONE u64 (dilation_1 | groups<<32) so Bun's slot layout
|
|
184
|
-
// byte-matches the Apple ABI. ops.conv2d does the packing.
|
|
185
|
-
mlx_conv2d: { args: [P, u64, u64, i32, i32, i32, i32, i32, u64, u64], returns: i32 },
|
|
178
|
+
// Natural mlx-c signatures. Bun 1.4 fixed the macOS arm64 stack packing
|
|
179
|
+
// bug recorded in lab/repro/bun-ffi-stack-args, so these must match the
|
|
180
|
+
// header exactly. mlx-bun now requires Bun 1.4 or newer.
|
|
181
|
+
mlx_conv2d: { args: [P, u64, u64, i32, i32, i32, i32, i32, i32, i32, u64], returns: i32 },
|
|
182
|
+
mlx_conv3d: { args: [P, u64, u64, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, u64], returns: i32 },
|
|
186
183
|
// --- training: autograd (value_and_grad) — proven in lab/spikes/phase-train-vag.ts.
|
|
187
184
|
// mlx_value_and_grad(res: mlx_closure_value_and_grad*, fun: mlx_closure,
|
|
188
185
|
// argnums: int*, n) → builds a value+grad closure differentiating the
|
package/src/mlx/ops.ts
CHANGED
|
@@ -113,6 +113,15 @@ export function matmul(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
|
113
113
|
return new MlxArray(outArray("matmul", (o) => C.mlx_matmul(o, a.handle, b.handle, s)));
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** Bit-identical copy through a FRESH primitive node (mx.copy). Unlike
|
|
117
|
+
* `contiguous`, this always creates a new graph node even for an already-
|
|
118
|
+
* contiguous input — the tool for DETACHING an array from a multi-output
|
|
119
|
+
* primitive's sibling set (a retained sibling output pins every co-output's
|
|
120
|
+
* buffer natively; see gatedDeltaUpdate's state detach, 2026-08-20). */
|
|
121
|
+
export function copyOf(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
122
|
+
return new MlxArray(outArray("copy", (o) => C.mlx_copy(o, a.handle, s)));
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
/** Materialize a row-major contiguous copy (needed before rawBytes on views). */
|
|
117
126
|
export function contiguous(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
118
127
|
return new MlxArray(
|
|
@@ -880,20 +889,36 @@ export function conv1d(
|
|
|
880
889
|
|
|
881
890
|
/** nn.Conv2d: input [B, H, W, C_in] (NHWC), weight [C_out, kH, kW, C_in/groups]
|
|
882
891
|
* → [B, H', W', C_out]. mlx weight layout — the oracle transposes PyTorch's
|
|
883
|
-
* [C_out, C_in, kH, kW] at load. For the gemma-4 audio SSCP subsampler.
|
|
884
|
-
* dilation_1 and groups ride in one packed u64 — see the mlx_conv2d hazard
|
|
885
|
-
* note in ffi.ts (bun:ffi stack-arg ABI bug). */
|
|
892
|
+
* [C_out, C_in, kH, kW] at load. For the gemma-4 audio SSCP subsampler. */
|
|
886
893
|
export function conv2d(
|
|
887
894
|
input: MlxArray, weight: MlxArray, stride: [number, number] = [1, 1],
|
|
888
895
|
padding: [number, number] = [0, 0], dilation: [number, number] = [1, 1],
|
|
889
896
|
groups = 1, s: S = gpuStream,
|
|
890
897
|
): MlxArray {
|
|
891
|
-
const dilation1AndGroups =
|
|
892
|
-
(BigInt(dilation[1] >>> 0) & 0xffffffffn) | (BigInt(groups >>> 0) << 32n);
|
|
893
898
|
return new MlxArray(
|
|
894
899
|
outArray("conv2d", (o) =>
|
|
895
900
|
C.mlx_conv2d(o, input.handle, weight.handle, stride[0], stride[1],
|
|
896
|
-
padding[0], padding[1], dilation[0],
|
|
901
|
+
padding[0], padding[1], dilation[0], dilation[1], groups, s),
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** nn.Conv3d: input [B, D, H, W, C_in] (NDHWC), weight [C_out, kD, kH, kW,
|
|
907
|
+
* C_in/groups] → [B, D', H', W', C_out]. The Qwen3-VL vision patch embed
|
|
908
|
+
* (kernel == stride) — the conv KERNEL's accumulation order is the parity-
|
|
909
|
+
* relevant arithmetic; a value-equivalent GEMM differs by bf16 ulp. */
|
|
910
|
+
export function conv3d(
|
|
911
|
+
input: MlxArray, weight: MlxArray,
|
|
912
|
+
stride: [number, number, number] = [1, 1, 1],
|
|
913
|
+
padding: [number, number, number] = [0, 0, 0],
|
|
914
|
+
dilation: [number, number, number] = [1, 1, 1],
|
|
915
|
+
groups = 1, s: S = gpuStream,
|
|
916
|
+
): MlxArray {
|
|
917
|
+
return new MlxArray(
|
|
918
|
+
outArray("conv3d", (o) =>
|
|
919
|
+
C.mlx_conv3d(o, input.handle, weight.handle, stride[0], stride[1],
|
|
920
|
+
stride[2], padding[0], padding[1], padding[2], dilation[0],
|
|
921
|
+
dilation[1], dilation[2], groups, s),
|
|
897
922
|
),
|
|
898
923
|
);
|
|
899
924
|
}
|
|
@@ -242,7 +242,7 @@ export function extractKVRow(cache: KVCache, leftPad: number, i: number): KVCach
|
|
|
242
242
|
const cut = (a: MlxArray): MlxArray => {
|
|
243
243
|
const [, H, , D] = a.shape as [number, number, number, number];
|
|
244
244
|
const view = a.slice([i, 0, leftPad, 0], [i + 1, H, S, D]);
|
|
245
|
-
const own = ops.contiguous(view)
|
|
245
|
+
const own = ops.copyOf(view); // TRUE copy: contiguous(view) is a no-op VIEW when already contiguous — pins the source buffer (2026-08-20 DeltaNet conv leak class)
|
|
246
246
|
view.dispose();
|
|
247
247
|
return own;
|
|
248
248
|
};
|
|
@@ -122,7 +122,7 @@ export function extractQuantRow(
|
|
|
122
122
|
tripleMap(t, (a) => {
|
|
123
123
|
const [, H, , D] = a.shape as [number, number, number, number];
|
|
124
124
|
const view = a.slice([i, 0, leftPad, 0], [i + 1, H, S, D]);
|
|
125
|
-
const own = ops.contiguous(view)
|
|
125
|
+
const own = ops.copyOf(view); // TRUE copy: contiguous(view) is a no-op VIEW when already contiguous — pins the source buffer (2026-08-20 DeltaNet conv leak class)
|
|
126
126
|
view.dispose();
|
|
127
127
|
return own;
|
|
128
128
|
});
|
|
@@ -32,6 +32,12 @@ import { Dtype } from "../mlx/ffi";
|
|
|
32
32
|
import { RotatingQuantizedKVCache, type Mask } from "./gemma4-base";
|
|
33
33
|
import { buildBatchedRotatingMask } from "./batched-rotating";
|
|
34
34
|
import type { QuantRow } from "./batched-quant";
|
|
35
|
+
import { BatchedRotatingState } from "./batched-rotating-state";
|
|
36
|
+
import {
|
|
37
|
+
mergeStorageRows,
|
|
38
|
+
quantizedRowStorage,
|
|
39
|
+
temporalStorageView,
|
|
40
|
+
} from "./batched-row-storage";
|
|
35
41
|
|
|
36
42
|
const STEP = 256; // shared with RotatingQuantizedKVCache.STEP and the bf16 twin
|
|
37
43
|
|
|
@@ -49,9 +55,7 @@ const mapTriple = (
|
|
|
49
55
|
* storage. Scalar ring state (`ringIdx`/`offset` reuse the base fields);
|
|
50
56
|
* per-row `offsetArr` (absolute positions → RoPE) and `leftPad`. */
|
|
51
57
|
export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
52
|
-
|
|
53
|
-
leftPad: number[];
|
|
54
|
-
#rotated = false;
|
|
58
|
+
readonly #rows: BatchedRotatingState;
|
|
55
59
|
/** Per-row RoPE positions. STABLE ACROSS A STEP — refreshed only at
|
|
56
60
|
* releaseRopeArr() (the scheduler's post-dispatch hook), never inside
|
|
57
61
|
* the update: the monolith CAPTURES it pre-update and ropes Q after
|
|
@@ -67,13 +71,16 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
67
71
|
maxSize: number, groupSize: number, bits: number, leftPad: number[], offsets: number[],
|
|
68
72
|
) {
|
|
69
73
|
super(maxSize, groupSize, bits);
|
|
70
|
-
this
|
|
71
|
-
this.offsetArr = [...offsets];
|
|
74
|
+
this.#rows = new BatchedRotatingState(maxSize, leftPad, offsets);
|
|
72
75
|
this.ropeOffsetArr = MlxArray.fromInt32(Int32Array.from(offsets), [offsets.length]);
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
get offsetArr(): number[] { return this.#rows.offsets; }
|
|
79
|
+
get leftPad(): number[] { return this.#rows.leftPad; }
|
|
80
|
+
get batchSize(): number { return this.#rows.batchSize; }
|
|
81
|
+
|
|
75
82
|
get #B(): number {
|
|
76
|
-
return this.
|
|
83
|
+
return this.#rows.batchSize;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
#seqLen2(): number {
|
|
@@ -104,7 +111,7 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
104
111
|
mode: "array",
|
|
105
112
|
arr: buildBatchedRotatingMask(
|
|
106
113
|
this.#B, N, this.leftPad, this.maxSize, window,
|
|
107
|
-
this.
|
|
114
|
+
this.#rows.ringIndex, this.#rows.totalOffset, this.#rows.rotated,
|
|
108
115
|
),
|
|
109
116
|
};
|
|
110
117
|
}
|
|
@@ -117,7 +124,7 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
117
124
|
const vD = v.shape[3]!;
|
|
118
125
|
if (S !== 1)
|
|
119
126
|
throw new Error("BatchedRotatingQuantCache supports N=1 decode updates only (solo-prefill then merge)");
|
|
120
|
-
const prev = this.
|
|
127
|
+
const prev = this.#rows.totalOffset;
|
|
121
128
|
|
|
122
129
|
// Grow the buffer (in STEP chunks) until it reaches maxSize.
|
|
123
130
|
if (!this.keys || (prev >= this.#seqLen2() && this.#seqLen2() < this.maxSize)) {
|
|
@@ -144,7 +151,8 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
144
151
|
this.keys = newK;
|
|
145
152
|
this.values = newV;
|
|
146
153
|
}
|
|
147
|
-
this.
|
|
154
|
+
this.#rows.markGrown(prev);
|
|
155
|
+
this.ringIdx = this.#rows.ringIndex;
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
// Trim any overshoot past maxSize (decrements left padding persistently).
|
|
@@ -159,16 +167,12 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
159
167
|
});
|
|
160
168
|
this.keys = cutFrom(this.keys!);
|
|
161
169
|
this.values = cutFrom(this.values!);
|
|
162
|
-
this.
|
|
163
|
-
this.
|
|
170
|
+
this.#rows.trimOvershoot(trimSize);
|
|
171
|
+
this.ringIdx = this.#rows.ringIndex;
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
// Rotate when the write head reaches the end of the ring.
|
|
167
|
-
|
|
168
|
-
this.#rotated = true;
|
|
169
|
-
this.ringIdx = 0;
|
|
170
|
-
}
|
|
171
|
-
if (this.#rotated) this.leftPad = this.leftPad.map((x) => x - S);
|
|
175
|
+
this.ringIdx = this.#rows.beginWrite(S);
|
|
172
176
|
|
|
173
177
|
// Quantize + write the new K/V at the ring head (six sliceUpdates).
|
|
174
178
|
const kq = ops.quantize(k, this.groupSize, this.bits);
|
|
@@ -184,9 +188,9 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
184
188
|
disposeTriple(kq);
|
|
185
189
|
disposeTriple(vq);
|
|
186
190
|
|
|
187
|
-
this.
|
|
188
|
-
this.
|
|
189
|
-
this.ringIdx
|
|
191
|
+
this.#rows.commitWrite(S);
|
|
192
|
+
this.offset = this.#rows.totalOffset;
|
|
193
|
+
this.ringIdx = this.#rows.ringIndex;
|
|
190
194
|
|
|
191
195
|
// Return the populated prefix (ring not yet full) or the whole buffer.
|
|
192
196
|
const upTo = this.offset < this.maxSize ? this.offset : this.#seqLen2();
|
|
@@ -209,27 +213,10 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
209
213
|
* overrides the base to use the BATCHED ring state. */
|
|
210
214
|
override temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
211
215
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let tt: MlxArray;
|
|
217
|
-
if (this.ringIdx === Sbuf) {
|
|
218
|
-
tt = a.slice([0, 0, 0, 0], [B, H, Sbuf, D]);
|
|
219
|
-
} else if (this.ringIdx < this.offset) {
|
|
220
|
-
const tail = a.slice([0, 0, this.ringIdx, 0], [B, H, Sbuf, D]);
|
|
221
|
-
const head = a.slice([0, 0, 0, 0], [B, H, this.ringIdx, D]);
|
|
222
|
-
tt = ops.concatAxis([tail, head], 2);
|
|
223
|
-
tail.dispose();
|
|
224
|
-
head.dispose();
|
|
225
|
-
} else {
|
|
226
|
-
tt = a.slice([0, 0, 0, 0], [B, H, this.ringIdx, D]);
|
|
227
|
-
}
|
|
228
|
-
const cut = tt.slice([0, 0, 0, 0], [B, H, valid, D]);
|
|
229
|
-
tt.dispose();
|
|
230
|
-
return cut;
|
|
231
|
-
});
|
|
232
|
-
return [order(this.keys), order(this.values)];
|
|
216
|
+
return [
|
|
217
|
+
temporalStorageView(quantizedRowStorage, this.keys, this.#rows),
|
|
218
|
+
temporalStorageView(quantizedRowStorage, this.values, this.#rows),
|
|
219
|
+
];
|
|
233
220
|
}
|
|
234
221
|
|
|
235
222
|
/** Quantized twin of BatchedRotatingCache.extractRow (mlx-lm
|
|
@@ -244,34 +231,13 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
244
231
|
extractRow(i: number): RotatingQuantizedKVCache | null {
|
|
245
232
|
if (!this.keys || !this.values) return null;
|
|
246
233
|
const pad = Math.max(0, this.leftPad[i]!);
|
|
247
|
-
const valid = Math.min(this.offset, this.maxSize);
|
|
248
|
-
const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
249
|
-
mapTriple(t, (a) => {
|
|
250
|
-
const [, H, Sbuf, D] = a.shape as [number, number, number, number];
|
|
251
|
-
const row = a.slice([i, 0, 0, 0], [i + 1, H, Sbuf, D]);
|
|
252
|
-
let tt: MlxArray;
|
|
253
|
-
if (this.ringIdx === Sbuf) {
|
|
254
|
-
tt = row;
|
|
255
|
-
} else if (this.ringIdx < this.offset) {
|
|
256
|
-
const tail = row.slice([0, 0, this.ringIdx, 0], [1, H, Sbuf, D]);
|
|
257
|
-
const head = row.slice([0, 0, 0, 0], [1, H, this.ringIdx, D]);
|
|
258
|
-
tt = ops.concatAxis([tail, head], 2);
|
|
259
|
-
tail.dispose();
|
|
260
|
-
head.dispose();
|
|
261
|
-
row.dispose();
|
|
262
|
-
} else {
|
|
263
|
-
tt = row.slice([0, 0, 0, 0], [1, H, this.ringIdx, D]);
|
|
264
|
-
row.dispose();
|
|
265
|
-
}
|
|
266
|
-
const cutV = tt.slice([0, 0, pad, 0], [1, H, valid, D]);
|
|
267
|
-
tt.dispose();
|
|
268
|
-
const own = ops.contiguous(cutV);
|
|
269
|
-
cutV.dispose();
|
|
270
|
-
return own;
|
|
271
|
-
});
|
|
272
234
|
const c = new RotatingQuantizedKVCache(this.maxSize, this.groupSize, this.bits);
|
|
273
|
-
const k =
|
|
274
|
-
|
|
235
|
+
const k = temporalStorageView(quantizedRowStorage, this.keys, this.#rows, {
|
|
236
|
+
row: i, from: pad, copy: true,
|
|
237
|
+
});
|
|
238
|
+
const v = temporalStorageView(quantizedRowStorage, this.values, this.#rows, {
|
|
239
|
+
row: i, from: pad, copy: true,
|
|
240
|
+
});
|
|
275
241
|
c.restoreState(k, v, this.offsetArr[i]!, k.packed.shape[2]!);
|
|
276
242
|
return c;
|
|
277
243
|
}
|
|
@@ -279,22 +245,19 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
279
245
|
/** Keep only `keep` rows along the batch axis (eviction), in place. */
|
|
280
246
|
filter(keep: number[]): void {
|
|
281
247
|
if (this.keys && this.values) {
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
mapTriple(t, (a) => ops.takeAxis(a, idxArr, 0));
|
|
285
|
-
const k = take(this.keys);
|
|
286
|
-
const v = take(this.values);
|
|
287
|
-
idxArr.dispose();
|
|
248
|
+
const k = quantizedRowStorage.takeRows(this.keys, keep);
|
|
249
|
+
const v = quantizedRowStorage.takeRows(this.values, keep);
|
|
288
250
|
disposeTriple(this.keys);
|
|
289
251
|
disposeTriple(this.values);
|
|
290
252
|
this.keys = k;
|
|
291
253
|
this.values = v;
|
|
292
254
|
}
|
|
293
|
-
this.
|
|
294
|
-
this.leftPad = keep.map((i) => this.leftPad[i]!);
|
|
255
|
+
this.#rows.filter(keep);
|
|
295
256
|
this.releaseRopeArr(); // safe here: filter runs between steps
|
|
296
257
|
}
|
|
297
258
|
|
|
259
|
+
filterRows(keep: readonly number[]): void { this.filter([...keep]); }
|
|
260
|
+
|
|
298
261
|
override isTrimmable(): boolean {
|
|
299
262
|
return false; // batched rows never re-enter the prompt cache directly
|
|
300
263
|
}
|
|
@@ -317,30 +280,13 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
317
280
|
const width = Math.max(...lens, 0);
|
|
318
281
|
const leftPad = lens.map((l) => width - l);
|
|
319
282
|
const cache = new BatchedRotatingQuantCache(maxSize, groupSize, bits, leftPad, offsets);
|
|
283
|
+
cache.#rows.restoreMerged(width, offsets);
|
|
284
|
+
cache.offset = cache.#rows.totalOffset;
|
|
285
|
+
cache.ringIdx = cache.#rows.ringIndex;
|
|
320
286
|
if (width === 0) return cache;
|
|
321
287
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
325
|
-
const z = ops.zeros([B, H, pad, D], a.dtype);
|
|
326
|
-
const out = ops.concatAxis([z, a], 2);
|
|
327
|
-
z.dispose();
|
|
328
|
-
return out;
|
|
329
|
-
};
|
|
330
|
-
const padded = rows.map((r, i) => ({
|
|
331
|
-
keys: mapTriple(r.keys, (a) => padTo(a, leftPad[i]!)),
|
|
332
|
-
values: mapTriple(r.values, (a) => padTo(a, leftPad[i]!)),
|
|
333
|
-
}));
|
|
334
|
-
const cat = (pick: (r: QuantRow) => ops.QuantizedTensor): ops.QuantizedTensor => ({
|
|
335
|
-
packed: ops.concatAxis(padded.map((r) => pick(r).packed), 0),
|
|
336
|
-
scales: ops.concatAxis(padded.map((r) => pick(r).scales), 0),
|
|
337
|
-
biases: ops.concatAxis(padded.map((r) => pick(r).biases), 0),
|
|
338
|
-
});
|
|
339
|
-
cache.keys = cat((r) => r.keys);
|
|
340
|
-
cache.values = cat((r) => r.values);
|
|
341
|
-
for (const r of padded) { disposeTriple(r.keys); disposeTriple(r.values); }
|
|
342
|
-
cache.ringIdx = width;
|
|
343
|
-
cache.offset = width;
|
|
288
|
+
cache.keys = mergeStorageRows(quantizedRowStorage, rows.map((row) => row.keys), leftPad);
|
|
289
|
+
cache.values = mergeStorageRows(quantizedRowStorage, rows.map((row) => row.values), leftPad);
|
|
344
290
|
return cache;
|
|
345
291
|
}
|
|
346
292
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Host-side row and ring bookkeeping shared by plain and quantized storage. */
|
|
2
|
+
export class BatchedRotatingState {
|
|
3
|
+
readonly maxSize: number;
|
|
4
|
+
offsets: number[];
|
|
5
|
+
leftPad: number[];
|
|
6
|
+
ringIndex = 0;
|
|
7
|
+
totalOffset = 0;
|
|
8
|
+
rotated = false;
|
|
9
|
+
|
|
10
|
+
constructor(maxSize: number, leftPad: readonly number[], offsets?: readonly number[]) {
|
|
11
|
+
this.maxSize = maxSize;
|
|
12
|
+
this.leftPad = [...leftPad];
|
|
13
|
+
this.offsets = offsets ? [...offsets] : leftPad.map((pad) => -pad);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get batchSize(): number { return this.leftPad.length; }
|
|
17
|
+
get validLength(): number { return Math.min(this.totalOffset, this.maxSize); }
|
|
18
|
+
get trimmable(): boolean { return this.totalOffset < this.maxSize; }
|
|
19
|
+
|
|
20
|
+
markGrown(previousOffset: number): void {
|
|
21
|
+
this.ringIndex = previousOffset;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
trimOvershoot(tokens: number): void {
|
|
25
|
+
if (tokens <= 0) return;
|
|
26
|
+
this.ringIndex = this.maxSize;
|
|
27
|
+
this.leftPad = this.leftPad.map((pad) => pad - tokens);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Prepare an N-token write and return its physical ring column. */
|
|
31
|
+
beginWrite(tokens: number): number {
|
|
32
|
+
if (this.ringIndex === this.maxSize) {
|
|
33
|
+
this.rotated = true;
|
|
34
|
+
this.ringIndex = 0;
|
|
35
|
+
}
|
|
36
|
+
if (this.rotated) this.leftPad = this.leftPad.map((pad) => pad - tokens);
|
|
37
|
+
return this.ringIndex;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
commitWrite(tokens: number): void {
|
|
41
|
+
this.totalOffset += tokens;
|
|
42
|
+
this.offsets = this.offsets.map((offset) => offset + tokens);
|
|
43
|
+
this.ringIndex += tokens;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
filter(keep: readonly number[]): void {
|
|
47
|
+
this.offsets = keep.map((row) => this.offsets[row]!);
|
|
48
|
+
this.leftPad = keep.map((row) => this.leftPad[row]!);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
trim(tokens: number): number {
|
|
52
|
+
const amount = Math.min(this.totalOffset, tokens);
|
|
53
|
+
this.totalOffset -= amount;
|
|
54
|
+
this.ringIndex -= amount;
|
|
55
|
+
this.offsets = this.offsets.map((offset) => offset - amount);
|
|
56
|
+
return amount;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
restoreMerged(width: number, offsets: readonly number[]): void {
|
|
60
|
+
this.totalOffset = width;
|
|
61
|
+
this.ringIndex = width;
|
|
62
|
+
this.offsets = [...offsets];
|
|
63
|
+
this.rotated = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Physical ranges that expose a ring buffer in temporal order. */
|
|
67
|
+
temporalRanges(bufferLength: number): readonly [number, number][] {
|
|
68
|
+
if (this.ringIndex === bufferLength) return [[0, bufferLength]];
|
|
69
|
+
if (this.ringIndex < this.totalOffset)
|
|
70
|
+
return [[this.ringIndex, bufferLength], [0, this.ringIndex]];
|
|
71
|
+
return [[0, this.ringIndex]];
|
|
72
|
+
}
|
|
73
|
+
}
|