mlx-bun 0.0.4 → 0.0.6
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 +175 -93
- package/docs/dag/training-inference-map.html +575 -0
- package/docs/design/adapters-end-to-end.md +50 -46
- package/docs/design/compat-cli-surface-design.md +10 -8
- package/docs/design/docs-reorg-plan.md +14 -1
- package/docs/design/hlg-sampling.md +12 -2
- package/docs/design/memory-system.md +193 -295
- package/docs/design/optimization_plan.md +4 -2
- package/docs/design/orpo-dynamic-lambda.md +179 -0
- package/docs/design/orpo-future-enhancements.md +340 -0
- package/docs/design/orpo-training.md +1212 -0
- package/docs/design/parallel-slots.md +34 -26
- package/docs/design/parity-tier-dag.md +143 -0
- package/docs/design/runtime-isolation.md +1 -1
- package/docs/design/segmented-backward-training.md +58 -39
- package/docs/design/tauri-desktop-app.md +127 -0
- package/docs/design/w4a16-compute-precision-spike.md +232 -0
- package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
- package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
- package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
- package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
- package/docs/investigations/starter-model-port-handoff.md +32 -13
- package/docs/investigations/steel-flash-cce-handoff.md +489 -0
- package/docs/investigations/trainer-validation-experiment.md +173 -0
- package/docs/planning/IDEAS.md +29 -0
- package/docs/planning/PRODUCT_ROADMAP.md +147 -27
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/journal.md +161 -0
- package/docs/reference/distribution.md +61 -5
- package/docs/reference/library-api.md +29 -14
- package/docs/reference/memory.md +77 -0
- package/docs/reference/orpo-quickstart.md +130 -0
- package/docs/reference/server-api.md +105 -7
- package/docs/reference/server-config.md +12 -5
- package/docs/reference/training.md +296 -0
- package/package.json +1 -1
- package/src/cli.ts +651 -18
- package/src/eval/ifeval.ts +138 -0
- package/src/eval/runner.ts +76 -6
- package/src/eval/tasks/bfcl.ts +49 -15
- package/src/eval/tasks/gsm8k.ts +21 -9
- package/src/eval/tasks/hashhop.ts +33 -4
- package/src/eval/tasks/humaneval.ts +30 -8
- package/src/eval/tasks/ifeval.ts +52 -18
- package/src/eval/tasks/mmlu.ts +85 -74
- package/src/eval/ultrafeedback.ts +60 -0
- package/src/jobs/runner.ts +6 -0
- package/src/lora.ts +12 -7
- package/src/memory/chunk.ts +25 -0
- package/src/memory/cluster.ts +24 -0
- package/src/memory/db.ts +114 -0
- package/src/memory/ingest.ts +22 -0
- package/src/memory/pipeline.ts +63 -0
- package/src/memory/prompts.ts +23 -0
- package/src/memory/schedule.ts +163 -0
- package/src/memory/synthesize.ts +29 -0
- package/src/memory/tools.ts +399 -0
- package/src/memory/vault.ts +694 -0
- package/src/memory/wikify.ts +23 -0
- package/src/mlx/metal-kernel.ts +31 -4
- package/src/model/flash-attention.ts +37 -19
- package/src/model/fused-geglu-kernel.ts +111 -0
- package/src/model/gemma4-base.ts +39 -1
- package/src/model/gemma4.ts +196 -21
- package/src/model/minicpm5.ts +44 -3
- package/src/pi-provider.ts +29 -13
- package/src/pi-session.ts +81 -0
- package/src/pi-terminal.ts +13 -24
- package/src/pi-web.ts +246 -166
- package/src/quantize/job.ts +17 -2
- package/src/quantize/quantizer.ts +26 -2
- package/src/server.ts +216 -47
- package/src/tokenizer.ts +6 -2
- package/src/train/dataset.ts +29 -6
- package/src/train/flash-cce.ts +1175 -0
- package/src/train/job.ts +64 -3
- package/src/train/lora-params.ts +53 -4
- package/src/train/loss.ts +947 -0
- package/src/train/optimizer.ts +10 -3
- package/src/train/prefix-shared.ts +435 -0
- package/src/train/rank.ts +36 -0
- package/src/train/segmented.ts +1148 -3
- package/src/train/steel-qmm-header.ts +1461 -0
- package/src/train/trainer.ts +570 -54
- package/src/train/watch.ts +542 -0
- package/src/web/app.html +285 -19
- package/src/web/skills/memory/SKILL.md +97 -0
- package/src/web/skills/web-research/SKILL.md +3 -1
- package/src/web/skills.ts +46 -8
- /package/{docs → src/assets}/curve-designer.html +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// mlx-bun memory — synthesis stage 5: WIKIFY (deterministic). ⚠️ M1 STUB.
|
|
2
|
+
//
|
|
3
|
+
// Non-model editorial cleanup after synthesis: canonicalize [[wikilinks]]
|
|
4
|
+
// (spaces↔underscores, case, resolve to existing stems), repair footnotes, and
|
|
5
|
+
// run the deterministic editorial gate. Non-fatal — a failure here logs and
|
|
6
|
+
// continues; it never corrupts an article. Mirrors lucien's wikify.ts +
|
|
7
|
+
// normalize-wikilinks.ts + normalize-footnotes.ts.
|
|
8
|
+
//
|
|
9
|
+
// See docs/design/memory-system.md → "The nightly pipeline" (wikify/normalize).
|
|
10
|
+
|
|
11
|
+
import type { SynthesisEvent } from "./pipeline";
|
|
12
|
+
|
|
13
|
+
export interface WikifyResult {
|
|
14
|
+
articlesNormalized: number;
|
|
15
|
+
linksFixed: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Deterministic editorial gate + link/footnote normalize. STUB. */
|
|
19
|
+
export async function wikify(_onEvent?: (e: SynthesisEvent) => void): Promise<WikifyResult> {
|
|
20
|
+
// TODO(M1): resolveWikilinkToStem across touched articles (vault.ts already
|
|
21
|
+
// has the resolver), footnote renumber/repair, {{stub}} marking.
|
|
22
|
+
throw new Error("memory.wikify is not implemented yet (M1)");
|
|
23
|
+
}
|
package/src/mlx/metal-kernel.ts
CHANGED
|
@@ -29,8 +29,16 @@ export interface MetalKernelSpec {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface MetalKernelCall {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/** Concrete output shapes/dtypes. Provide this OR `outputShapeFn` (the latter
|
|
33
|
+
* makes the kernel derive its outputs from the input shapes, like a real MLX
|
|
34
|
+
* primitive — required to compose inside an mx.compile'd / shapeless closure,
|
|
35
|
+
* where the wrapper must answer "given these input shapes, what comes out?"). */
|
|
36
|
+
outputs?: { shape: number[]; dtype: Dtype }[];
|
|
37
|
+
/** Derive output shapes/dtypes from the (current/traced) input shapes. */
|
|
38
|
+
outputShapeFn?: (inputs: MlxArray[]) => { shape: number[]; dtype: Dtype }[];
|
|
39
|
+
/** Concrete launch grid, OR derive it from inputs (so it tracks shape under
|
|
40
|
+
* compile replay the same way `outputShapeFn` tracks output shapes). */
|
|
41
|
+
grid: [number, number, number] | ((inputs: MlxArray[]) => [number, number, number]);
|
|
34
42
|
threadGroup: [number, number, number];
|
|
35
43
|
templateInts?: Record<string, number>;
|
|
36
44
|
templateDtypes?: Record<string, Dtype>;
|
|
@@ -59,31 +67,50 @@ export class MetalKernel {
|
|
|
59
67
|
apply(inputs: MlxArray[], call: MetalKernelCall): MlxArray[] {
|
|
60
68
|
if (this.#disposed) throw new Error("MetalKernel used after dispose");
|
|
61
69
|
const cfg = C.mlx_fast_metal_kernel_config_new();
|
|
70
|
+
// bun:ffi ptr() does NOT retain its argument, so any JS buffer whose pointer
|
|
71
|
+
// crosses an FFI call below is anchored here until that call returns —
|
|
72
|
+
// otherwise GC could free it mid-call (the lifetime class behind the
|
|
73
|
+
// flash-CCE pin bug). Pure-additive: holds references, changes no logic.
|
|
74
|
+
const keepAlive: unknown[] = [];
|
|
62
75
|
try {
|
|
63
|
-
|
|
76
|
+
// Derive outputs + grid from the (current) input shapes when a function is
|
|
77
|
+
// given — so the SAME kernel object answers correctly for any input shape
|
|
78
|
+
// during a compile/replay trace, instead of baking in one call's shapes.
|
|
79
|
+
const outputs = call.outputShapeFn ? call.outputShapeFn(inputs) : call.outputs;
|
|
80
|
+
if (!outputs) throw new Error("metal_kernel: provide outputs or outputShapeFn");
|
|
81
|
+
const grid = typeof call.grid === "function" ? call.grid(inputs) : call.grid;
|
|
82
|
+
for (const o of outputs) {
|
|
64
83
|
const shape = new Int32Array(o.shape);
|
|
84
|
+
keepAlive.push(shape);
|
|
65
85
|
if (C.mlx_fast_metal_kernel_config_add_output_arg(cfg, ptr(shape), BigInt(o.shape.length), o.dtype) !== 0)
|
|
66
86
|
throw new Error(`metal_kernel add_output_arg failed: ${takeMlxError() ?? ""}`);
|
|
67
87
|
}
|
|
68
|
-
C.mlx_fast_metal_kernel_config_set_grid(cfg,
|
|
88
|
+
C.mlx_fast_metal_kernel_config_set_grid(cfg, grid[0], grid[1], grid[2]);
|
|
69
89
|
C.mlx_fast_metal_kernel_config_set_thread_group(cfg, call.threadGroup[0], call.threadGroup[1], call.threadGroup[2]);
|
|
70
90
|
if (call.initValue !== undefined)
|
|
71
91
|
C.mlx_fast_metal_kernel_config_set_init_value(cfg, call.initValue);
|
|
72
92
|
for (const [k, v] of Object.entries(call.templateInts ?? {})) {
|
|
73
93
|
const kb = Buffer.from(k + "\0", "utf8");
|
|
94
|
+
keepAlive.push(kb);
|
|
74
95
|
C.mlx_fast_metal_kernel_config_add_template_arg_int(cfg, ptr(kb), v);
|
|
75
96
|
}
|
|
76
97
|
for (const [k, v] of Object.entries(call.templateDtypes ?? {})) {
|
|
77
98
|
const kb = Buffer.from(k + "\0", "utf8");
|
|
99
|
+
keepAlive.push(kb);
|
|
78
100
|
C.mlx_fast_metal_kernel_config_add_template_arg_dtype(cfg, ptr(kb), v);
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
const handles = new BigUint64Array(inputs.map((a) => a.handle));
|
|
104
|
+
keepAlive.push(handles);
|
|
82
105
|
const inVec = C.mlx_vector_array_new_data(ptr(handles), BigInt(inputs.length));
|
|
83
106
|
const slot = new BigUint64Array([C.mlx_vector_array_new()]);
|
|
107
|
+
keepAlive.push(slot);
|
|
84
108
|
const slotPtr = ptr(slot);
|
|
85
109
|
const status = C.mlx_fast_metal_kernel_apply(slotPtr, this.#kernel, inVec, cfg, call.stream ?? gpuStream);
|
|
86
110
|
const outVec = read.u64(slotPtr, 0);
|
|
111
|
+
// anchor: all ptr()'d JS buffers above are done being read by native code by
|
|
112
|
+
// this point; touching keepAlive keeps the JIT from freeing them earlier.
|
|
113
|
+
if (keepAlive.length < 0) throw new Error("unreachable");
|
|
87
114
|
C.mlx_vector_array_free(inVec);
|
|
88
115
|
if (status !== 0) {
|
|
89
116
|
C.mlx_vector_array_free(outVec);
|
|
@@ -29,10 +29,12 @@ function forwardTiles(headDim: number): [number, number] {
|
|
|
29
29
|
throw new Error(`unsupported head_dim=${headDim}`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** uint32 [n] array (matches optiq's shape/causal input dtype).
|
|
32
|
+
/** uint32 [n] array (matches optiq's shape/causal input dtype). COPY into an
|
|
33
|
+
* mlx-owned array (not the pinning fromView) — these tiny kernel-arg arrays are
|
|
34
|
+
* disposed before the lazy kernel evaluates; pinning leaks the host buffer per
|
|
35
|
+
* call (see flash-cce.ts u32 for the full diagnosis). */
|
|
33
36
|
function u32(values: number[]): MlxArray {
|
|
34
|
-
|
|
35
|
-
return MlxArray.fromView(new Uint8Array(u.buffer.slice(0)), [values.length], Dtype.uint32);
|
|
37
|
+
return MlxArray.fromBytesCopy(new Uint8Array(new Uint32Array(values).buffer), [values.length], Dtype.uint32);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
// Forward shader — verbatim from optiq flash_attention_metal.py _KERNEL_SOURCE.
|
|
@@ -406,7 +408,13 @@ for (uint kj = 0u; kj < n_kv_tiles; ++kj) {
|
|
|
406
408
|
}
|
|
407
409
|
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
408
410
|
|
|
409
|
-
|
|
411
|
+
// Skip a KV tile only when it is entirely beyond this Q tile's last row
|
|
412
|
+
// (fully causally masked). The condition is UNIFORM across the threadgroup
|
|
413
|
+
// (q_tile_idx·BQ is the same for every thread), so the barrier below is hit
|
|
414
|
+
// by all threads — using the per-thread q_row_global here would diverge the
|
|
415
|
+
// barrier (undefined behavior). The per-element mask below still handles the
|
|
416
|
+
// partial diagonal tile.
|
|
417
|
+
if (is_causal && kv_base_row >= (q_tile_idx * BQ + BQ)) {
|
|
410
418
|
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
411
419
|
continue;
|
|
412
420
|
}
|
|
@@ -533,16 +541,12 @@ export function flashBackward(
|
|
|
533
541
|
grid: [nQTiles * bq, Hq, B],
|
|
534
542
|
threadGroup: [bq, 1, 1],
|
|
535
543
|
});
|
|
536
|
-
//
|
|
537
|
-
// (
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
[B, Hkv, Tkv, D],
|
|
543
|
-
);
|
|
544
|
-
dkv[0]!.dispose();
|
|
545
|
-
return [dq[0]!, dkFixed, dkv[1]!];
|
|
544
|
+
// dK is written by the kernel in the correct [B,Hkv,Tkv,D] layout
|
|
545
|
+
// (dK_j[d] → [kv_row][d]); no post-transpose. (An earlier "fix" transposed
|
|
546
|
+
// it on the false belief the buffer was swapped — that transpose is only the
|
|
547
|
+
// identity at Tkv==D and corrupted dK for Tkv≠D; removed after
|
|
548
|
+
// scripts/experiments/flash-dkv-debug.ts showed the raw output is correct.)
|
|
549
|
+
return [dq[0]!, dkv[0]!, dkv[1]!];
|
|
546
550
|
} finally {
|
|
547
551
|
Dvec.dispose();
|
|
548
552
|
shape.dispose();
|
|
@@ -607,11 +611,25 @@ export function flashAttention(
|
|
|
607
611
|
}
|
|
608
612
|
|
|
609
613
|
// ---------------------------------------------------------------------------
|
|
610
|
-
// Training-attention routing
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
614
|
+
// Training-attention routing (opt-in; default null = ops.sdpa).
|
|
615
|
+
//
|
|
616
|
+
// Parity oracle, made explicit:
|
|
617
|
+
// - DEFAULT (ops.sdpa) is the L1 path: mlx-lm's tuner differentiates the same
|
|
618
|
+
// mx.fast.scaled_dot_product_attention, so training through it is bit-faithful
|
|
619
|
+
// to mlx-lm. Exact dQ/dK/dV (finite-difference cross-checked), fast, but
|
|
620
|
+
// O(L²) backward memory (materializes the score matrix).
|
|
621
|
+
// - This flash kernel is the L2 path: a port of mlx-optiq's
|
|
622
|
+
// flash_attention_metal (O(L) backward memory). Its parity oracle is OPTIQ,
|
|
623
|
+
// not finite differences. Confirmed: optiq's flash dK == ops.sdpa to f16
|
|
624
|
+
// (scripts/flash-optiq-check.py, rel 0.0%), and mlx-bun's flash == ops.sdpa
|
|
625
|
+
// (tests/flash-attention.test.ts) ⟹ mlx-bun flash == optiq flash (L2 parity).
|
|
626
|
+
//
|
|
627
|
+
// Two real port bugs were fixed to reach that parity (the original diverged
|
|
628
|
+
// ~100% from optiq on dK): a spurious dK transpose in flashBackward (corrupted
|
|
629
|
+
// dK for Tkv≠D), and a per-thread (divergent) threadgroup_barrier in the dQ
|
|
630
|
+
// causal tile-skip. flash is opt-in (MLX_BUN_TRAIN_ATTN=flash) for memory-bound
|
|
631
|
+
// long context; it is ~30× slower than ops.sdpa. flashSupported gates
|
|
632
|
+
// head_dim/seq; sliding-window (array-mask) layers fall back to manualSdpa.
|
|
615
633
|
// ---------------------------------------------------------------------------
|
|
616
634
|
|
|
617
635
|
let trainAttnMode: "flash" | "manual" | null = null;
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import { MlxArray } from "../mlx/array";
|
|
17
17
|
import { Dtype } from "../mlx/ffi";
|
|
18
18
|
import { MetalKernel } from "../mlx/metal-kernel";
|
|
19
|
+
import { CustomVjp } from "../mlx/custom-vjp";
|
|
20
|
+
import * as ops from "../mlx/ops";
|
|
19
21
|
|
|
20
22
|
/** ON by default: the match-compat kernel is BIT-EXACT with the spelled-out
|
|
21
23
|
* path (kl --decode = 0 on e4b), so per the bit-parity-floor principle it
|
|
@@ -28,6 +30,24 @@ export function fusedGeluEnabled(): boolean {
|
|
|
28
30
|
/** Dispatch counter (a gate test asserts the kernel actually ran). */
|
|
29
31
|
export let fusedGeluCalls = 0;
|
|
30
32
|
|
|
33
|
+
// Training-mode flag: when set, model GeGLU sites use the DIFFERENTIABLE wrapper
|
|
34
|
+
// (Metal-kernel forward + hand-derived vjp) instead of the plain `fusedGeglu`
|
|
35
|
+
// (a bare CustomKernel with no vjp — feeding it to autograd throws
|
|
36
|
+
// "[Primitive::vjp] Not implemented for CustomKernel"). The differentiable
|
|
37
|
+
// wrapper also recomputes the gelu in the backward from the primal `a` instead
|
|
38
|
+
// of retaining the ~9 forward intermediates, so it lowers the MLP activation
|
|
39
|
+
// memory the backward holds — the dominant resident at long context under
|
|
40
|
+
// ops.sdpa (which never materializes the O(T²) scores). Set by the trainer
|
|
41
|
+
// around the train loop; cleared in its finally. Off → inference behaviour
|
|
42
|
+
// (plain `fusedGeglu`) is byte-for-byte unchanged.
|
|
43
|
+
let _trainingGeglu = false;
|
|
44
|
+
export function setFusedGeluTraining(on: boolean): void {
|
|
45
|
+
_trainingGeglu = on;
|
|
46
|
+
}
|
|
47
|
+
export function fusedGeluTraining(): boolean {
|
|
48
|
+
return _trainingGeglu;
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
// Pointwise: one thread per element. gelu_approx =
|
|
32
52
|
// 0.5 x (1 + tanh(√(2/π)·(x + 0.044715 x³))) — composed in f32.
|
|
33
53
|
const SOURCE = String.raw`
|
|
@@ -85,3 +105,94 @@ export function fusedGeglu(a: MlxArray, b: MlxArray): MlxArray {
|
|
|
85
105
|
});
|
|
86
106
|
return out!;
|
|
87
107
|
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Differentiable wrapper: Metal kernel forward + pure-MLX backward
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
// GELU tanh-approx constants
|
|
114
|
+
const SQRT_2_PI = 0.7978845608028654;
|
|
115
|
+
const C_CUBE = 0.044715;
|
|
116
|
+
|
|
117
|
+
/** Compute gelu_approx(a) = 0.5 * a * (1 + tanh(sqrt(2/pi) * (a + 0.044715*a^3)))
|
|
118
|
+
* and gelu'(a) using the MLX ops layer. All intermediates are disposed
|
|
119
|
+
* before returning; caller owns the two returned arrays. */
|
|
120
|
+
function geluAndGrad(a: MlxArray): { gelu: MlxArray; geluGrad: MlxArray } {
|
|
121
|
+
// a^3 = a * a * a
|
|
122
|
+
const a2 = ops.mul(a, a);
|
|
123
|
+
const a3 = ops.mul(a2, a);
|
|
124
|
+
// inner = a + 0.044715 * a^3
|
|
125
|
+
const cx3 = ops.mulScalar(a3, C_CUBE);
|
|
126
|
+
const inner = ops.add(a, cx3);
|
|
127
|
+
// z = sqrt(2/pi) * inner
|
|
128
|
+
const z = ops.mulScalar(inner, SQRT_2_PI);
|
|
129
|
+
// t = tanh(z)
|
|
130
|
+
const t = ops.tanh(z);
|
|
131
|
+
// gelu = 0.5 * a * (1 + t)
|
|
132
|
+
const one = ops.scalarLike(1, a);
|
|
133
|
+
const t1 = ops.add(one, t);
|
|
134
|
+
const halfA = ops.mulScalar(a, 0.5);
|
|
135
|
+
const gelu = ops.mul(halfA, t1);
|
|
136
|
+
|
|
137
|
+
// gelu'(a) = 0.5*(1+t) + 0.5*a*(1-t^2)*dz/da
|
|
138
|
+
// dz/da = sqrt(2/pi) * (1 + 3*0.044715*a^2)
|
|
139
|
+
// 1 - t^2 = sech^2(z)
|
|
140
|
+
const halfT1 = ops.mulScalar(t1, 0.5); // 0.5*(1+t)
|
|
141
|
+
const t2 = ops.mul(t, t); // t^2
|
|
142
|
+
const sech2 = ops.sub(one, t2); // 1 - t^2
|
|
143
|
+
const halfASech2 = ops.mul(halfA, sech2); // 0.5*a*(1-t^2)
|
|
144
|
+
// dz/da
|
|
145
|
+
const a2scaled = ops.mulScalar(a2, 3 * C_CUBE); // 3*0.044715*a^2
|
|
146
|
+
const dzda = ops.add(one, a2scaled); // 1 + 3*0.044715*a^2
|
|
147
|
+
const dzdaScaled = ops.mulScalar(dzda, SQRT_2_PI);// sqrt(2/pi)*dz_unnorm
|
|
148
|
+
// chain: 0.5*a*(1-t^2)*dz/da
|
|
149
|
+
const chainTerm = ops.mul(halfASech2, dzdaScaled);
|
|
150
|
+
const geluGrad = ops.add(halfT1, chainTerm); // 0.5*(1+t) + chain
|
|
151
|
+
|
|
152
|
+
// dispose all intermediates
|
|
153
|
+
for (const arr of [a2, a3, cx3, inner, z, t, one, t1, halfA, halfT1, t2, sech2,
|
|
154
|
+
halfASech2, a2scaled, dzda, dzdaScaled, chainTerm]) {
|
|
155
|
+
arr.dispose();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { gelu, geluGrad };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let _differentiableKernel: CustomVjp | null = null;
|
|
162
|
+
|
|
163
|
+
function getDifferentiableKernel(): CustomVjp {
|
|
164
|
+
if (!_differentiableKernel) {
|
|
165
|
+
_differentiableKernel = new CustomVjp(
|
|
166
|
+
// forward: use the Metal kernel
|
|
167
|
+
([a, b]: MlxArray[]) => [fusedGeglu(a!, b!)],
|
|
168
|
+
// backward
|
|
169
|
+
(primals: MlxArray[], cotangents: MlxArray[], _outputs: MlxArray[]) => {
|
|
170
|
+
const [a, b] = primals as [MlxArray, MlxArray];
|
|
171
|
+
const [dc] = cotangents as [MlxArray];
|
|
172
|
+
|
|
173
|
+
const { gelu, geluGrad } = geluAndGrad(a);
|
|
174
|
+
|
|
175
|
+
// grad_b = dc * gelu(a)
|
|
176
|
+
const grad_b = ops.mul(dc, gelu);
|
|
177
|
+
|
|
178
|
+
// grad_a = dc * b * gelu'(a)
|
|
179
|
+
const dcb = ops.mul(dc, b);
|
|
180
|
+
const grad_a = ops.mul(dcb, geluGrad);
|
|
181
|
+
|
|
182
|
+
gelu.dispose();
|
|
183
|
+
geluGrad.dispose();
|
|
184
|
+
dcb.dispose();
|
|
185
|
+
|
|
186
|
+
return [grad_a, grad_b];
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return _differentiableKernel;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** gelu_approx(a) · b with a hand-derived vjp. Uses the Metal kernel forward,
|
|
194
|
+
* MLX-ops backward. For training only (inference uses the non-differentiable
|
|
195
|
+
* fusedGeglu). */
|
|
196
|
+
export function fusedGegluDifferentiable(a: MlxArray, b: MlxArray): MlxArray {
|
|
197
|
+
return getDifferentiableKernel().apply([a, b])[0]!;
|
|
198
|
+
}
|
package/src/model/gemma4-base.ts
CHANGED
|
@@ -46,6 +46,31 @@ export interface LoraWeights {
|
|
|
46
46
|
* (PLAN Phase 8 decision). Set/restored by generate(). */
|
|
47
47
|
export class LoraState {
|
|
48
48
|
active: string[] = [];
|
|
49
|
+
/** Training-only LoRA-input dropout. `rate` is the drop probability; `seed` is
|
|
50
|
+
* set per micro-step by the trainer and is CONSTANT across that step's forward
|
|
51
|
+
* and any recompute (segmented / gradient-checkpoint), so each layer's mask —
|
|
52
|
+
* keyed by (seed, the linear's dropoutId) — is reproduced exactly in the
|
|
53
|
+
* backward. `seed === null` (the default, and for inference) disables it. */
|
|
54
|
+
dropoutRate = 0;
|
|
55
|
+
dropoutSeed: number | null = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Inverted LoRA-input dropout, keyed by (seed, id) so it is deterministic —
|
|
59
|
+
* the same (seed, id, shape) reproduces the mask, which is what makes the
|
|
60
|
+
* segmented / gradient-checkpoint recompute correct. kept ⇒ x/(1-p),
|
|
61
|
+
* dropped ⇒ 0 (preserves the expectation). Caller owns the result. */
|
|
62
|
+
export function loraInputDropout(x: MlxArray, p: number, seed: number, id: number): MlxArray {
|
|
63
|
+
const key = ops.randomKey(BigInt(seed) * 100003n + BigInt(id));
|
|
64
|
+
const u = ops.randomUniform(x.shape, Dtype.float32, 0, 1, key); // [shape] in [0,1)
|
|
65
|
+
key.dispose();
|
|
66
|
+
const pArr = ops.scalarLike(p, u);
|
|
67
|
+
const keep = ops.less(pArr, u); // u > p ⇒ keep (bool [shape])
|
|
68
|
+
const scaleX = ops.scalarLike(1 / (1 - p), x);
|
|
69
|
+
const zeroX = ops.scalarLike(0, x);
|
|
70
|
+
const mask = ops.where(keep, scaleX, zeroX); // x.dtype
|
|
71
|
+
const xd = ops.mul(x, mask);
|
|
72
|
+
for (const a of [u, pArr, keep, scaleX, zeroX, mask]) a.dispose();
|
|
73
|
+
return xd;
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
export class QuantizedLinear {
|
|
@@ -53,6 +78,9 @@ export class QuantizedLinear {
|
|
|
53
78
|
adapters: Map<string, LoraWeights> | null = null;
|
|
54
79
|
/** Shared per-model active state (wired by AdapterManager.mount). */
|
|
55
80
|
loraState: LoraState | null = null;
|
|
81
|
+
/** Stable per-target index for keying training-only LoRA dropout (set by
|
|
82
|
+
* attachForTraining). Gives each adapted linear an independent dropout mask. */
|
|
83
|
+
dropoutId = 0;
|
|
56
84
|
|
|
57
85
|
constructor(
|
|
58
86
|
readonly w: MlxArray,
|
|
@@ -92,10 +120,19 @@ export class QuantizedLinear {
|
|
|
92
120
|
// cast form is what the adapters were trained behind.)
|
|
93
121
|
const st = this.loraState;
|
|
94
122
|
if (st && st.active.length > 0 && this.adapters && this.adapters.size > 0) {
|
|
123
|
+
// Training-only LoRA-input dropout (PEFT applies dropout to x before A;
|
|
124
|
+
// the base quantized path is untouched). Keyed by (seed, dropoutId) so the
|
|
125
|
+
// backward recompute reproduces the exact mask.
|
|
126
|
+
let xLora = x;
|
|
127
|
+
let xDrop: MlxArray | null = null;
|
|
128
|
+
if (st.dropoutSeed !== null && st.dropoutRate > 0) {
|
|
129
|
+
xDrop = loraInputDropout(x, st.dropoutRate, st.dropoutSeed, this.dropoutId);
|
|
130
|
+
xLora = xDrop;
|
|
131
|
+
}
|
|
95
132
|
for (const id of st.active) {
|
|
96
133
|
const lw = this.adapters.get(id);
|
|
97
134
|
if (!lw) continue;
|
|
98
|
-
const xa = ops.matmul(
|
|
135
|
+
const xa = ops.matmul(xLora, lw.a);
|
|
99
136
|
const z = ops.matmul(xa, lw.b);
|
|
100
137
|
xa.dispose();
|
|
101
138
|
const zs = ops.mulScalar(z, lw.scale);
|
|
@@ -105,6 +142,7 @@ export class QuantizedLinear {
|
|
|
105
142
|
out = disposing(out, ops.add(out, zc));
|
|
106
143
|
zc.dispose();
|
|
107
144
|
}
|
|
145
|
+
xDrop?.dispose();
|
|
108
146
|
}
|
|
109
147
|
return out;
|
|
110
148
|
}
|