mlx-bun 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -20
- package/docs/design/batching-perf-path.md +232 -0
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/omlx-adoption-map.md +90 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/segmented-backward-training.md +15 -0
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/ssd-kv-cold-tier.md +158 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +348 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +43 -9
- package/docs/reference/training.md +31 -12
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1077 -90
- package/src/config.ts +4 -1
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +15 -6
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +149 -18
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/kv-store.ts +240 -98
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/array.ts +21 -0
- package/src/mlx/ffi.ts +3 -0
- package/src/mlx/ops.ts +72 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/factory.ts +29 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +96 -7
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3-delta.ts +48 -1
- package/src/model/qwen3.ts +276 -0
- package/src/model/qwen3_5.ts +11 -2
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +38 -8
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/prompt-cache.ts +55 -9
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +219 -8
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +307 -83
- package/src/serve/generation-gateway.ts +156 -29
- package/src/server.ts +746 -73
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/ssd-cache.ts +213 -0
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/flash-cce.ts +81 -23
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +341 -40
- package/src/train/prefix-shared.ts +62 -11
- package/src/train/segmented.ts +215 -55
- package/src/train/trainer.ts +112 -38
- package/src/web/skills/memory/SKILL.md +40 -17
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Fused SwiGLU MLP expansion kernel (cpm5-generate-and-fuse-inference): compute
|
|
2
|
+
// hidden = silu(gate_proj(x)) · up_proj(x)
|
|
3
|
+
// in ONE Metal dispatch, reusing MLX's verbatim steel quantized BlockMMA GEMM
|
|
4
|
+
// (src/train/steel-qmm-header.ts — the same building block the ORPO flash-CCE
|
|
5
|
+
// head uses). Replaces the spelled-out path's two quantized_matmul dispatches +
|
|
6
|
+
// the gate/up/silu/product global-memory materialization (minicpm5.ts LlamaMLP):
|
|
7
|
+
// gate = qmm(x, Wg); up = qmm(x, Wu); silu = gate·σ(gate); hidden = silu·up
|
|
8
|
+
// down_proj is LEFT as a separate qmm so the `hidden` boundary stays bit-exact —
|
|
9
|
+
// the existing minicpm5 L1/L2 parity goldens (maxDiff===0) gate this path directly
|
|
10
|
+
// (their 6-token prompt prefill is an M>1 MLP call).
|
|
11
|
+
//
|
|
12
|
+
// WHERE THE WIN IS: prefill / M>1, where the BlockMMA GEMM is the right tool and
|
|
13
|
+
// the [M, intermediate] materialization is real. At decode (M=1) the matmuls are
|
|
14
|
+
// memory-bound GEMVs (reading Wg/Wu dominates), so the fused kernel only saves the
|
|
15
|
+
// per-token dispatch + a tiny [1,I] round-trip — the caller falls back to unfused
|
|
16
|
+
// below M_MIN. Benchmarks (prefill AND decode) decide the default.
|
|
17
|
+
//
|
|
18
|
+
// PRECISION (two variants, env-flagged — "flag and try both"):
|
|
19
|
+
// - MATCH (default): T = the activation dtype (bf16/f16). The gate/up tiles are
|
|
20
|
+
// stored bf16-rounded out of the float BlockMMA accumulator, exactly as MLX's
|
|
21
|
+
// quantized_matmul casts its float accum to the input dtype; σ/·/· then run
|
|
22
|
+
// op-for-op with ops.sigmoid+ops.mul+ops.mul on bf16. AIM: maxDiff===0 vs the
|
|
23
|
+
// unfused path (like the fused-gelu kernel, kl=0). Only residual: the metal
|
|
24
|
+
// exp() vs MLX's math lib — quantified by the parity test.
|
|
25
|
+
// - F32 (MLX_BUN_FUSED_SWIGLU_F32=1): keep gate/up/silu in f32 through the fusion,
|
|
26
|
+
// round to T only at the final store. More accurate, NOT bit-exact → L3, gated
|
|
27
|
+
// by teacher-forced KL like the fused-decode kernel.
|
|
28
|
+
//
|
|
29
|
+
// Never inside a compiled-decode trace: a CustomKernel has no output_shapes (same
|
|
30
|
+
// rule as fusedDecodeSdpa / fusedGeglu) — the caller guards on !isCompiledTrace().
|
|
31
|
+
|
|
32
|
+
import { MlxArray } from "../mlx/array";
|
|
33
|
+
import { Dtype } from "../mlx/ffi";
|
|
34
|
+
import { MetalKernel } from "../mlx/metal-kernel";
|
|
35
|
+
import { STEEL_QMM_HEADER } from "../train/steel-qmm-header";
|
|
36
|
+
import * as ops from "../mlx/ops";
|
|
37
|
+
import type { QuantSpec } from "../mlx/ops";
|
|
38
|
+
|
|
39
|
+
/** OFF by default until the prefill/decode benchmark + parity delta are in (then
|
|
40
|
+
* flip to on like FUSED_GELU if it lands bit-exact). MLX_BUN_FUSED_SWIGLU=1 opts in. */
|
|
41
|
+
export function fusedSwigluEnabled(): boolean {
|
|
42
|
+
return process.env.MLX_BUN_FUSED_SWIGLU === "1";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** f32-internal variant (NOT bit-exact). Default off → the bit-exact MATCH path. */
|
|
46
|
+
function f32Internal(): boolean {
|
|
47
|
+
return process.env.MLX_BUN_FUSED_SWIGLU_F32 === "1";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Dispatch counter — a gate test asserts the kernel actually ran. */
|
|
51
|
+
export let fusedSwigluCalls = 0;
|
|
52
|
+
|
|
53
|
+
/** Debug bisect: cap how many fused calls actually run (rest fall back). */
|
|
54
|
+
function maxCalls(): number {
|
|
55
|
+
const v = Number(process.env.MLX_BUN_SWIGLU_MAXCALLS);
|
|
56
|
+
return Number.isFinite(v) && v >= 0 ? v : Infinity;
|
|
57
|
+
}
|
|
58
|
+
export function swigluUnderCap(): boolean {
|
|
59
|
+
return fusedSwigluCalls < maxCalls();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Below this many rows (M) the matmuls are memory-bound GEMVs and the fusion win
|
|
63
|
+
// is just dispatch overhead; the caller keeps the unfused path. Tunable via env
|
|
64
|
+
// for the decode-vs-prefill sweep.
|
|
65
|
+
export function swigluMMin(): number {
|
|
66
|
+
const v = Number(process.env.MLX_BUN_FUSED_SWIGLU_MMIN);
|
|
67
|
+
return Number.isFinite(v) && v > 0 ? v : 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const BM = 32, BN = 32, BK = 32, WM = 2, WN = 2;
|
|
71
|
+
const TGN = WM * WN * 32; // 128 threads / threadgroup
|
|
72
|
+
|
|
73
|
+
// One BM×BN output tile per threadgroup (grid.y = I-tiles, grid.z = M-tiles), the
|
|
74
|
+
// K=H reduction looped inside. Xs is loaded once and fed to BOTH the gate and up
|
|
75
|
+
// BlockMMA (the whole point of fusing — x is read from global once for two GEMMs).
|
|
76
|
+
const SOURCE = String.raw`
|
|
77
|
+
// M from the x_shape helper (varies per call); I, H BAKED as template literals
|
|
78
|
+
// (model-constant) — no runtime `+"`shape`"+` input array to thread through the
|
|
79
|
+
// lazy graph. The per-loader quant constants (GS_G/BITS_G gate, GS_U/BITS_U up)
|
|
80
|
+
// are baked too — the de-branch from the known per-tensor mixed-precision config,
|
|
81
|
+
// so the gate=4/up=8 layers fuse instead of falling back.
|
|
82
|
+
const uint M=x_shape[0]; constexpr int I=I_T, H=H_T;
|
|
83
|
+
constexpr int gsG=GS_G, btsG=BITS_G, gsU=GS_U, btsU=BITS_U;
|
|
84
|
+
constexpr int pfG=get_pack_factor<btsG,8>(), bppG=get_bytes_per_pack<btsG>();
|
|
85
|
+
constexpr int pfU=get_pack_factor<btsU,8>(), bppU=get_bytes_per_pack<btsU>();
|
|
86
|
+
constexpr int BKp=BK_T+16/sizeof(T);
|
|
87
|
+
threadgroup T Xs[BM_T*BKp];
|
|
88
|
+
threadgroup T Wgs[BN_T*BKp];
|
|
89
|
+
threadgroup T Wus[BN_T*BKp];
|
|
90
|
+
threadgroup T Lg[BM_T*BN_T];
|
|
91
|
+
threadgroup T Lu[BM_T*BN_T];
|
|
92
|
+
using mma_t=mlx::steel::BlockMMA<T,T,BM_T,BN_T,BK_T,WM_T,WN_T,false,true,BKp,BKp>;
|
|
93
|
+
using lx_t=mlx::steel::BlockLoader<T,BM_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE>;
|
|
94
|
+
using lwg_t=QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gsG,btsG>;
|
|
95
|
+
using lwu_t=QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gsU,btsU>;
|
|
96
|
+
const uint sg=simdgroup_index_in_threadgroup, sl=thread_index_in_simdgroup;
|
|
97
|
+
const uint lid=thread_position_in_threadgroup.x;
|
|
98
|
+
const int Hh=(int)H;
|
|
99
|
+
const int K_wG=Hh*bppG/pfG, K_gG=Hh/gsG; // gate: packed-row bytes, group count
|
|
100
|
+
const int K_wU=Hh*bppU/pfU, K_gU=Hh/gsU; // up
|
|
101
|
+
const int m0=(int)threadgroup_position_in_grid.z*BM_T;
|
|
102
|
+
const int n0=(int)threadgroup_position_in_grid.y*BN_T;
|
|
103
|
+
const short num_els=(short)min(BM_T,(int)M-m0);
|
|
104
|
+
const short num_outs=(short)min(BN_T,(int)I-n0);
|
|
105
|
+
|
|
106
|
+
#if SWIGLU_TGZERO
|
|
107
|
+
// Diagnostic: zero threadgroup memory so a back-to-back dispatch can't read the
|
|
108
|
+
// previous kernel's leftover staging (the cross-dispatch hazard hypothesis).
|
|
109
|
+
for (uint z=lid; z<(uint)(BM_T*BKp); z+=TGN_T) Xs[z]=T(0);
|
|
110
|
+
for (uint z=lid; z<(uint)(BN_T*BKp); z+=TGN_T){ Wgs[z]=T(0); Wus[z]=T(0); }
|
|
111
|
+
for (uint z=lid; z<(uint)(BM_T*BN_T); z+=TGN_T){ Lg[z]=T(0); Lu[z]=T(0); }
|
|
112
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
113
|
+
#endif
|
|
114
|
+
mma_t mg(sg,sl), mu(sg,sl);
|
|
115
|
+
lx_t lx (x + (int64_t)m0*Hh, Hh, Xs, sg, sl);
|
|
116
|
+
lwg_t lwg((const device uint8_t*)wg + (int64_t)n0*K_wG, sg_w + (int64_t)n0*K_gG, bg_w + (int64_t)n0*K_gG, Hh, Wgs, sg, sl);
|
|
117
|
+
lwu_t lwu((const device uint8_t*)wu + (int64_t)n0*K_wU, su_w + (int64_t)n0*K_gU, bu_w + (int64_t)n0*K_gU, Hh, Wus, sg, sl);
|
|
118
|
+
|
|
119
|
+
for (int k=0; k<Hh; k+=BK_T){
|
|
120
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
121
|
+
if (num_els<BM_T) lx.load_safe(short2(BK_T,num_els)); else lx.load_unsafe();
|
|
122
|
+
if (num_outs<BN_T){ lwg.load_safe(short2(BK_T,num_outs)); lwu.load_safe(short2(BK_T,num_outs)); }
|
|
123
|
+
else { lwg.load_unsafe(); lwu.load_unsafe(); }
|
|
124
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
125
|
+
mg.mma(Xs,Wgs);
|
|
126
|
+
mu.mma(Xs,Wus);
|
|
127
|
+
lx.next(); lwg.next(); lwu.next();
|
|
128
|
+
}
|
|
129
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
130
|
+
// Stage both accumulators to threadgroup. store<T,...> casts the float accum to
|
|
131
|
+
// T (bf16) — the SAME rounding MLX's quantized_matmul applies to its output, so
|
|
132
|
+
// Lg/Lu hold the bf16 gate/up the unfused path would have written to global.
|
|
133
|
+
// Xs/Wgs/Wus alias Lg/Lu only if a kernel reused the same tgmem — they don't here,
|
|
134
|
+
// but the stores write threadgroup that the epilogue reads, so bracket with barriers.
|
|
135
|
+
mg.Ctile.template store<T,WM_T,WN_T,BN_T,1>(Lg + mg.sm*BN_T + mg.sn);
|
|
136
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
137
|
+
mu.Ctile.template store<T,WM_T,WN_T,BN_T,1>(Lu + mu.sm*BN_T + mu.sn);
|
|
138
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
139
|
+
|
|
140
|
+
for (uint idx=lid; idx<(uint)(BM_T*BN_T); idx+=TGN_T){
|
|
141
|
+
int r=(int)idx/BN_T, c=(int)idx%BN_T;
|
|
142
|
+
if (m0+r>=(int)M || n0+c>=(int)I) continue;
|
|
143
|
+
#if SWIGLU_DEBUG_GATE
|
|
144
|
+
hidden[(int64_t)(m0+r)*(int64_t)I + n0+c] = Lg[idx]; // raw gate proj (debug)
|
|
145
|
+
#elif SWIGLU_F32
|
|
146
|
+
// f32-internal: gate/up stayed bf16 in Lg/Lu (the matmul output is the same),
|
|
147
|
+
// but σ·/· run in f32 with no intermediate rounding → NOT bit-exact.
|
|
148
|
+
float g=(float)Lg[idx], u=(float)Lu[idx];
|
|
149
|
+
float sig=1.0f/(1.0f+metal::precise::exp(-g));
|
|
150
|
+
hidden[(int64_t)(m0+r)*(int64_t)I + n0+c] = (T)(g*sig*u);
|
|
151
|
+
#else
|
|
152
|
+
// MATCH: round σ, silu, product to T op-for-op with ops.sigmoid+mul+mul on bf16.
|
|
153
|
+
T gT=Lg[idx], uT=Lu[idx];
|
|
154
|
+
T sigT=(T)(1.0f/(1.0f+metal::precise::exp(-(float)gT)));
|
|
155
|
+
T siluT=(T)((float)gT*(float)sigT);
|
|
156
|
+
hidden[(int64_t)(m0+r)*(int64_t)I + n0+c] = (T)((float)siluT*(float)uT);
|
|
157
|
+
#endif
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
const _kernels = new Map<string, MetalKernel>();
|
|
162
|
+
function getKernel(gsG: number, btsG: number, gsU: number, btsU: number, f32: boolean): MetalKernel {
|
|
163
|
+
const key = `${gsG}:${btsG}:${gsU}:${btsU}:${f32 ? 1 : 0}`;
|
|
164
|
+
let k = _kernels.get(key);
|
|
165
|
+
if (!k) {
|
|
166
|
+
k = new MetalKernel({
|
|
167
|
+
name: "mlx_bun_fused_swiglu",
|
|
168
|
+
inputNames: ["x", "wg", "sg_w", "bg_w", "wu", "su_w", "bu_w"],
|
|
169
|
+
outputNames: ["hidden"],
|
|
170
|
+
source: SOURCE,
|
|
171
|
+
header:
|
|
172
|
+
`${STEEL_QMM_HEADER}\n` +
|
|
173
|
+
`#define GS_G ${gsG}\n#define BITS_G ${btsG}\n#define GS_U ${gsU}\n#define BITS_U ${btsU}\n` +
|
|
174
|
+
`#define BM_T ${BM}\n#define BN_T ${BN}\n#define BK_T ${BK}\n` +
|
|
175
|
+
`#define WM_T ${WM}\n#define WN_T ${WN}\n#define TGN_T ${TGN}\n` +
|
|
176
|
+
`#define SWIGLU_F32 ${f32 ? 1 : 0}\n` +
|
|
177
|
+
`#define SWIGLU_TGZERO ${process.env.MLX_BUN_SWIGLU_TGZERO === "1" ? 1 : 0}\n` +
|
|
178
|
+
`#define SWIGLU_DEBUG_GATE ${process.env.MLX_BUN_SWIGLU_DEBUG_GATE === "1" ? 1 : 0}\n`,
|
|
179
|
+
ensureRowContiguous: true,
|
|
180
|
+
});
|
|
181
|
+
_kernels.set(key, k);
|
|
182
|
+
}
|
|
183
|
+
return k;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Each loader's (bits, groupSize) is baked independently, so gate and up may
|
|
187
|
+
* differ (the per-tensor mixed-precision layers). Requires bf16/f16 activations,
|
|
188
|
+
* affine biases, group size a multiple of BK, and H % BK == 0. */
|
|
189
|
+
export function fusedSwigluSupported(
|
|
190
|
+
x: MlxArray, gateSpec: QuantSpec, upSpec: QuantSpec,
|
|
191
|
+
gateBias: MlxArray | null, upBias: MlxArray | null,
|
|
192
|
+
): boolean {
|
|
193
|
+
const H = x.shape[x.shape.length - 1]!;
|
|
194
|
+
const specOk = (s: QuantSpec) =>
|
|
195
|
+
(s.bits === 2 || s.bits === 4 || s.bits === 8) && s.groupSize % BK === 0 && BK <= s.groupSize;
|
|
196
|
+
return (
|
|
197
|
+
(x.dtype === Dtype.bfloat16 || x.dtype === Dtype.float16) &&
|
|
198
|
+
gateBias != null && upBias != null &&
|
|
199
|
+
specOk(gateSpec) && specOk(upSpec) &&
|
|
200
|
+
H % BK === 0
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** silu(gate_proj(x)) · up_proj(x), fused. `x` is [..., H] (collapsed to [M, H]);
|
|
205
|
+
* wg/wu are the packed quantized weights [I, H*bits/32], with per-group
|
|
206
|
+
* scales/biases [I, H/groupSize]. gate and up may use different quant specs.
|
|
207
|
+
* Returns hidden [M, I] in x.dtype. Caller owns it. */
|
|
208
|
+
export function fusedSwiglu(
|
|
209
|
+
x: MlxArray,
|
|
210
|
+
wg: MlxArray, sg: MlxArray, bg: MlxArray,
|
|
211
|
+
wu: MlxArray, su: MlxArray, bu: MlxArray,
|
|
212
|
+
gateSpec: QuantSpec, upSpec: QuantSpec = gateSpec,
|
|
213
|
+
): MlxArray {
|
|
214
|
+
fusedSwigluCalls++;
|
|
215
|
+
const H = x.shape[x.shape.length - 1]!;
|
|
216
|
+
const M = x.shape.reduce((a, b) => a * b, 1) / H;
|
|
217
|
+
const I = wg.shape[0]!;
|
|
218
|
+
const x2 = x.shape.length === 2 ? x : ops.reshape(x, [M, H]);
|
|
219
|
+
try {
|
|
220
|
+
const [hidden] = getKernel(gateSpec.groupSize, gateSpec.bits, upSpec.groupSize, upSpec.bits, f32Internal()).apply(
|
|
221
|
+
[x2, wg, sg, bg, wu, su, bu],
|
|
222
|
+
{
|
|
223
|
+
outputShapeFn: (ins) => [{ shape: [ins[0]!.shape[0]!, I], dtype: x.dtype }],
|
|
224
|
+
grid: (ins) => [TGN, Math.ceil(I / BN), Math.ceil(ins[0]!.shape[0]! / BM)],
|
|
225
|
+
threadGroup: [TGN, 1, 1],
|
|
226
|
+
templateInts: { I_T: I, H_T: H },
|
|
227
|
+
templateDtypes: { T: x.dtype },
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
// Restore the leading dims the caller passed (e.g. [B, T, I]).
|
|
231
|
+
if (x.shape.length !== 2) {
|
|
232
|
+
const out = ops.reshape(hidden!, [...x.shape.slice(0, -1), I]);
|
|
233
|
+
hidden!.dispose();
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
return hidden!;
|
|
237
|
+
} finally {
|
|
238
|
+
if (x2 !== x && process.env.MLX_BUN_SWIGLU_NODISPOSE !== "1") x2.dispose();
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/model/gemma4-base.ts
CHANGED
|
@@ -87,6 +87,11 @@ export class QuantizedLinear {
|
|
|
87
87
|
readonly scales: MlxArray,
|
|
88
88
|
readonly biases: MlxArray | null,
|
|
89
89
|
readonly spec: ops.QuantSpec,
|
|
90
|
+
/** ADDITIVE bias term (`.bias`, mlx nn.QuantizedLinear's optional bias —
|
|
91
|
+
* qwen2 qkv, starcoder2, …). Distinct from `biases`, the quantization
|
|
92
|
+
* zero-points. Applied after the matmul, before any LoRA residual,
|
|
93
|
+
* exactly like mlx's QuantizedLinear.__call__. */
|
|
94
|
+
readonly bias: MlxArray | null = null,
|
|
90
95
|
) {}
|
|
91
96
|
|
|
92
97
|
static load(weights: Weights, path: string, config: ModelConfig): QuantizedLinear {
|
|
@@ -99,6 +104,7 @@ export class QuantizedLinear {
|
|
|
99
104
|
weights.tensor(`${path}.scales`),
|
|
100
105
|
weights.has(`${path}.biases`) ? weights.tensor(`${path}.biases`) : null,
|
|
101
106
|
spec,
|
|
107
|
+
weights.has(`${path}.bias`) ? weights.tensor(`${path}.bias`) : null,
|
|
102
108
|
);
|
|
103
109
|
}
|
|
104
110
|
|
|
@@ -113,6 +119,9 @@ export class QuantizedLinear {
|
|
|
113
119
|
|
|
114
120
|
forward(x: MlxArray): MlxArray {
|
|
115
121
|
let out = ops.quantizedMatmul(x, this.w, this.scales, this.biases, this.spec, true);
|
|
122
|
+
// Additive bias (mlx QuantizedLinear: `x = x + self["bias"]`), before
|
|
123
|
+
// the LoRA residual (LoRALinear calls the base linear bias-inclusive).
|
|
124
|
+
if (this.bias) out = disposing(out, ops.add(out, this.bias));
|
|
116
125
|
// LoRA residual — composition is mlx-lm LoRALinear / optiq apply.py:
|
|
117
126
|
// y + (scale · ((x @ A) @ B)).astype(x.dtype)
|
|
118
127
|
// (optiq mount.py omits the astype, leaking the f32 residual into the
|
|
@@ -213,8 +222,11 @@ export interface Cache {
|
|
|
213
222
|
/** Can `trim(n)` drop the last n tokens? (Ring caches lose trimability
|
|
214
223
|
* once wrapped.) */
|
|
215
224
|
isTrimmable(): boolean;
|
|
216
|
-
/** Drop the last n tokens (future writes overwrite them).
|
|
217
|
-
|
|
225
|
+
/** Drop the last n tokens (future writes overwrite them). `bypass` (default
|
|
226
|
+
* false → unchanged behavior) skips the isTrimmable guard for the
|
|
227
|
+
* speculative-decode rollback case (trim the last n ≤ γ tips right after a
|
|
228
|
+
* >1 concat write); rotating caches then physically shrink the buffer tail. */
|
|
229
|
+
trim(n: number, bypass?: boolean): void;
|
|
218
230
|
dispose(): void;
|
|
219
231
|
}
|
|
220
232
|
|
|
@@ -578,6 +590,15 @@ export class QuantizedKVCache implements Cache {
|
|
|
578
590
|
this.offset = Math.max(0, this.offset - n);
|
|
579
591
|
}
|
|
580
592
|
|
|
593
|
+
/** Adopt persisted state (takes ownership of the triples' arrays) —
|
|
594
|
+
* the quantized twin of KVCache.restoreState (kv-store persistence). */
|
|
595
|
+
restoreState(keys: ops.QuantizedTensor, values: ops.QuantizedTensor, offset: number): void {
|
|
596
|
+
this.dispose();
|
|
597
|
+
this.keys = keys;
|
|
598
|
+
this.values = values;
|
|
599
|
+
this.offset = offset;
|
|
600
|
+
}
|
|
601
|
+
|
|
581
602
|
dispose(): void {
|
|
582
603
|
for (const a of this.state()) a.dispose();
|
|
583
604
|
this.keys = this.values = null;
|
|
@@ -749,10 +770,32 @@ export class RotatingKVCache implements Cache {
|
|
|
749
770
|
return this.offset < this.maxSize;
|
|
750
771
|
}
|
|
751
772
|
|
|
752
|
-
trim(n: number): void {
|
|
773
|
+
trim(n: number, bypass = false): void {
|
|
753
774
|
const k = Math.min(this.offset, n);
|
|
775
|
+
if (!bypass) {
|
|
776
|
+
// mlx-lm semantics: only valid pre-wrap (guarded by isTrimmable upstream).
|
|
777
|
+
this.offset -= k;
|
|
778
|
+
this.#idx -= k;
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
// Spec-decode rollback after a >1 #updateConcat write: the buffer is in
|
|
782
|
+
// temporal order with #idx === bufferLen and the newest tokens are the last
|
|
783
|
+
// rows, so physically slice the last k off and keep #idx === new length.
|
|
784
|
+
// (Only sound right after a concat write with k ≤ γ — DSpark's only caller.)
|
|
754
785
|
this.offset -= k;
|
|
755
|
-
this
|
|
786
|
+
if (k > 0 && this.keys && this.values) {
|
|
787
|
+
const [B, H, S, D] = this.keys.shape as [number, number, number, number];
|
|
788
|
+
const vD = this.values.shape[3]!;
|
|
789
|
+
const nk = this.keys.slice([0, 0, 0, 0], [B, H, S - k, D]);
|
|
790
|
+
const nv = this.values.slice([0, 0, 0, 0], [B, H, S - k, vD]);
|
|
791
|
+
this.keys.dispose();
|
|
792
|
+
this.values.dispose();
|
|
793
|
+
this.keys = nk;
|
|
794
|
+
this.values = nv;
|
|
795
|
+
this.#idx = nk.shape[2]!;
|
|
796
|
+
} else {
|
|
797
|
+
this.#idx -= k;
|
|
798
|
+
}
|
|
756
799
|
}
|
|
757
800
|
|
|
758
801
|
get ringIdx(): number {
|
|
@@ -1100,10 +1143,42 @@ export class RotatingQuantizedKVCache implements Cache {
|
|
|
1100
1143
|
return this.offset < this.maxSize;
|
|
1101
1144
|
}
|
|
1102
1145
|
|
|
1103
|
-
trim(n: number): void {
|
|
1146
|
+
trim(n: number, bypass = false): void {
|
|
1104
1147
|
const k = Math.min(this.offset, n);
|
|
1148
|
+
if (!bypass) {
|
|
1149
|
+
this.offset -= k;
|
|
1150
|
+
this.ringIdx -= k;
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
// Spec-decode rollback after a >1 concat write — physically slice the last k
|
|
1154
|
+
// off each (packed, scales, biases) component and keep ringIdx === seqLen.
|
|
1105
1155
|
this.offset -= k;
|
|
1106
|
-
this.
|
|
1156
|
+
if (k > 0 && this.keys && this.values) {
|
|
1157
|
+
const S = this.#seqLen();
|
|
1158
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
1159
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1160
|
+
return a.slice([0, 0, 0, 0], [B, H, S - k, D]);
|
|
1161
|
+
};
|
|
1162
|
+
const nk = mapTriple(this.keys, cut);
|
|
1163
|
+
const nv = mapTriple(this.values, cut);
|
|
1164
|
+
disposeTriple(this.keys);
|
|
1165
|
+
disposeTriple(this.values);
|
|
1166
|
+
this.keys = nk;
|
|
1167
|
+
this.values = nv;
|
|
1168
|
+
this.ringIdx = this.#seqLen();
|
|
1169
|
+
} else {
|
|
1170
|
+
this.ringIdx -= k;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** Adopt persisted state (takes ownership of the triples' arrays) —
|
|
1175
|
+
* ring order as-laid-out, ringIdx carried with it (kv-store persistence). */
|
|
1176
|
+
restoreState(keys: ops.QuantizedTensor, values: ops.QuantizedTensor, offset: number, idx: number): void {
|
|
1177
|
+
this.dispose();
|
|
1178
|
+
this.keys = keys;
|
|
1179
|
+
this.values = values;
|
|
1180
|
+
this.offset = offset;
|
|
1181
|
+
this.ringIdx = idx;
|
|
1107
1182
|
}
|
|
1108
1183
|
|
|
1109
1184
|
/** Oracle: to_quantized on an already-quantized rotating cache is
|
|
@@ -1516,8 +1591,22 @@ export function quantizedSdpa(
|
|
|
1516
1591
|
)
|
|
1517
1592
|
return fusedDecodeSdpa(q, kq, vq, groupSize, bits);
|
|
1518
1593
|
const tile = q.shape[2]! > 1 || process.env.MLX_BUN_FUSED_DECODE === "1";
|
|
1519
|
-
if (tile && fusedSdpaSupported(q, mask, groupSize, bits))
|
|
1594
|
+
if (tile && fusedSdpaSupported(q, mask, groupSize, bits)) {
|
|
1595
|
+
// The N-tiled loop CANNOT be traced: its tile count and slice extents
|
|
1596
|
+
// bake at trace-time N, so under shapeless replay a growing (concat-
|
|
1597
|
+
// phase) quantized cache's newest rows are silently never attended —
|
|
1598
|
+
// reproduced on e4b (whole-graph form, FUSED_DECODE=1: diverges within
|
|
1599
|
+
// 3 tokens, then repetition-loops). generate() refuses to compile when
|
|
1600
|
+
// MLX_BUN_FUSED_DECODE=1; this throw is the backstop for any other
|
|
1601
|
+
// compiled caller — it lands in the trace's transactional fallback
|
|
1602
|
+
// instead of producing silently wrong attention.
|
|
1603
|
+
if (isCompiledTrace())
|
|
1604
|
+
throw new Error(
|
|
1605
|
+
"quantizedSdpaTiled inside a compiled trace: the tile loop bakes trace-time N " +
|
|
1606
|
+
"(growing KV rows would be silently dropped) — run this generation uncompiled",
|
|
1607
|
+
);
|
|
1520
1608
|
return quantizedSdpaTiled(q, kq, vq, scale, mask, groupSize, bits);
|
|
1609
|
+
}
|
|
1521
1610
|
return quantizedSdpaUnfused(q, kq, vq, scale, mask, groupSize, bits);
|
|
1522
1611
|
}
|
|
1523
1612
|
|
package/src/model/gemma4.ts
CHANGED
|
@@ -295,7 +295,14 @@ class Attention {
|
|
|
295
295
|
} else if (
|
|
296
296
|
ta === "flash" && flashSupported(q) && (mask.mode === "causal" || mask.mode === "array")
|
|
297
297
|
) {
|
|
298
|
-
// Training
|
|
298
|
+
// Training, OPT-IN flash path (MLX_BUN_TRAIN_ATTN=flash) for O(L)-memory
|
|
299
|
+
// long context. NOTE the default (ops.sdpa, below) is the CORRECT and
|
|
300
|
+
// verified route: its dQ/dK/dV vjps are exact (finite-difference checked;
|
|
301
|
+
// it's what mlx-lm's tuner differentiates). The two historical bugs were
|
|
302
|
+
// in THIS flash kernel (spurious dK transpose, divergent dQ barrier),
|
|
303
|
+
// both fixed and gated by tests/flash-attention.test.ts vs the ops.sdpa
|
|
304
|
+
// oracle — see flash-attention.ts's routing comment. Flash also remains
|
|
305
|
+
// Gemma-guarded in the trainer (e4b >=2K SIGTRAP not yet re-validated).
|
|
299
306
|
// Full layers → window 0 (pure causal); sliding layers → their window.
|
|
300
307
|
const window = this.isSliding ? this.windowSize : 0;
|
|
301
308
|
attn = flashAttention(q, shared.keys, shared.values, 1.0, true, window);
|
|
@@ -663,6 +670,13 @@ export class Gemma4Model {
|
|
|
663
670
|
/** Set by the trainer to enable gradient checkpointing in forwardLayers
|
|
664
671
|
* (null = off; the non-checkpointed path is untouched). */
|
|
665
672
|
gradCkpt: GradCheckpointCtx | null = null;
|
|
673
|
+
/** Optional per-forward hidden-state tap for DSpark's DFlash H_ctx (paper
|
|
674
|
+
* Eq 2). null = off → byte-identical forward. `layers`: residual-output
|
|
675
|
+
* indices to capture (0..nLayers-1 = post-layer residual; nLayers =
|
|
676
|
+
* post-finalNorm). `pos`: if set, capture only [1,1,H] at that seq position
|
|
677
|
+
* (live decode); else the full [1,L,H] (offline regen). The caller owns and
|
|
678
|
+
* disposes the captured arrays. Additive/read-only → parity untouched. */
|
|
679
|
+
hiddenTap: { layers: Set<number>; pos?: number; captured: Map<number, MlxArray> } | null = null;
|
|
666
680
|
// e2b/e4b per-layer input machinery
|
|
667
681
|
readonly perLayerEmbed: QuantizedEmbedding | null;
|
|
668
682
|
readonly perLayerModelProjection: QuantizedLinear | null;
|
|
@@ -820,6 +834,20 @@ export class Gemma4Model {
|
|
|
820
834
|
return out;
|
|
821
835
|
}
|
|
822
836
|
|
|
837
|
+
/** Store layer `i`'s residual stream `h` ([1,L,H]) if the hidden tap requests
|
|
838
|
+
* it. Copies (astype bf16, optional slice) so the loop's `h.dispose()` can't
|
|
839
|
+
* free the capture. No-op and no graph ops when the tap is null → the forward
|
|
840
|
+
* is byte-identical to an untapped run. (DSpark DFlash H_ctx.) */
|
|
841
|
+
protected captureLayer(i: number, h: MlxArray): void {
|
|
842
|
+
const tap = this.hiddenTap;
|
|
843
|
+
if (!tap || !tap.layers.has(i)) return;
|
|
844
|
+
const H = h.shape[2]!;
|
|
845
|
+
const src = tap.pos !== undefined ? h.slice([0, tap.pos, 0], [1, tap.pos + 1, H]) : h;
|
|
846
|
+
const copy = src.astype(this.embed.scales.dtype); // bf16, matches regen dump dtype
|
|
847
|
+
if (src !== h) src.dispose();
|
|
848
|
+
tap.captured.set(i, copy);
|
|
849
|
+
}
|
|
850
|
+
|
|
823
851
|
/** Consumes h. */
|
|
824
852
|
protected forwardLayers(
|
|
825
853
|
h0: MlxArray, cache: Cache[], bidir: MlxArray | null, ids: MlxArray | null,
|
|
@@ -878,6 +906,7 @@ export class Gemma4Model {
|
|
|
878
906
|
pls?.dispose();
|
|
879
907
|
h.dispose();
|
|
880
908
|
h = next;
|
|
909
|
+
this.captureLayer(i, h); // DFlash H_ctx tap (no-op unless hiddenTap set)
|
|
881
910
|
intermediates[i] = shared;
|
|
882
911
|
}
|
|
883
912
|
perLayer?.dispose();
|
|
@@ -895,7 +924,9 @@ export class Gemma4Model {
|
|
|
895
924
|
}
|
|
896
925
|
for (const m of masks.values()) m.arr?.dispose();
|
|
897
926
|
|
|
898
|
-
|
|
927
|
+
h = disposing(h, this.finalNorm.forward(h));
|
|
928
|
+
this.captureLayer(this.layers.length, h); // post-finalNorm sentinel (index = nLayers)
|
|
929
|
+
return h;
|
|
899
930
|
}
|
|
900
931
|
|
|
901
932
|
// --- Segmented-backward support (docs/design/segmented-backward-training.md
|
package/src/model/minicpm5.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { ModelConfig } from "../config";
|
|
5
5
|
import type { Weights } from "../weights";
|
|
6
6
|
import { MlxArray } from "../mlx/array";
|
|
7
|
+
import { Dtype } from "../mlx/ffi";
|
|
7
8
|
import * as ops from "../mlx/ops";
|
|
8
9
|
import {
|
|
9
10
|
argmaxLastPosition,
|
|
@@ -16,9 +17,56 @@ import {
|
|
|
16
17
|
QuantizedLinear,
|
|
17
18
|
quantizedSdpa,
|
|
18
19
|
RMSNorm,
|
|
20
|
+
isCompiledTrace,
|
|
19
21
|
type Cache,
|
|
20
22
|
type Mask,
|
|
21
23
|
} from "./gemma4-base";
|
|
24
|
+
import {
|
|
25
|
+
fusedSwiglu,
|
|
26
|
+
fusedSwigluEnabled,
|
|
27
|
+
fusedSwigluSupported,
|
|
28
|
+
swigluMMin,
|
|
29
|
+
swigluUnderCap,
|
|
30
|
+
} from "./fused-swiglu-kernel";
|
|
31
|
+
import { steelLinear } from "./steel-linear-kernel";
|
|
32
|
+
import { fusedMlp } from "./fused-mlp-kernel";
|
|
33
|
+
|
|
34
|
+
/** Break the graph at a boundary: contiguous → eval → fresh graph-free leaf — the
|
|
35
|
+
* exact primitive the segmented TRAINER uses so MLX never plans across boundaries
|
|
36
|
+
* and prior-segment activations are freed (copy of train/segmented.ts detachLeaf). */
|
|
37
|
+
function detachLeaf(a: MlxArray): MlxArray {
|
|
38
|
+
const c = ops.contiguous(a);
|
|
39
|
+
c.eval();
|
|
40
|
+
const leaf = MlxArray.fromBytesCopy(c.rawBytes(), c.shape, c.dtype);
|
|
41
|
+
c.dispose();
|
|
42
|
+
a.dispose();
|
|
43
|
+
return leaf;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A quantized projection as OUR steel dispatch when the controlled path is live
|
|
47
|
+
* (no adapter, affine, clean tiling, M>1 so it's not a decode GEMV); otherwise
|
|
48
|
+
* the normal linear. x is [B,L,H] or [M,H]. */
|
|
49
|
+
function ourLinear(lin: QuantizedLinear, x: MlxArray): MlxArray {
|
|
50
|
+
const H = x.shape[x.shape.length - 1]!;
|
|
51
|
+
const M = x.shape.reduce((a, b) => a * b, 1) / H;
|
|
52
|
+
if (
|
|
53
|
+
!fusedSwigluEnabled() || isCompiledTrace() || M < swigluMMin() ||
|
|
54
|
+
lin.adapters != null || lin.biases == null ||
|
|
55
|
+
(x.dtype !== Dtype.bfloat16 && x.dtype !== Dtype.float16) ||
|
|
56
|
+
H % 32 !== 0 || lin.spec.groupSize % 32 !== 0
|
|
57
|
+
) {
|
|
58
|
+
return lin.forward(x);
|
|
59
|
+
}
|
|
60
|
+
const x2 = x.shape.length === 2 ? x : ops.reshape(x, [M, H]);
|
|
61
|
+
const y = steelLinear(x2, lin.w, lin.scales, lin.biases, lin.spec);
|
|
62
|
+
if (x2 !== x) x2.dispose();
|
|
63
|
+
return x.shape.length === 2
|
|
64
|
+
? y
|
|
65
|
+
: disposing(y, ops.reshape(y, [...x.shape.slice(0, -1), lin.w.shape[0]!]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** How often the inference fused path materializes the residual stream (layers). */
|
|
69
|
+
const EVAL_EVERY = Math.max(1, Number(process.env.MLX_BUN_SWIGLU_EVAL_EVERY) || 1);
|
|
22
70
|
import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
|
|
23
71
|
|
|
24
72
|
// Shared prompt-prefix plan (lever 7). When set, the attention ropes the
|
|
@@ -156,7 +204,40 @@ class LlamaMLP {
|
|
|
156
204
|
this.down = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
|
|
157
205
|
}
|
|
158
206
|
|
|
159
|
-
|
|
207
|
+
/** MLP. When `residual` (the layer input h) is given AND the fused path applies,
|
|
208
|
+
* the kernel computes h + mlp(x) directly — the residual is ABSORBED, so nothing
|
|
209
|
+
* stays live across the kernel for MLX's planner to alias (it just runs our one
|
|
210
|
+
* dispatch). Otherwise returns mlp(x) and the caller adds the residual. */
|
|
211
|
+
forward(x: MlxArray, residual?: MlxArray): MlxArray {
|
|
212
|
+
const H = x.shape[x.shape.length - 1]!;
|
|
213
|
+
const M = x.shape.reduce((a, b) => a * b, 1) / H;
|
|
214
|
+
if (
|
|
215
|
+
residual != null &&
|
|
216
|
+
fusedSwigluEnabled() && !isCompiledTrace() && M >= swigluMMin() && swigluUnderCap() &&
|
|
217
|
+
this.gate.adapters == null && this.up.adapters == null && this.down.adapters == null &&
|
|
218
|
+
this.down.biases != null && this.down.spec.groupSize % 32 === 0 &&
|
|
219
|
+
fusedSwigluSupported(x, this.gate.spec, this.up.spec, this.gate.biases, this.up.biases)
|
|
220
|
+
) {
|
|
221
|
+
// The WHOLE MLP + residual in ONE kernel — gate+up+silu+down then +h, `hidden`
|
|
222
|
+
// never materialized, residual absorbed. MLX runs one dispatch; nothing for
|
|
223
|
+
// its planner to alias → chains across layers with no boundary.
|
|
224
|
+
const Hin = this.gate.scales.shape[1]! * this.gate.spec.groupSize;
|
|
225
|
+
const Ho = this.down.w.shape[0]!;
|
|
226
|
+
const x2 = x.shape.length === 2 ? x : ops.reshape(x, [M, Hin]);
|
|
227
|
+
const h2 = residual.shape.length === 2 ? residual : ops.reshape(residual, [M, Ho]);
|
|
228
|
+
const out2 = fusedMlp(
|
|
229
|
+
x2,
|
|
230
|
+
this.gate.w, this.gate.scales, this.gate.biases!, this.gate.spec,
|
|
231
|
+
this.up.w, this.up.scales, this.up.biases!, this.up.spec,
|
|
232
|
+
this.down.w, this.down.scales, this.down.biases!, this.down.spec,
|
|
233
|
+
h2,
|
|
234
|
+
);
|
|
235
|
+
if (x2 !== x) x2.dispose();
|
|
236
|
+
if (h2 !== residual) h2.dispose();
|
|
237
|
+
return x.shape.length === 2
|
|
238
|
+
? out2
|
|
239
|
+
: disposing(out2, ops.reshape(out2, [...x.shape.slice(0, -1), Ho]));
|
|
240
|
+
}
|
|
160
241
|
const gate = this.gate.forward(x);
|
|
161
242
|
const up = this.up.forward(x);
|
|
162
243
|
const sig = ops.sigmoid(gate);
|
|
@@ -166,9 +247,10 @@ class LlamaMLP {
|
|
|
166
247
|
const hidden = ops.mul(silu, up);
|
|
167
248
|
silu.dispose();
|
|
168
249
|
up.dispose();
|
|
169
|
-
const
|
|
250
|
+
const m = this.down.forward(hidden);
|
|
170
251
|
hidden.dispose();
|
|
171
|
-
return
|
|
252
|
+
if (residual == null) return m;
|
|
253
|
+
return disposing(m, ops.add(residual, m));
|
|
172
254
|
}
|
|
173
255
|
}
|
|
174
256
|
|
|
@@ -192,11 +274,10 @@ class LlamaLayer {
|
|
|
192
274
|
const h = ops.add(x, a);
|
|
193
275
|
a.dispose();
|
|
194
276
|
const hn = this.postAttnNorm.forward(h);
|
|
195
|
-
|
|
277
|
+
// Pass the residual h INTO the MLP so the fused kernel absorbs it (out = h + mlp).
|
|
278
|
+
const out = this.mlp.forward(hn, h);
|
|
196
279
|
hn.dispose();
|
|
197
|
-
const out = ops.add(h, m);
|
|
198
280
|
h.dispose();
|
|
199
|
-
m.dispose();
|
|
200
281
|
return out;
|
|
201
282
|
}
|
|
202
283
|
}
|
|
@@ -256,7 +337,15 @@ export class MiniCPM5Model {
|
|
|
256
337
|
}
|
|
257
338
|
|
|
258
339
|
protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
|
|
259
|
-
|
|
340
|
+
// When the fused SwiGLU path is live, WE own the eval schedule: materialize
|
|
341
|
+
// each layer's output so the custom kernel sits in a shallow per-layer graph
|
|
342
|
+
// rather than a 24-deep lazy graph whose buffer planner aliases the kernel's
|
|
343
|
+
// output (the deep-graph corruption). Training/segmented-backward call
|
|
344
|
+
// runLayerRange directly and never set this, so autograd is untouched.
|
|
345
|
+
const h = this.runLayerRange(
|
|
346
|
+
h0, 0, this.layers.length, cache,
|
|
347
|
+
fusedSwigluEnabled() && process.env.MLX_BUN_SWIGLU_NOEVAL !== "1",
|
|
348
|
+
);
|
|
260
349
|
h0.dispose(); // runLayerRange keeps its input alive; we own h0 here
|
|
261
350
|
return disposing(h, this.finalNorm.forward(h));
|
|
262
351
|
}
|
|
@@ -269,7 +358,7 @@ export class MiniCPM5Model {
|
|
|
269
358
|
* segmented-backward training path (docs/design/segmented-backward-training.md).
|
|
270
359
|
* MiniCPM5 is plain full-attention with no KV-sharing, so each layer reads
|
|
271
360
|
* its own stateless `cache[i]` and a single causal mask. */
|
|
272
|
-
runLayerRange(h: MlxArray, aIdx: number, bIdx: number, cache: Cache[]): MlxArray {
|
|
361
|
+
runLayerRange(h: MlxArray, aIdx: number, bIdx: number, cache: Cache[], evalPerLayer = false): MlxArray {
|
|
273
362
|
if (aIdx >= bIdx) return h;
|
|
274
363
|
const L = h.shape[1]!;
|
|
275
364
|
const mask = cache[aIdx]!.makeMask(L, null);
|
|
@@ -278,6 +367,15 @@ export class MiniCPM5Model {
|
|
|
278
367
|
const next = this.layers[i]!.forward(cur, mask, cache[i]!);
|
|
279
368
|
if (i > aIdx) cur.dispose(); // keep the input boundary leaf; drop interiors
|
|
280
369
|
cur = next;
|
|
370
|
+
// Per-layer boundary (segmented-trainer pattern): materialize each layer's
|
|
371
|
+
// output so MLX never plans across layers — our single-dispatch fused MLP is
|
|
372
|
+
// the only custom kernel it runs per layer. evalAll (GPU sync, no host copy)
|
|
373
|
+
// suffices for one kernel/boundary; MLX_BUN_SWIGLU_DETACH=1 forces the
|
|
374
|
+
// stronger graph-breaking detachLeaf copy.
|
|
375
|
+
if (evalPerLayer) {
|
|
376
|
+
if (process.env.MLX_BUN_SWIGLU_DETACH === "1") cur = detachLeaf(cur);
|
|
377
|
+
else ops.evalAll([cur]);
|
|
378
|
+
}
|
|
281
379
|
}
|
|
282
380
|
mask.arr?.dispose();
|
|
283
381
|
return cur;
|