mlx-bun 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -20
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/diffusion-gemma-port.md +602 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +262 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +40 -9
- package/docs/reference/training.md +25 -11
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1029 -86
- package/src/config.ts +54 -3
- package/src/diffusion/diffusion-generate.ts +436 -0
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +19 -7
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +196 -19
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/ffi.ts +11 -0
- package/src/mlx/ops.ts +113 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/diffusion-gemma.ts +1026 -0
- package/src/model/factory.ts +35 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +62 -6
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3.ts +276 -0
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +45 -7
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +215 -7
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +272 -80
- package/src/serve/generation-gateway.ts +90 -18
- package/src/server.ts +643 -50
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/diffusion-lora.ts +216 -0
- package/src/train/flash-cce.ts +70 -15
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +289 -30
- package/src/train/prefix-shared.ts +33 -5
- package/src/train/segmented.ts +67 -11
- package/src/train/trainer.ts +92 -28
- package/src/vision/diffusion-vision.ts +620 -0
- package/src/web/skills/memory/SKILL.md +40 -17
package/src/model/factory.ts
CHANGED
|
@@ -11,17 +11,44 @@ import { configFingerprint } from "./fingerprint";
|
|
|
11
11
|
import { GENERATED } from "./generated";
|
|
12
12
|
import { MiniCPM5Model } from "./minicpm5";
|
|
13
13
|
import { Qwen35Model } from "./qwen3_5";
|
|
14
|
-
import {
|
|
14
|
+
import { Qwen3Model } from "./qwen3";
|
|
15
|
+
import { DiffusionGemmaModel } from "./diffusion-gemma";
|
|
16
|
+
import { UniversalDenseModel } from "./universal/dense";
|
|
17
|
+
import { genericArgsFor, GENERIC_MODEL_TYPES, remapModelType } from "./universal/archs";
|
|
18
|
+
import { isDiffusionGemmaConfig, isMiniCPM5Config, isQwen35Config, isQwen3Config } from "./support";
|
|
15
19
|
|
|
16
|
-
export type RuntimeModel =
|
|
20
|
+
export type RuntimeModel =
|
|
21
|
+
| Gemma4Model | MiniCPM5Model | Qwen35Model | Qwen3Model | DiffusionGemmaModel
|
|
22
|
+
| UniversalDenseModel;
|
|
17
23
|
|
|
24
|
+
/** Dispatch ladder (docs/design/generic-model-support.md §3.3):
|
|
25
|
+
* dedicated → generated/monolith (gemma4*) → generic (Tier-0 universal)
|
|
26
|
+
* → reject with a helpful error. Generic never shadows a dedicated port. */
|
|
18
27
|
export function createModel(weights: Weights, config: ModelConfig): RuntimeModel {
|
|
28
|
+
// 1. Dedicated classes.
|
|
29
|
+
// DiffusionGemma is non-autoregressive — generate() detects it and routes to
|
|
30
|
+
// the denoising engine instead of the AR decode loop. It exposes the shared
|
|
31
|
+
// RuntimeModel surface (config/weightsBytes/loraState/makeCache) with the
|
|
32
|
+
// AR-only methods as throwing stubs (never called on this model).
|
|
33
|
+
if (isDiffusionGemmaConfig(config)) return new DiffusionGemmaModel(weights, config);
|
|
19
34
|
if (isMiniCPM5Config(config)) return new MiniCPM5Model(weights, config);
|
|
20
35
|
if (isQwen35Config(config)) return new Qwen35Model(weights, config);
|
|
21
|
-
if (config
|
|
22
|
-
|
|
23
|
-
if (config.modelType.startsWith("
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
if (isQwen3Config(config)) return new Qwen3Model(weights, config);
|
|
37
|
+
// 2. gemma4*: fingerprint-matched generated specialization, else monolith.
|
|
38
|
+
if (config.modelType.startsWith("gemma4")) {
|
|
39
|
+
const cls = GENERATED.get(configFingerprint(config)) ?? Gemma4Model;
|
|
40
|
+
return new cls(weights, config);
|
|
41
|
+
}
|
|
42
|
+
// 3. Generic Tier-0 fallback: the universal-dense descriptor table
|
|
43
|
+
// (replaces the old hard "llama = MiniCPM5 only" / unknown-type throws).
|
|
44
|
+
const generic = genericArgsFor(config);
|
|
45
|
+
if (generic) return new UniversalDenseModel(weights, config, generic);
|
|
46
|
+
// 4. Reject, naming the arch (post-remap) and the declared surface.
|
|
47
|
+
const arch = remapModelType(config.modelType);
|
|
48
|
+
throw new Error(
|
|
49
|
+
`unsupported model_type "${config.modelType}"` +
|
|
50
|
+
(arch !== config.modelType ? ` (mlx-lm remaps it to "${arch}")` : "") +
|
|
51
|
+
` — targeted: gemma4*, diffusion_gemma, qwen3_5, qwen3, MiniCPM5;` +
|
|
52
|
+
` generic (Tier-0): ${[...GENERIC_MODEL_TYPES].sort().join(", ")}`,
|
|
53
|
+
);
|
|
27
54
|
}
|
|
@@ -649,8 +649,10 @@ export function flashSupported(q: MlxArray): boolean {
|
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
/** Materialized differentiable attention (matmul → mask → softmax → matmul),
|
|
652
|
-
* GQA-aware.
|
|
653
|
-
*
|
|
652
|
+
* GQA-aware. A correct-vjp fallback for shapes/masks the flash kernel doesn't
|
|
653
|
+
* take. (ops.sdpa's dK vjp is CORRECT — FD-verified, the training default;
|
|
654
|
+
* the old "broken dK" note here predated the flash-kernel root-cause: the
|
|
655
|
+
* bugs were flash's own dK transpose + dQ barrier, both fixed.) O(L²) —
|
|
654
656
|
* pair with gradient checkpointing. */
|
|
655
657
|
export function manualSdpa(q: MlxArray, k: MlxArray, v: MlxArray, scale: number, mask: Mask): MlxArray {
|
|
656
658
|
const [B, Hq, T, Dh] = q.shape as [number, number, number, number];
|
|
@@ -26,13 +26,20 @@ import { Dtype } from "../mlx/ffi";
|
|
|
26
26
|
import { MetalKernel } from "../mlx/metal-kernel";
|
|
27
27
|
import * as ops from "../mlx/ops";
|
|
28
28
|
|
|
29
|
-
/** Perf-mode lever: DEFAULT ON (2026-06-11 cleared-machine pass, ~1.02x @8k)
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
/** Perf-mode lever: DEFAULT ON (2026-06-11 cleared-machine pass, ~1.02x @8k)
|
|
30
|
+
* — but an L3/explicit-only lever, OFF in the bare --l2 tier. This is an
|
|
31
|
+
* mlx-bun ORIGINAL kernel, NOT a port of optiq's fused decode, and the
|
|
32
|
+
* quantized-KV parity goldens do NOT track it: the optiq-golden decode
|
|
33
|
+
* composition is stock UNFUSED L=1 (scripts/regen-kvq-goldens.ts — the fused
|
|
34
|
+
* N-tiled SDPA covers L>1 prefill only, where this kernel never dispatches).
|
|
35
|
+
* It is NOT bit-exact vs the -O0 reference (online softmax, tier-b); its gate
|
|
36
|
+
* is ENVELOPE quality — ≥56/64 teacher-forced argmax agreement vs mlx-bun's
|
|
37
|
+
* own frozen compat trajectory (tests/perf-kernel-oracle.test.ts,
|
|
38
|
+
* scripts/freeze-perf-oracle.ts) plus bounded divergence vs the unfused port
|
|
39
|
+
* (tests/fused-decode-kernel.test.ts). MLX_BUN_PERF_KERNEL=0 selects the
|
|
40
|
+
* compat path we bench against the python libs. (B/C-style bit-exact
|
|
41
|
+
* optimizations stay on either way — this lever only toggles the
|
|
42
|
+
* parity-breaking decode kernel.) */
|
|
36
43
|
export function perfKernelEnabled(): boolean {
|
|
37
44
|
return process.env.MLX_BUN_PERF_KERNEL !== "0";
|
|
38
45
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The WHOLE MLP in ONE dispatch — down = (silu(x·Wgᵀ)·(x·Wuᵀ)) · Wdownᵀ — with no
|
|
2
|
+
// intermediate `hidden` ever exposed to MLX. MLX runs this one kernel; there is no
|
|
3
|
+
// boundary between our kernels for its planner to alias (the corruption only lives
|
|
4
|
+
// at those boundaries). Built from the steel building blocks in steel-qmm-header.ts,
|
|
5
|
+
// structured like the flash-CCE backward: each threadgroup owns one (M-block,I-block),
|
|
6
|
+
// computes silu·up for that I-block ONCE (no recompute), then atomic-accumulates its
|
|
7
|
+
// contribution to every down output — exactly how flash-CCE BWD atomic-adds dh.
|
|
8
|
+
//
|
|
9
|
+
// grid = (TGN, I/BI, M/BM); each tg: i-block i0 = tid.y*BI, m-block m0 = tid.z*BM
|
|
10
|
+
// phase A: gate/up [BM,BI] = x[BM,H] @ {Wg,Wu}[i0,H]ᵀ (reduce H) → silu·up = Hs[BM,BI]
|
|
11
|
+
// phase B: for each Ho-block no: D[BM,BN] = Hs @ Wdown[no,i0]ᵀ; atomic-add D → y[M,Ho]
|
|
12
|
+
//
|
|
13
|
+
// Per-weight quant constants (gate/up/down) are baked literals; H/I/Ho are templateInts;
|
|
14
|
+
// M from x_shape. Output is f32 (atomic accumulation); the caller casts to the model
|
|
15
|
+
// dtype at the MLP boundary. L3 (atomic-add ordering + bf16-rounded silu); gated by
|
|
16
|
+
// teacher-forced KL like the rest of the controlled path.
|
|
17
|
+
|
|
18
|
+
import { MlxArray } from "../mlx/array";
|
|
19
|
+
import { Dtype } from "../mlx/ffi";
|
|
20
|
+
import { MetalKernel } from "../mlx/metal-kernel";
|
|
21
|
+
import { STEEL_QMM_HEADER } from "../train/steel-qmm-header";
|
|
22
|
+
import * as ops from "../mlx/ops";
|
|
23
|
+
import type { QuantSpec } from "../mlx/ops";
|
|
24
|
+
|
|
25
|
+
const BM = 32, BN = 32, BK = 32, WM = 2, WN = 2;
|
|
26
|
+
const TGN = WM * WN * 32; // 128 threads / threadgroup
|
|
27
|
+
|
|
28
|
+
const SOURCE = String.raw`
|
|
29
|
+
const uint M=x_shape[0];
|
|
30
|
+
constexpr int I=I_T, H=H_T, Ho=HO_T;
|
|
31
|
+
constexpr int gsG=GS_G,btsG=BITS_G, gsU=GS_U,btsU=BITS_U, gsD=GS_D,btsD=BITS_D;
|
|
32
|
+
constexpr int pfG=get_pack_factor<btsG,8>(), bppG=get_bytes_per_pack<btsG>();
|
|
33
|
+
constexpr int pfU=get_pack_factor<btsU,8>(), bppU=get_bytes_per_pack<btsU>();
|
|
34
|
+
constexpr int pfD=get_pack_factor<btsD,8>(), bppD=get_bytes_per_pack<btsD>();
|
|
35
|
+
constexpr int BKp=BK_T+16/sizeof(T);
|
|
36
|
+
threadgroup T Xs[BM_T*BKp];
|
|
37
|
+
threadgroup T Wgs[BN_T*BKp];
|
|
38
|
+
threadgroup T Wus[BN_T*BKp];
|
|
39
|
+
threadgroup T Lg[BM_T*BN_T];
|
|
40
|
+
threadgroup T Lu[BM_T*BN_T];
|
|
41
|
+
threadgroup T Hs[BM_T*BKp]; // silu·up for this I-block, padded leading dim BKp
|
|
42
|
+
threadgroup T Wds[BN_T*BKp]; // a down-weight [BN, BI] tile
|
|
43
|
+
using gu_mma=mlx::steel::BlockMMA<T,T,BM_T,BN_T,BK_T,WM_T,WN_T,false,true,BKp,BKp>;
|
|
44
|
+
using lx_t =mlx::steel::BlockLoader<T,BM_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE>;
|
|
45
|
+
using lwg_t =QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gsG,btsG>;
|
|
46
|
+
using lwu_t =QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gsU,btsU>;
|
|
47
|
+
using d_mma =mlx::steel::BlockMMA<T,T,BM_T,BN_T,BK_T,WM_T,WN_T,false,true,BKp,BKp>;
|
|
48
|
+
using lwd_t =QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gsD,btsD>;
|
|
49
|
+
const uint sg=simdgroup_index_in_threadgroup, sl=thread_index_in_simdgroup;
|
|
50
|
+
const uint lid=thread_position_in_threadgroup.x;
|
|
51
|
+
const int Hh=(int)H;
|
|
52
|
+
const int KwG=Hh*bppG/pfG, KgG=Hh/gsG;
|
|
53
|
+
const int KwU=Hh*bppU/pfU, KgU=Hh/gsU;
|
|
54
|
+
const int KwD=I*bppD/pfD, KgD=I/gsD; // down weight rows are length I
|
|
55
|
+
const int m0=(int)threadgroup_position_in_grid.z*BM_T;
|
|
56
|
+
const int i0=(int)threadgroup_position_in_grid.y*BN_T;
|
|
57
|
+
const short num_els=(short)min(BM_T,(int)M-m0);
|
|
58
|
+
|
|
59
|
+
// ---- Phase A: hidden[BM,BI] = silu(x@Wg[i0]ᵀ)·(x@Wu[i0]ᵀ), reduce over H ----
|
|
60
|
+
gu_mma mg(sg,sl), mu(sg,sl);
|
|
61
|
+
lx_t lx (x + (int64_t)m0*Hh, Hh, Xs, sg, sl);
|
|
62
|
+
lwg_t lwg((const device uint8_t*)wg + (int64_t)i0*KwG, sg_w + (int64_t)i0*KgG, bg_w + (int64_t)i0*KgG, Hh, Wgs, sg, sl);
|
|
63
|
+
lwu_t lwu((const device uint8_t*)wu + (int64_t)i0*KwU, su_w + (int64_t)i0*KgU, bu_w + (int64_t)i0*KgU, Hh, Wus, sg, sl);
|
|
64
|
+
for (int k=0;k<Hh;k+=BK_T){
|
|
65
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
66
|
+
if (num_els<BM_T) lx.load_safe(short2(BK_T,num_els)); else lx.load_unsafe();
|
|
67
|
+
lwg.load_unsafe(); lwu.load_unsafe();
|
|
68
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
69
|
+
mg.mma(Xs,Wgs); mu.mma(Xs,Wus);
|
|
70
|
+
lx.next(); lwg.next(); lwu.next();
|
|
71
|
+
}
|
|
72
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
73
|
+
mg.Ctile.template store<T,WM_T,WN_T,BN_T,1>(Lg + mg.sm*BN_T + mg.sn);
|
|
74
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
75
|
+
mu.Ctile.template store<T,WM_T,WN_T,BN_T,1>(Lu + mu.sm*BN_T + mu.sn);
|
|
76
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
77
|
+
// silu·up into Hs (padded BKp leading dim, so the phase-B BlockMMA can read it as A)
|
|
78
|
+
for (uint idx=lid; idx<(uint)(BM_T*BN_T); idx+=TGN_T){
|
|
79
|
+
int r=(int)idx/BN_T, c=(int)idx%BN_T;
|
|
80
|
+
T gT=Lg[idx], uT=Lu[idx];
|
|
81
|
+
T sigT=(T)(1.0f/(1.0f+metal::precise::exp(-(float)gT)));
|
|
82
|
+
T siluT=(T)((float)gT*(float)sigT);
|
|
83
|
+
Hs[r*BKp + c] = (T)((float)siluT*(float)uT);
|
|
84
|
+
}
|
|
85
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
86
|
+
|
|
87
|
+
// ---- Phase B: for each Ho-block, D[BM,BN] = Hs @ Wdown[no,i0]ᵀ; atomic-add → y ----
|
|
88
|
+
for (int no=0; no<Ho; no+=BN_T){
|
|
89
|
+
lwd_t lwd((const device uint8_t*)wd + (int64_t)no*KwD + (int64_t)i0*bppD/pfD,
|
|
90
|
+
sd_w + (int64_t)no*KgD + i0/gsD, bd_w + (int64_t)no*KgD + i0/gsD, I, Wds, sg, sl);
|
|
91
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
92
|
+
lwd.load_unsafe(); // one BI-wide tile (BI==BK, single step)
|
|
93
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
94
|
+
d_mma D(sg,sl);
|
|
95
|
+
D.mma(Hs, Wds); // D[BM,BN] = Hs[BM,BI] @ Wds[BN,BI]ᵀ
|
|
96
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
97
|
+
D.Ctile.template store<T,WM_T,WN_T,BN_T,1>(Lg + D.sm*BN_T + D.sn); // reuse Lg to stage D
|
|
98
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
99
|
+
for (uint idx=lid; idx<(uint)(BM_T*BN_T); idx+=TGN_T){
|
|
100
|
+
int r=(int)idx/BN_T, c=(int)idx%BN_T;
|
|
101
|
+
if (m0+r<(int)M){
|
|
102
|
+
const int64_t off=(int64_t)(m0+r)*(int64_t)Ho + (no+c);
|
|
103
|
+
// Absorb the residual: add h exactly once (only the i0==0 threadgroups,
|
|
104
|
+
// which cover every (m,ho) once) so the kernel outputs h + mlp(x) and
|
|
105
|
+
// nothing is left live across it for MLX to plan a buffer around.
|
|
106
|
+
float add = (float)Lg[idx];
|
|
107
|
+
if (i0==0) add += (float)h[off];
|
|
108
|
+
atomic_fetch_add_explicit(&y[off], add, memory_order_relaxed);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const _kernels = new Map<string, MetalKernel>();
|
|
116
|
+
function getKernel(
|
|
117
|
+
gsG: number, btsG: number, gsU: number, btsU: number, gsD: number, btsD: number,
|
|
118
|
+
): MetalKernel {
|
|
119
|
+
const key = `${gsG}:${btsG}:${gsU}:${btsU}:${gsD}:${btsD}`;
|
|
120
|
+
let k = _kernels.get(key);
|
|
121
|
+
if (!k) {
|
|
122
|
+
k = new MetalKernel({
|
|
123
|
+
name: "mlx_bun_fused_mlp",
|
|
124
|
+
inputNames: ["x", "wg", "sg_w", "bg_w", "wu", "su_w", "bu_w", "wd", "sd_w", "bd_w", "h"],
|
|
125
|
+
outputNames: ["y"],
|
|
126
|
+
source: SOURCE,
|
|
127
|
+
header:
|
|
128
|
+
`${STEEL_QMM_HEADER}\n` +
|
|
129
|
+
`#define GS_G ${gsG}\n#define BITS_G ${btsG}\n#define GS_U ${gsU}\n#define BITS_U ${btsU}\n` +
|
|
130
|
+
`#define GS_D ${gsD}\n#define BITS_D ${btsD}\n` +
|
|
131
|
+
`#define BM_T ${BM}\n#define BN_T ${BN}\n#define BK_T ${BK}\n` +
|
|
132
|
+
`#define WM_T ${WM}\n#define WN_T ${WN}\n#define TGN_T ${TGN}\n`,
|
|
133
|
+
ensureRowContiguous: true,
|
|
134
|
+
atomicOutputs: true,
|
|
135
|
+
});
|
|
136
|
+
_kernels.set(key, k);
|
|
137
|
+
}
|
|
138
|
+
return k;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The whole MLP, fused, as one dispatch. x is [M, H] (bf16/f16). Returns the MLP
|
|
142
|
+
* output [M, Ho] in x.dtype (cast from the f32 atomic accumulator). gate/up have
|
|
143
|
+
* shape [I, H*bits/32]; down has [Ho, I*bits/32]. Caller owns the result. */
|
|
144
|
+
export function fusedMlp(
|
|
145
|
+
x: MlxArray,
|
|
146
|
+
wg: MlxArray, sg: MlxArray, bg: MlxArray, gateSpec: QuantSpec,
|
|
147
|
+
wu: MlxArray, su: MlxArray, bu: MlxArray, upSpec: QuantSpec,
|
|
148
|
+
wd: MlxArray, sd: MlxArray, bd: MlxArray, downSpec: QuantSpec,
|
|
149
|
+
h: MlxArray, // the residual (layer input) — absorbed: output = h + mlp(x)
|
|
150
|
+
): MlxArray {
|
|
151
|
+
const M = x.shape[0]!, I = wg.shape[0]!, H = x.shape[1]!, Ho = wd.shape[0]!;
|
|
152
|
+
const [yF] = getKernel(
|
|
153
|
+
gateSpec.groupSize, gateSpec.bits, upSpec.groupSize, upSpec.bits, downSpec.groupSize, downSpec.bits,
|
|
154
|
+
).apply([x, wg, sg, bg, wu, su, bu, wd, sd, bd, h], {
|
|
155
|
+
// f32 atomic accumulator, zero-initialised; grid tiles (M, I) — every I-block
|
|
156
|
+
// contributes to the output via atomic-add.
|
|
157
|
+
outputShapeFn: (ins) => [{ shape: [ins[0]!.shape[0]!, Ho], dtype: Dtype.float32 }],
|
|
158
|
+
grid: (ins) => [TGN, Math.ceil(I / BN), Math.ceil(ins[0]!.shape[0]! / BM)],
|
|
159
|
+
threadGroup: [TGN, 1, 1],
|
|
160
|
+
templateInts: { I_T: I, H_T: H, HO_T: Ho },
|
|
161
|
+
templateDtypes: { T: x.dtype },
|
|
162
|
+
initValue: 0,
|
|
163
|
+
});
|
|
164
|
+
void M;
|
|
165
|
+
if (yF!.dtype === x.dtype) return yF!;
|
|
166
|
+
const y = yF!.astype(x.dtype);
|
|
167
|
+
yF!.dispose();
|
|
168
|
+
return y;
|
|
169
|
+
}
|
|
@@ -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
|
|
|
@@ -749,10 +761,32 @@ export class RotatingKVCache implements Cache {
|
|
|
749
761
|
return this.offset < this.maxSize;
|
|
750
762
|
}
|
|
751
763
|
|
|
752
|
-
trim(n: number): void {
|
|
764
|
+
trim(n: number, bypass = false): void {
|
|
753
765
|
const k = Math.min(this.offset, n);
|
|
766
|
+
if (!bypass) {
|
|
767
|
+
// mlx-lm semantics: only valid pre-wrap (guarded by isTrimmable upstream).
|
|
768
|
+
this.offset -= k;
|
|
769
|
+
this.#idx -= k;
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
// Spec-decode rollback after a >1 #updateConcat write: the buffer is in
|
|
773
|
+
// temporal order with #idx === bufferLen and the newest tokens are the last
|
|
774
|
+
// rows, so physically slice the last k off and keep #idx === new length.
|
|
775
|
+
// (Only sound right after a concat write with k ≤ γ — DSpark's only caller.)
|
|
754
776
|
this.offset -= k;
|
|
755
|
-
this
|
|
777
|
+
if (k > 0 && this.keys && this.values) {
|
|
778
|
+
const [B, H, S, D] = this.keys.shape as [number, number, number, number];
|
|
779
|
+
const vD = this.values.shape[3]!;
|
|
780
|
+
const nk = this.keys.slice([0, 0, 0, 0], [B, H, S - k, D]);
|
|
781
|
+
const nv = this.values.slice([0, 0, 0, 0], [B, H, S - k, vD]);
|
|
782
|
+
this.keys.dispose();
|
|
783
|
+
this.values.dispose();
|
|
784
|
+
this.keys = nk;
|
|
785
|
+
this.values = nv;
|
|
786
|
+
this.#idx = nk.shape[2]!;
|
|
787
|
+
} else {
|
|
788
|
+
this.#idx -= k;
|
|
789
|
+
}
|
|
756
790
|
}
|
|
757
791
|
|
|
758
792
|
get ringIdx(): number {
|
|
@@ -1100,10 +1134,32 @@ export class RotatingQuantizedKVCache implements Cache {
|
|
|
1100
1134
|
return this.offset < this.maxSize;
|
|
1101
1135
|
}
|
|
1102
1136
|
|
|
1103
|
-
trim(n: number): void {
|
|
1137
|
+
trim(n: number, bypass = false): void {
|
|
1104
1138
|
const k = Math.min(this.offset, n);
|
|
1139
|
+
if (!bypass) {
|
|
1140
|
+
this.offset -= k;
|
|
1141
|
+
this.ringIdx -= k;
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
// Spec-decode rollback after a >1 concat write — physically slice the last k
|
|
1145
|
+
// off each (packed, scales, biases) component and keep ringIdx === seqLen.
|
|
1105
1146
|
this.offset -= k;
|
|
1106
|
-
this.
|
|
1147
|
+
if (k > 0 && this.keys && this.values) {
|
|
1148
|
+
const S = this.#seqLen();
|
|
1149
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
1150
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1151
|
+
return a.slice([0, 0, 0, 0], [B, H, S - k, D]);
|
|
1152
|
+
};
|
|
1153
|
+
const nk = mapTriple(this.keys, cut);
|
|
1154
|
+
const nv = mapTriple(this.values, cut);
|
|
1155
|
+
disposeTriple(this.keys);
|
|
1156
|
+
disposeTriple(this.values);
|
|
1157
|
+
this.keys = nk;
|
|
1158
|
+
this.values = nv;
|
|
1159
|
+
this.ringIdx = this.#seqLen();
|
|
1160
|
+
} else {
|
|
1161
|
+
this.ringIdx -= k;
|
|
1162
|
+
}
|
|
1107
1163
|
}
|
|
1108
1164
|
|
|
1109
1165
|
/** Oracle: to_quantized on an already-quantized rotating cache is
|