mlx-bun 0.0.4
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/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- package/src/weights.ts +91 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
// L2 flash-attention training kernels — port of optiq/ops/flash_attention_{metal,
|
|
2
|
+
// backward_metal}.py. FlashAttention-2 as Metal kernels: the forward runs in one
|
|
3
|
+
// dispatch with online softmax (never materializing the [.,L,L] score matrix),
|
|
4
|
+
// and the backward is three dispatches (D-vec, dKV, dQ) that recompute scores per
|
|
5
|
+
// tile. Memory is O(seq·head_dim), not O(seq²) — this is what lets long-context
|
|
6
|
+
// LoRA training fit where stock fast-sdpa (L1) tops out ~2048 tokens.
|
|
7
|
+
//
|
|
8
|
+
// Bit-exact to OPTIQ (the L2 oracle), NOT to mlx-lm: online softmax accumulates
|
|
9
|
+
// in fp32 but stores fp16, so it differs from one-shot SDPA. Wired through
|
|
10
|
+
// CustomVjp (the forward is a CustomKernel with no auto-vjp).
|
|
11
|
+
//
|
|
12
|
+
// Shaders are copied verbatim from optiq; only the host glue is TS.
|
|
13
|
+
|
|
14
|
+
import { MlxArray } from "../mlx/array";
|
|
15
|
+
import { Dtype } from "../mlx/ffi";
|
|
16
|
+
import * as ops from "../mlx/ops";
|
|
17
|
+
import { MetalKernel } from "../mlx/metal-kernel";
|
|
18
|
+
import { CustomVjp } from "../mlx/custom-vjp";
|
|
19
|
+
import { createCausalMask, type Mask } from "./gemma4-base";
|
|
20
|
+
|
|
21
|
+
export const SUPPORTED_HEAD_DIMS = new Set([64, 96, 128, 256, 512]);
|
|
22
|
+
|
|
23
|
+
/** (BLOCK_Q, BLOCK_KV) for the forward kernel at this head_dim. */
|
|
24
|
+
function forwardTiles(headDim: number): [number, number] {
|
|
25
|
+
if (headDim <= 64) return [64, 64];
|
|
26
|
+
if (headDim <= 128) return [32, 32];
|
|
27
|
+
if (headDim <= 256) return [16, 16];
|
|
28
|
+
if (headDim <= 512) return [8, 8];
|
|
29
|
+
throw new Error(`unsupported head_dim=${headDim}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** uint32 [n] array (matches optiq's shape/causal input dtype). */
|
|
33
|
+
function u32(values: number[]): MlxArray {
|
|
34
|
+
const u = new Uint32Array(values);
|
|
35
|
+
return MlxArray.fromView(new Uint8Array(u.buffer.slice(0)), [values.length], Dtype.uint32);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Forward shader — verbatim from optiq flash_attention_metal.py _KERNEL_SOURCE.
|
|
39
|
+
const FWD_SOURCE = `
|
|
40
|
+
const uint q_tile_idx = threadgroup_position_in_grid.x;
|
|
41
|
+
const uint head = threadgroup_position_in_grid.y; // 0..Hq-1
|
|
42
|
+
const uint batch = threadgroup_position_in_grid.z;
|
|
43
|
+
const uint tid = thread_position_in_threadgroup.x;
|
|
44
|
+
|
|
45
|
+
const uint B = shape[0];
|
|
46
|
+
const uint Hq = shape[1];
|
|
47
|
+
const uint Hkv = shape[2];
|
|
48
|
+
const uint Tq = shape[3];
|
|
49
|
+
const uint Tkv = shape[4];
|
|
50
|
+
const uint D = shape[5];
|
|
51
|
+
|
|
52
|
+
const float fscale = scale[0];
|
|
53
|
+
const uint is_causal = causal[0];
|
|
54
|
+
|
|
55
|
+
const uint reps = Hq / Hkv;
|
|
56
|
+
const uint kv_head = head / reps;
|
|
57
|
+
|
|
58
|
+
const uint q_row_global = q_tile_idx * BQ + tid;
|
|
59
|
+
const bool q_row_valid = q_row_global < Tq;
|
|
60
|
+
|
|
61
|
+
threadgroup half q_tile[BQ * D_MAX];
|
|
62
|
+
threadgroup half k_tile[BKV * D_MAX];
|
|
63
|
+
threadgroup half v_tile[BKV * D_MAX];
|
|
64
|
+
|
|
65
|
+
float m_i = -INFINITY;
|
|
66
|
+
float l_i = 0.0f;
|
|
67
|
+
float o_i[D_MAX];
|
|
68
|
+
for (uint d = 0; d < D; ++d) { o_i[d] = 0.0f; }
|
|
69
|
+
|
|
70
|
+
if (q_row_valid) {
|
|
71
|
+
const uint q_base = ((batch * Hq + head) * Tq + q_row_global) * D;
|
|
72
|
+
for (uint d = 0; d < D; ++d) {
|
|
73
|
+
q_tile[tid * D + d] = q[q_base + d];
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
for (uint d = 0; d < D; ++d) {
|
|
77
|
+
q_tile[tid * D + d] = 0.0h;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
81
|
+
|
|
82
|
+
const uint n_kv_tiles = (Tkv + BKV - 1u) / BKV;
|
|
83
|
+
for (uint kv_tile = 0u; kv_tile < n_kv_tiles; ++kv_tile) {
|
|
84
|
+
const uint kv_base_row = kv_tile * BKV;
|
|
85
|
+
|
|
86
|
+
const uint rows_this_tile = min((uint)BKV, Tkv - kv_base_row);
|
|
87
|
+
if (tid < rows_this_tile) {
|
|
88
|
+
const uint kv_global_row = kv_base_row + tid;
|
|
89
|
+
const uint kv_global = ((batch * Hkv + kv_head) * Tkv + kv_global_row) * D;
|
|
90
|
+
for (uint d = 0; d < D; ++d) {
|
|
91
|
+
k_tile[tid * D + d] = k[kv_global + d];
|
|
92
|
+
v_tile[tid * D + d] = v[kv_global + d];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
96
|
+
|
|
97
|
+
if (is_causal && kv_base_row > q_row_global) {
|
|
98
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
float s[BKV];
|
|
103
|
+
float row_max = -INFINITY;
|
|
104
|
+
for (uint j = 0u; j < rows_this_tile; ++j) {
|
|
105
|
+
float dot = 0.0f;
|
|
106
|
+
for (uint d = 0u; d < D; ++d) {
|
|
107
|
+
dot += (float)q_tile[tid * D + d] * (float)k_tile[j * D + d];
|
|
108
|
+
}
|
|
109
|
+
dot *= fscale;
|
|
110
|
+
const uint kpos = kv_base_row + j;
|
|
111
|
+
if (is_causal && kpos > q_row_global) {
|
|
112
|
+
dot = -INFINITY;
|
|
113
|
+
} else if (shape[6] != 0u && q_row_global >= kpos && (q_row_global - kpos) >= shape[6]) {
|
|
114
|
+
dot = -INFINITY; // sliding window: too far in the past
|
|
115
|
+
}
|
|
116
|
+
s[j] = dot;
|
|
117
|
+
row_max = max(row_max, dot);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const float m_new = max(m_i, row_max);
|
|
121
|
+
// Skip a tile that is fully masked for this q row (every score -inf) —
|
|
122
|
+
// happens with a sliding window when a past tile is entirely out of range.
|
|
123
|
+
// Without this guard the online softmax does exp(-inf - -inf) = NaN.
|
|
124
|
+
if (m_new > -INFINITY) {
|
|
125
|
+
const float rescale = exp(m_i - m_new);
|
|
126
|
+
float row_sum = 0.0f;
|
|
127
|
+
for (uint j = 0u; j < rows_this_tile; ++j) {
|
|
128
|
+
s[j] = exp(s[j] - m_new);
|
|
129
|
+
row_sum += s[j];
|
|
130
|
+
}
|
|
131
|
+
l_i = rescale * l_i + row_sum;
|
|
132
|
+
|
|
133
|
+
for (uint d = 0u; d < D; ++d) {
|
|
134
|
+
float new_contrib = 0.0f;
|
|
135
|
+
for (uint j = 0u; j < rows_this_tile; ++j) {
|
|
136
|
+
new_contrib += s[j] * (float)v_tile[j * D + d];
|
|
137
|
+
}
|
|
138
|
+
o_i[d] = rescale * o_i[d] + new_contrib;
|
|
139
|
+
}
|
|
140
|
+
m_i = m_new;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (q_row_valid) {
|
|
147
|
+
const uint out_base = ((batch * Hq + head) * Tq + q_row_global) * D;
|
|
148
|
+
const float inv_l = 1.0f / l_i;
|
|
149
|
+
for (uint d = 0u; d < D; ++d) {
|
|
150
|
+
o[out_base + d] = (half)(o_i[d] * inv_l);
|
|
151
|
+
}
|
|
152
|
+
const uint lse_base = (batch * Hq + head) * Tq + q_row_global;
|
|
153
|
+
l_out[lse_base] = m_i + log(l_i);
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const fwdKernels = new Map<number, MetalKernel>();
|
|
158
|
+
function fwdKernel(headDim: number): MetalKernel {
|
|
159
|
+
let k = fwdKernels.get(headDim);
|
|
160
|
+
if (!k) {
|
|
161
|
+
const [bq, bkv] = forwardTiles(headDim);
|
|
162
|
+
const header = `#define BQ ${bq}\n#define BKV ${bkv}\n#define D_MAX ${headDim}\n`;
|
|
163
|
+
k = new MetalKernel({
|
|
164
|
+
name: `optiq_flash_attention_fwd_d${headDim}`,
|
|
165
|
+
inputNames: ["q", "k", "v", "shape", "scale", "causal"],
|
|
166
|
+
outputNames: ["o", "l_out"],
|
|
167
|
+
source: FWD_SOURCE,
|
|
168
|
+
header,
|
|
169
|
+
ensureRowContiguous: true,
|
|
170
|
+
});
|
|
171
|
+
fwdKernels.set(headDim, k);
|
|
172
|
+
}
|
|
173
|
+
return k;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Single-dispatch Metal flash-attention forward. q,k,v are fp16
|
|
177
|
+
* ([B,Hq,Tq,D] / [B,Hkv,Tkv,D]). Returns [O fp16, L fp32 (log-sum-exp)]. */
|
|
178
|
+
export function flashForward(
|
|
179
|
+
q: MlxArray, k: MlxArray, v: MlxArray, scale: number, causal: boolean, window = 0,
|
|
180
|
+
): [MlxArray, MlxArray] {
|
|
181
|
+
const [B, Hq, Tq, D] = q.shape as [number, number, number, number];
|
|
182
|
+
const [, Hkv, Tkv] = k.shape as [number, number, number, number];
|
|
183
|
+
const [bq] = forwardTiles(D);
|
|
184
|
+
const nQTiles = Math.ceil(Tq / bq);
|
|
185
|
+
|
|
186
|
+
const shape = u32([B, Hq, Hkv, Tq, Tkv, D, window]);
|
|
187
|
+
const scaleArr = MlxArray.fromFloat32(new Float32Array([scale]), [1]);
|
|
188
|
+
const causalArr = u32([causal ? 1 : 0]);
|
|
189
|
+
try {
|
|
190
|
+
const outs = fwdKernel(D).apply([q, k, v, shape, scaleArr, causalArr], {
|
|
191
|
+
outputs: [
|
|
192
|
+
{ shape: [B, Hq, Tq, D], dtype: Dtype.float16 },
|
|
193
|
+
{ shape: [B, Hq, Tq], dtype: Dtype.float32 },
|
|
194
|
+
],
|
|
195
|
+
grid: [nQTiles * bq, Hq, B],
|
|
196
|
+
threadGroup: [bq, 1, 1],
|
|
197
|
+
});
|
|
198
|
+
return [outs[0]!, outs[1]!];
|
|
199
|
+
} finally {
|
|
200
|
+
shape.dispose();
|
|
201
|
+
scaleArr.dispose();
|
|
202
|
+
causalArr.dispose();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Backward — three Metal dispatches (D-vec, dKV, dQ), verbatim from optiq
|
|
208
|
+
// flash_attention_backward_metal.py.
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/** (BQ_BWD, BKV_BWD) for the backward kernels at this head_dim. */
|
|
212
|
+
function backwardTiles(headDim: number): [number, number] {
|
|
213
|
+
if (headDim <= 64) return [32, 32];
|
|
214
|
+
if (headDim <= 128) return [16, 32];
|
|
215
|
+
if (headDim <= 256) return [8, 16];
|
|
216
|
+
if (headDim <= 512) return [4, 8];
|
|
217
|
+
throw new Error(`unsupported head_dim=${headDim}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Pass 1: D_i = rowsum(dO_i ⊙ O_i). No tile defines needed.
|
|
221
|
+
const D_SOURCE = `
|
|
222
|
+
const uint idx = thread_position_in_grid.x;
|
|
223
|
+
const uint B = shape[0];
|
|
224
|
+
const uint Hq = shape[1];
|
|
225
|
+
const uint Tq = shape[2];
|
|
226
|
+
const uint D = shape[3];
|
|
227
|
+
const uint total = B * Hq * Tq;
|
|
228
|
+
if (idx >= total) return;
|
|
229
|
+
|
|
230
|
+
const uint base = idx * D;
|
|
231
|
+
float acc = 0.0f;
|
|
232
|
+
for (uint d = 0u; d < D; ++d) {
|
|
233
|
+
acc += (float)o[base + d] * (float)do_[base + d];
|
|
234
|
+
}
|
|
235
|
+
d_out[idx] = acc;
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
// Pass 2: dK, dV — one dispatch per KV tile, inner loop over Q tiles.
|
|
239
|
+
const DKV_SOURCE = `
|
|
240
|
+
const uint kv_tile_idx = threadgroup_position_in_grid.x;
|
|
241
|
+
const uint kv_head = threadgroup_position_in_grid.y; // 0..Hkv-1
|
|
242
|
+
const uint batch = threadgroup_position_in_grid.z;
|
|
243
|
+
const uint tid = thread_position_in_threadgroup.x;
|
|
244
|
+
|
|
245
|
+
const uint B = shape[0];
|
|
246
|
+
const uint Hq = shape[1];
|
|
247
|
+
const uint Hkv = shape[2];
|
|
248
|
+
const uint Tq = shape[3];
|
|
249
|
+
const uint Tkv = shape[4];
|
|
250
|
+
const uint D = shape[5];
|
|
251
|
+
const uint reps = Hq / Hkv;
|
|
252
|
+
|
|
253
|
+
const float fscale = scale[0];
|
|
254
|
+
const uint is_causal = causal[0];
|
|
255
|
+
|
|
256
|
+
const uint kv_row_global = kv_tile_idx * BKV + tid;
|
|
257
|
+
const bool kv_row_valid = kv_row_global < Tkv;
|
|
258
|
+
|
|
259
|
+
threadgroup half q_tile[BQ * D_MAX];
|
|
260
|
+
threadgroup half do_tile[BQ * D_MAX];
|
|
261
|
+
threadgroup half k_tile[BKV * D_MAX];
|
|
262
|
+
threadgroup half v_tile[BKV * D_MAX];
|
|
263
|
+
threadgroup float l_tile[BQ];
|
|
264
|
+
threadgroup float dRow[BQ];
|
|
265
|
+
|
|
266
|
+
float dK_j[D_MAX];
|
|
267
|
+
float dV_j[D_MAX];
|
|
268
|
+
for (uint d = 0u; d < D; ++d) {
|
|
269
|
+
dK_j[d] = 0.0f;
|
|
270
|
+
dV_j[d] = 0.0f;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (kv_row_valid) {
|
|
274
|
+
const uint kv_base = ((batch * Hkv + kv_head) * Tkv + kv_row_global) * D;
|
|
275
|
+
for (uint d = 0u; d < D; ++d) {
|
|
276
|
+
k_tile[tid * D + d] = k[kv_base + d];
|
|
277
|
+
v_tile[tid * D + d] = v[kv_base + d];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
281
|
+
|
|
282
|
+
const uint n_q_tiles = (Tq + BQ - 1u) / BQ;
|
|
283
|
+
for (uint q_h_rel = 0u; q_h_rel < reps; ++q_h_rel) {
|
|
284
|
+
const uint q_head = kv_head * reps + q_h_rel;
|
|
285
|
+
|
|
286
|
+
for (uint qi = 0u; qi < n_q_tiles; ++qi) {
|
|
287
|
+
const uint q_base_row = qi * BQ;
|
|
288
|
+
|
|
289
|
+
if (is_causal && (q_base_row + BQ) <= kv_tile_idx * BKV) {
|
|
290
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const uint rows_this_q = min((uint)BQ, Tq - q_base_row);
|
|
295
|
+
if (tid < rows_this_q) {
|
|
296
|
+
const uint q_global_row = q_base_row + tid;
|
|
297
|
+
const uint q_base = ((batch * Hq + q_head) * Tq + q_global_row) * D;
|
|
298
|
+
for (uint d = 0u; d < D; ++d) {
|
|
299
|
+
q_tile[tid * D + d] = q[q_base + d];
|
|
300
|
+
do_tile[tid * D + d] = do_[q_base + d];
|
|
301
|
+
}
|
|
302
|
+
const uint lse_idx = (batch * Hq + q_head) * Tq + q_global_row;
|
|
303
|
+
l_tile[tid] = l_in[lse_idx];
|
|
304
|
+
dRow[tid] = d_vec[lse_idx];
|
|
305
|
+
}
|
|
306
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
307
|
+
|
|
308
|
+
if (kv_row_valid) {
|
|
309
|
+
for (uint i = 0u; i < rows_this_q; ++i) {
|
|
310
|
+
float s = 0.0f;
|
|
311
|
+
for (uint d = 0u; d < D; ++d) {
|
|
312
|
+
s += (float)q_tile[i * D + d] * (float)k_tile[tid * D + d];
|
|
313
|
+
}
|
|
314
|
+
s *= fscale;
|
|
315
|
+
const uint q_row_global = q_base_row + i;
|
|
316
|
+
if (is_causal && kv_row_global > q_row_global) {
|
|
317
|
+
s = -INFINITY;
|
|
318
|
+
} else if (shape[6] != 0u && q_row_global >= kv_row_global && (q_row_global - kv_row_global) >= shape[6]) {
|
|
319
|
+
s = -INFINITY; // sliding window
|
|
320
|
+
}
|
|
321
|
+
const float p = exp(s - l_tile[i]);
|
|
322
|
+
|
|
323
|
+
float dp = 0.0f;
|
|
324
|
+
for (uint d = 0u; d < D; ++d) {
|
|
325
|
+
dp += (float)do_tile[i * D + d] * (float)v_tile[tid * D + d];
|
|
326
|
+
}
|
|
327
|
+
const float ds = p * (dp - dRow[i]);
|
|
328
|
+
|
|
329
|
+
for (uint d = 0u; d < D; ++d) {
|
|
330
|
+
dV_j[d] += p * (float)do_tile[i * D + d];
|
|
331
|
+
}
|
|
332
|
+
for (uint d = 0u; d < D; ++d) {
|
|
333
|
+
dK_j[d] += ds * (float)q_tile[i * D + d] * fscale;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (kv_row_valid) {
|
|
342
|
+
const uint out_base = ((batch * Hkv + kv_head) * Tkv + kv_row_global) * D;
|
|
343
|
+
for (uint d = 0u; d < D; ++d) {
|
|
344
|
+
dk_out[out_base + d] = (half)dK_j[d];
|
|
345
|
+
dv_out[out_base + d] = (half)dV_j[d];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
`;
|
|
349
|
+
|
|
350
|
+
// Pass 3: dQ — one dispatch per Q tile, inner loop over KV tiles.
|
|
351
|
+
const DQ_SOURCE = `
|
|
352
|
+
const uint q_tile_idx = threadgroup_position_in_grid.x;
|
|
353
|
+
const uint head = threadgroup_position_in_grid.y; // 0..Hq-1
|
|
354
|
+
const uint batch = threadgroup_position_in_grid.z;
|
|
355
|
+
const uint tid = thread_position_in_threadgroup.x;
|
|
356
|
+
|
|
357
|
+
const uint B = shape[0];
|
|
358
|
+
const uint Hq = shape[1];
|
|
359
|
+
const uint Hkv = shape[2];
|
|
360
|
+
const uint Tq = shape[3];
|
|
361
|
+
const uint Tkv = shape[4];
|
|
362
|
+
const uint D = shape[5];
|
|
363
|
+
|
|
364
|
+
const uint reps = Hq / Hkv;
|
|
365
|
+
const uint kv_head = head / reps;
|
|
366
|
+
|
|
367
|
+
const float fscale = scale[0];
|
|
368
|
+
const uint is_causal = causal[0];
|
|
369
|
+
|
|
370
|
+
const uint q_row_global = q_tile_idx * BQ + tid;
|
|
371
|
+
const bool q_row_valid = q_row_global < Tq;
|
|
372
|
+
|
|
373
|
+
threadgroup half k_tile[BKV * D_MAX];
|
|
374
|
+
threadgroup half v_tile[BKV * D_MAX];
|
|
375
|
+
|
|
376
|
+
half q_i[D_MAX];
|
|
377
|
+
half do_i[D_MAX];
|
|
378
|
+
float l_i = 0.0f;
|
|
379
|
+
float D_i = 0.0f;
|
|
380
|
+
float dq_i[D_MAX];
|
|
381
|
+
for (uint d = 0u; d < D; ++d) { dq_i[d] = 0.0f; }
|
|
382
|
+
|
|
383
|
+
if (q_row_valid) {
|
|
384
|
+
const uint q_base = ((batch * Hq + head) * Tq + q_row_global) * D;
|
|
385
|
+
for (uint d = 0u; d < D; ++d) {
|
|
386
|
+
q_i[d] = q[q_base + d];
|
|
387
|
+
do_i[d] = do_[q_base + d];
|
|
388
|
+
}
|
|
389
|
+
const uint lse_idx = (batch * Hq + head) * Tq + q_row_global;
|
|
390
|
+
l_i = l_in[lse_idx];
|
|
391
|
+
D_i = d_vec[lse_idx];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const uint n_kv_tiles = (Tkv + BKV - 1u) / BKV;
|
|
395
|
+
for (uint kj = 0u; kj < n_kv_tiles; ++kj) {
|
|
396
|
+
const uint kv_base_row = kj * BKV;
|
|
397
|
+
|
|
398
|
+
const uint rows_this_tile = min((uint)BKV, Tkv - kv_base_row);
|
|
399
|
+
for (uint r = tid; r < rows_this_tile; r += BQ) {
|
|
400
|
+
const uint kv_global_row = kv_base_row + r;
|
|
401
|
+
const uint kv_base = ((batch * Hkv + kv_head) * Tkv + kv_global_row) * D;
|
|
402
|
+
for (uint d = 0u; d < D; ++d) {
|
|
403
|
+
k_tile[r * D + d] = k[kv_base + d];
|
|
404
|
+
v_tile[r * D + d] = v[kv_base + d];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
408
|
+
|
|
409
|
+
if (is_causal && kv_base_row > q_row_global) {
|
|
410
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (q_row_valid) {
|
|
415
|
+
for (uint j = 0u; j < rows_this_tile; ++j) {
|
|
416
|
+
float s = 0.0f;
|
|
417
|
+
for (uint d = 0u; d < D; ++d) {
|
|
418
|
+
s += (float)q_i[d] * (float)k_tile[j * D + d];
|
|
419
|
+
}
|
|
420
|
+
s *= fscale;
|
|
421
|
+
const uint kpos = kv_base_row + j;
|
|
422
|
+
if (is_causal && kpos > q_row_global) {
|
|
423
|
+
s = -INFINITY;
|
|
424
|
+
} else if (shape[6] != 0u && q_row_global >= kpos && (q_row_global - kpos) >= shape[6]) {
|
|
425
|
+
s = -INFINITY; // sliding window
|
|
426
|
+
}
|
|
427
|
+
float p = exp(s - l_i);
|
|
428
|
+
float dp = 0.0f;
|
|
429
|
+
for (uint d = 0u; d < D; ++d) {
|
|
430
|
+
dp += (float)do_i[d] * (float)v_tile[j * D + d];
|
|
431
|
+
}
|
|
432
|
+
float ds = p * (dp - D_i);
|
|
433
|
+
for (uint d = 0u; d < D; ++d) {
|
|
434
|
+
dq_i[d] += ds * (float)k_tile[j * D + d] * fscale;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (q_row_valid) {
|
|
442
|
+
const uint out_base = ((batch * Hq + head) * Tq + q_row_global) * D;
|
|
443
|
+
for (uint d = 0u; d < D; ++d) {
|
|
444
|
+
dq_out[out_base + d] = (half)dq_i[d];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
`;
|
|
448
|
+
|
|
449
|
+
let dKernelCache: MetalKernel | null = null;
|
|
450
|
+
function dKernel(): MetalKernel {
|
|
451
|
+
if (!dKernelCache)
|
|
452
|
+
dKernelCache = new MetalKernel({
|
|
453
|
+
name: "optiq_flash_attention_D",
|
|
454
|
+
inputNames: ["o", "do_", "shape"],
|
|
455
|
+
outputNames: ["d_out"],
|
|
456
|
+
source: D_SOURCE,
|
|
457
|
+
ensureRowContiguous: true,
|
|
458
|
+
});
|
|
459
|
+
return dKernelCache;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const dkvKernels = new Map<number, MetalKernel>();
|
|
463
|
+
function dkvKernel(headDim: number): MetalKernel {
|
|
464
|
+
let k = dkvKernels.get(headDim);
|
|
465
|
+
if (!k) {
|
|
466
|
+
const [bq, bkv] = backwardTiles(headDim);
|
|
467
|
+
const header = `#define BQ ${bq}\n#define BKV ${bkv}\n#define D_MAX ${headDim}\n`;
|
|
468
|
+
k = new MetalKernel({
|
|
469
|
+
name: `optiq_flash_attention_dKV_d${headDim}`,
|
|
470
|
+
inputNames: ["q", "k", "v", "do_", "l_in", "d_vec", "shape", "scale", "causal"],
|
|
471
|
+
outputNames: ["dk_out", "dv_out"],
|
|
472
|
+
source: DKV_SOURCE,
|
|
473
|
+
header,
|
|
474
|
+
ensureRowContiguous: true,
|
|
475
|
+
});
|
|
476
|
+
dkvKernels.set(headDim, k);
|
|
477
|
+
}
|
|
478
|
+
return k;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const dqKernels = new Map<number, MetalKernel>();
|
|
482
|
+
function dqKernel(headDim: number): MetalKernel {
|
|
483
|
+
let k = dqKernels.get(headDim);
|
|
484
|
+
if (!k) {
|
|
485
|
+
const [bq, bkv] = backwardTiles(headDim);
|
|
486
|
+
const header = `#define BQ ${bq}\n#define BKV ${bkv}\n#define D_MAX ${headDim}\n`;
|
|
487
|
+
k = new MetalKernel({
|
|
488
|
+
name: `optiq_flash_attention_dQ_d${headDim}`,
|
|
489
|
+
inputNames: ["q", "k", "v", "do_", "l_in", "d_vec", "shape", "scale", "causal"],
|
|
490
|
+
outputNames: ["dq_out"],
|
|
491
|
+
source: DQ_SOURCE,
|
|
492
|
+
header,
|
|
493
|
+
ensureRowContiguous: true,
|
|
494
|
+
});
|
|
495
|
+
dqKernels.set(headDim, k);
|
|
496
|
+
}
|
|
497
|
+
return k;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Three-dispatch Metal backward. q,k,v,O,dO fp16; L fp32. Returns [dQ,dK,dV]. */
|
|
501
|
+
export function flashBackward(
|
|
502
|
+
q: MlxArray, k: MlxArray, v: MlxArray, O: MlxArray, L: MlxArray, dO: MlxArray,
|
|
503
|
+
scale: number, causal: boolean, window = 0,
|
|
504
|
+
): [MlxArray, MlxArray, MlxArray] {
|
|
505
|
+
const [B, Hq, Tq, D] = q.shape as [number, number, number, number];
|
|
506
|
+
const [, Hkv, Tkv] = k.shape as [number, number, number, number];
|
|
507
|
+
const [bq, bkv] = backwardTiles(D);
|
|
508
|
+
|
|
509
|
+
const dShape = u32([B, Hq, Tq, D]);
|
|
510
|
+
const Dvec = dKernel().apply([O, dO, dShape], {
|
|
511
|
+
outputs: [{ shape: [B, Hq, Tq], dtype: Dtype.float32 }],
|
|
512
|
+
grid: [B * Hq * Tq, 1, 1],
|
|
513
|
+
threadGroup: [256, 1, 1],
|
|
514
|
+
})[0]!;
|
|
515
|
+
dShape.dispose();
|
|
516
|
+
|
|
517
|
+
const shape = u32([B, Hq, Hkv, Tq, Tkv, D, window]);
|
|
518
|
+
const scaleArr = MlxArray.fromFloat32(new Float32Array([scale]), [1]);
|
|
519
|
+
const causalArr = u32([causal ? 1 : 0]);
|
|
520
|
+
try {
|
|
521
|
+
const nKvTiles = Math.ceil(Tkv / bkv);
|
|
522
|
+
const dkv = dkvKernel(D).apply([q, k, v, dO, L, Dvec, shape, scaleArr, causalArr], {
|
|
523
|
+
outputs: [
|
|
524
|
+
{ shape: [B, Hkv, Tkv, D], dtype: Dtype.float16 },
|
|
525
|
+
{ shape: [B, Hkv, Tkv, D], dtype: Dtype.float16 },
|
|
526
|
+
],
|
|
527
|
+
grid: [nKvTiles * bkv, Hkv, B],
|
|
528
|
+
threadGroup: [bkv, 1, 1],
|
|
529
|
+
});
|
|
530
|
+
const nQTiles = Math.ceil(Tq / bq);
|
|
531
|
+
const dq = dqKernel(D).apply([q, k, v, dO, L, Dvec, shape, scaleArr, causalArr], {
|
|
532
|
+
outputs: [{ shape: [B, Hq, Tq, D], dtype: Dtype.float16 }],
|
|
533
|
+
grid: [nQTiles * bq, Hq, B],
|
|
534
|
+
threadGroup: [bq, 1, 1],
|
|
535
|
+
});
|
|
536
|
+
// The dKV kernel emits dK with its (Tkv, D) axes transposed in the buffer
|
|
537
|
+
// (verified: dK[i][j] == correct[j][i] at T=D, a [Tkv,D]↔[D,Tkv] swap in
|
|
538
|
+
// general — scripts/flash-dk-debug.ts JS-FIX 0.07% vs autograd). dV is
|
|
539
|
+
// unaffected. Recover the correct layout: transpose → materialize → reshape.
|
|
540
|
+
const dkFixed = ops.reshape(
|
|
541
|
+
ops.contiguous(ops.transposeAxes(dkv[0]!, [0, 1, 3, 2])),
|
|
542
|
+
[B, Hkv, Tkv, D],
|
|
543
|
+
);
|
|
544
|
+
dkv[0]!.dispose();
|
|
545
|
+
return [dq[0]!, dkFixed, dkv[1]!];
|
|
546
|
+
} finally {
|
|
547
|
+
Dvec.dispose();
|
|
548
|
+
shape.dispose();
|
|
549
|
+
scaleArr.dispose();
|
|
550
|
+
causalArr.dispose();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
// Differentiable op: flash forward + manual backward via CustomVjp.
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
// One CustomVjp per (head_dim, scale, causal) — the closures capture those as
|
|
559
|
+
// constants and read q/k/v from the input vector, so a single instance is
|
|
560
|
+
// reused across layers/steps. Module-cached (never disposed): the closures
|
|
561
|
+
// must outlive every value_and_grad apply.
|
|
562
|
+
const faCache = new Map<string, CustomVjp>();
|
|
563
|
+
function faOp(headDim: number, scale: number, causal: boolean, window: number): CustomVjp {
|
|
564
|
+
const key = `${headDim}|${scale}|${causal ? 1 : 0}|${window}`;
|
|
565
|
+
let cv = faCache.get(key);
|
|
566
|
+
if (!cv) {
|
|
567
|
+
cv = new CustomVjp(
|
|
568
|
+
(ins) => {
|
|
569
|
+
const [O, L] = flashForward(ins[0]!, ins[1]!, ins[2]!, scale, causal, window);
|
|
570
|
+
return [O, L];
|
|
571
|
+
},
|
|
572
|
+
(primals, cots, outs) => {
|
|
573
|
+
// primals=[q,k,v]; cots=[dO, dL] (dL=0, discarded); outs=[O, L]
|
|
574
|
+
return flashBackward(primals[0]!, primals[1]!, primals[2]!, outs[0]!, outs[1]!, cots[0]!, scale, causal, window);
|
|
575
|
+
},
|
|
576
|
+
);
|
|
577
|
+
faCache.set(key, cv);
|
|
578
|
+
}
|
|
579
|
+
return cv;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Differentiable flash attention matching optiq's training kernel. q,k,v are
|
|
583
|
+
* [B,Hq,Tq,D]/[B,Hkv,Tkv,D]; bf16 is cast to fp16 (kernel dtype) and the
|
|
584
|
+
* output cast back. Only causal/full attention (sliding-window masks fall
|
|
585
|
+
* back to ops.sdpa at the call site, mirroring optiq's router). */
|
|
586
|
+
export function flashAttention(
|
|
587
|
+
q: MlxArray, k: MlxArray, v: MlxArray, scale: number, causal: boolean, window = 0,
|
|
588
|
+
): MlxArray {
|
|
589
|
+
const D = q.shape[3]!;
|
|
590
|
+
const inDtype = q.dtype;
|
|
591
|
+
const f16 = inDtype === Dtype.float16;
|
|
592
|
+
const q16 = f16 ? q : q.astype(Dtype.float16);
|
|
593
|
+
const k16 = f16 ? k : k.astype(Dtype.float16);
|
|
594
|
+
const v16 = f16 ? v : v.astype(Dtype.float16);
|
|
595
|
+
|
|
596
|
+
const outs = faOp(D, scale, causal, window).apply([q16, k16, v16]);
|
|
597
|
+
const O = outs[0]!;
|
|
598
|
+
outs[1]?.dispose(); // L — only needed inside the vjp (mlx keeps it via the graph)
|
|
599
|
+
const result = f16 ? O : O.astype(inDtype);
|
|
600
|
+
if (!f16) {
|
|
601
|
+
O.dispose();
|
|
602
|
+
q16.dispose();
|
|
603
|
+
k16.dispose();
|
|
604
|
+
v16.dispose();
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
// Training-attention routing. ops.sdpa's vjp gives a wrong dK (it's an
|
|
611
|
+
// inference op), so the training forward must use a known-correct attention:
|
|
612
|
+
// the flash kernel for causal/full layers (memory-efficient, validated), and a
|
|
613
|
+
// manual materialized SDPA for sliding-window (array-mask) layers. Set by the
|
|
614
|
+
// trainer; null = inference (use ops.sdpa as before).
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
let trainAttnMode: "flash" | "manual" | null = null;
|
|
618
|
+
export function setTrainingAttn(mode: "flash" | "manual" | null): void {
|
|
619
|
+
trainAttnMode = mode;
|
|
620
|
+
}
|
|
621
|
+
export function getTrainingAttn(): "flash" | "manual" | null {
|
|
622
|
+
return trainAttnMode;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Can the flash kernel handle this q? (head_dim supported, seq≥32, fp16/bf16.) */
|
|
626
|
+
export function flashSupported(q: MlxArray): boolean {
|
|
627
|
+
const T = q.shape[2]!;
|
|
628
|
+
const D = q.shape[3]!;
|
|
629
|
+
return SUPPORTED_HEAD_DIMS.has(D) && T >= 32 &&
|
|
630
|
+
(q.dtype === Dtype.float16 || q.dtype === Dtype.bfloat16);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Materialized differentiable attention (matmul → mask → softmax → matmul),
|
|
634
|
+
* GQA-aware. The correct-vjp fallback for masks flash doesn't take (sliding
|
|
635
|
+
* window) and for any case where ops.sdpa's broken dK can't be used. O(L²) —
|
|
636
|
+
* pair with gradient checkpointing. */
|
|
637
|
+
export function manualSdpa(q: MlxArray, k: MlxArray, v: MlxArray, scale: number, mask: Mask): MlxArray {
|
|
638
|
+
const [B, Hq, T, Dh] = q.shape as [number, number, number, number];
|
|
639
|
+
const Hkv = k.shape[1]!;
|
|
640
|
+
const Tk = k.shape[2]!;
|
|
641
|
+
const reps = Hq / Hkv;
|
|
642
|
+
|
|
643
|
+
const q5 = ops.reshape(q, [B, Hkv, reps, T, Dh]);
|
|
644
|
+
const k5 = ops.reshape(k, [B, Hkv, 1, Tk, Dh]);
|
|
645
|
+
const v5 = ops.reshape(v, [B, Hkv, 1, Tk, Dh]);
|
|
646
|
+
const kT = ops.transposeAxes(k5, [0, 1, 2, 4, 3]); // [B,Hkv,1,Dh,Tk]
|
|
647
|
+
let scores = ops.matmul(q5, kT); // [B,Hkv,reps,T,Tk]
|
|
648
|
+
k5.dispose();
|
|
649
|
+
kT.dispose();
|
|
650
|
+
if (scale !== 1.0) {
|
|
651
|
+
const s2 = ops.mulScalar(scores, scale);
|
|
652
|
+
scores.dispose();
|
|
653
|
+
scores = s2;
|
|
654
|
+
}
|
|
655
|
+
if (mask.mode === "causal" || mask.mode === "array") {
|
|
656
|
+
const mBool = mask.mode === "causal" ? createCausalMask(T, 0, null) : mask.arr!;
|
|
657
|
+
const ninf = ops.scalarLike(-3.0e38, scores);
|
|
658
|
+
const masked = ops.where(mBool, scores, ninf);
|
|
659
|
+
scores.dispose();
|
|
660
|
+
ninf.dispose();
|
|
661
|
+
if (mask.mode === "causal") mBool.dispose();
|
|
662
|
+
scores = masked;
|
|
663
|
+
}
|
|
664
|
+
const probs = ops.softmaxAxis(scores, -1, true);
|
|
665
|
+
scores.dispose();
|
|
666
|
+
const out5 = ops.matmul(probs, v5); // [B,Hkv,reps,T,Dh]
|
|
667
|
+
probs.dispose();
|
|
668
|
+
v5.dispose();
|
|
669
|
+
q5.dispose();
|
|
670
|
+
const out = ops.reshape(out5, [B, Hq, T, Dh]);
|
|
671
|
+
out5.dispose();
|
|
672
|
+
return out;
|
|
673
|
+
}
|
|
674
|
+
|