mlx-bun 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
package/src/model/gemma4-base.ts
CHANGED
|
@@ -7,15 +7,19 @@
|
|
|
7
7
|
// DecoderLayer, Gemma4Model) stays in gemma4.ts and becomes the
|
|
8
8
|
// per-model generated code in Phase C.
|
|
9
9
|
|
|
10
|
+
import { decodedKvDonorAttention } from "./decoded-kv-donor";
|
|
10
11
|
import type { ModelConfig } from "../config";
|
|
11
12
|
import { quantFor } from "../config";
|
|
12
13
|
import type { Weights } from "../weights";
|
|
13
14
|
import { MlxArray } from "../mlx/array";
|
|
14
|
-
import { Dtype } from "../mlx/ffi";
|
|
15
|
+
import { Dtype, deviceArchitecture } from "../mlx/ffi";
|
|
15
16
|
import * as ops from "../mlx/ops";
|
|
16
17
|
import { quantizedMatmulRows } from "../mlx/quantized-rows";
|
|
17
18
|
import { expertOffloadArray } from "../expert-offload";
|
|
18
19
|
import * as tq from "../mlx/turboquant-ops";
|
|
20
|
+
import type { KvCodec } from "../backends/mlx/kv-codec";
|
|
21
|
+
import { TurboQuantCodec, turboQuantFusedDecode, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
22
|
+
export { disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
19
23
|
import { runtimeValue } from "../runtime-config";
|
|
20
24
|
|
|
21
25
|
export type MaskMode = "" | "causal";
|
|
@@ -249,9 +253,9 @@ export class QuantizedEmbedding {
|
|
|
249
253
|
|
|
250
254
|
/** Fetched KV handed from a donor layer to its sharers (and to the
|
|
251
255
|
* speculative drafter). Owned by forwardLayers for the pass.
|
|
252
|
-
* `offsetArr` is
|
|
253
|
-
*
|
|
254
|
-
*
|
|
256
|
+
* `offsetArr` is an owned pre-write position snapshot. Donor and sharing
|
|
257
|
+
* attention layers keep it until the fetched KV is released, even if the
|
|
258
|
+
* cache advances or replaces its borrowed row-position array. */
|
|
255
259
|
export type SharedKv =
|
|
256
260
|
| { kind: "plain"; keys: MlxArray; values: MlxArray; offset: number;
|
|
257
261
|
offsetArr?: MlxArray;
|
|
@@ -260,11 +264,90 @@ export type SharedKv =
|
|
|
260
264
|
* (tq.unrotateValues) after sdpa. Attention is linear in V, so the
|
|
261
265
|
* result is the same decode, just transformed once per query row
|
|
262
266
|
* instead of once per cached token. */
|
|
263
|
-
vRotated?: boolean
|
|
267
|
+
vRotated?: boolean;
|
|
268
|
+
restoreValues?: (output: MlxArray) => MlxArray }
|
|
264
269
|
| { kind: "quant"; keys: ops.QuantizedTensor; values: ops.QuantizedTensor;
|
|
265
|
-
offset: number; groupSize: number; bits: number; offsetArr?: MlxArray }
|
|
270
|
+
offset: number; groupSize: number; bits: number; offsetArr?: MlxArray }
|
|
271
|
+
| { kind: "view"; attention: KvAttentionView; offset: number; offsetArr?: MlxArray };
|
|
272
|
+
|
|
273
|
+
/** Retain an independent array handle for positions used across cache writes.
|
|
274
|
+
* The contiguous view preserves the integer values and shares their storage;
|
|
275
|
+
* callers release it with the fetched attention state. */
|
|
276
|
+
export function captureRopeOffsets(cache: Pick<Cache, "ropeOffsetArr">): MlxArray | undefined {
|
|
277
|
+
const offsets = cache.ropeOffsetArr;
|
|
278
|
+
return offsets ? ops.contiguous(offsets) : undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Quantized attention consumes a numerical storage port, independent of the
|
|
282
|
+
* concrete layout used for row positions, retention and persistence. */
|
|
283
|
+
export interface QuantizedAttentionState {
|
|
284
|
+
readonly groupSize: number;
|
|
285
|
+
readonly bits: number;
|
|
286
|
+
updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Values may stay in the codec's rotated domain until after attention. */
|
|
290
|
+
export interface RotatedValueAttentionState {
|
|
291
|
+
/** Capture a fetched value domain independently of later row changes. */
|
|
292
|
+
captureValueTransform?(): (output: MlxArray) => MlxArray;
|
|
293
|
+
updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** A captured numerical view supports multiple queries without appending KV
|
|
297
|
+
* again. It owns its tensor handles independently of later cache membership or
|
|
298
|
+
* precision changes. Query/mask inputs are borrowed; outputs are owned. */
|
|
299
|
+
export interface KvAttentionView {
|
|
300
|
+
/** Qualified committed spans retain the single-position reduction order. */
|
|
301
|
+
attend(q: MlxArray, scale: number, mask: Mask, independentPositions?: boolean): MlxArray;
|
|
302
|
+
dispose(): void;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Storage appends once and hands every attention consumer the same view. */
|
|
306
|
+
export interface KvAttentionState {
|
|
307
|
+
appendAndFetch(k: MlxArray, v: MlxArray): KvAttentionView;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Owned read-only planes with validity separate from physical padding. */
|
|
311
|
+
export interface KvDonorRows {
|
|
312
|
+
readonly keys: MlxArray;
|
|
313
|
+
readonly values: MlxArray;
|
|
314
|
+
readonly offsets: readonly number[];
|
|
315
|
+
readonly starts: readonly number[];
|
|
316
|
+
readonly ends: readonly number[];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Captured Q-only attention hides plain or encoded donor storage. */
|
|
320
|
+
export interface KvDonorAttention extends KvAttentionView {
|
|
321
|
+
readonly width: number;
|
|
322
|
+
readonly dtype: Dtype;
|
|
323
|
+
readonly offsets: readonly number[];
|
|
324
|
+
readonly starts: readonly number[];
|
|
325
|
+
readonly ends: readonly number[];
|
|
326
|
+
}
|
|
266
327
|
|
|
267
328
|
export interface Cache {
|
|
329
|
+
/** Maximum committed positions before this state changes precision. */
|
|
330
|
+
maxAppendTokens?(): number;
|
|
331
|
+
captureDonorRows?(): KvDonorRows;
|
|
332
|
+
captureDonorAttention?(): KvDonorAttention;
|
|
333
|
+
/** A preparation method commits each request's own precision boundary.
|
|
334
|
+
* Splitting a forward for a sibling must not move that boundary. */
|
|
335
|
+
readonly prefillMaintenance?: {
|
|
336
|
+
beginPrefill(): void;
|
|
337
|
+
commitPrefill(rows: readonly number[]): void;
|
|
338
|
+
endPrefill(): void;
|
|
339
|
+
};
|
|
340
|
+
/** Optional representation-owned affine conversion. Logical position can
|
|
341
|
+
* differ from the physical write head in a padded rotating layout. */
|
|
342
|
+
readonly affineConversion?: { readonly offset: number; toQuantized(groupSize: number, bits: number): Cache };
|
|
343
|
+
readonly turboConversion?: { readonly offset: number; toTurboQuantized(kBits: number, vBits: number): Cache };
|
|
344
|
+
readonly attentionState?: KvAttentionState;
|
|
345
|
+
/** Earliest prefix whose retained representation can resume generation.
|
|
346
|
+
* Irreversible transitions may preserve bytes while invalidating older
|
|
347
|
+
* precision boundaries. Reuse policy must not trim below this offset. */
|
|
348
|
+
minimumReusableOffset?: number;
|
|
349
|
+
readonly rotatedValueAttention?: RotatedValueAttentionState;
|
|
350
|
+
readonly quantizedAttention?: QuantizedAttentionState;
|
|
268
351
|
offset: number;
|
|
269
352
|
/** Stable storage identity for compatibility guards and persistence.
|
|
270
353
|
* REQUIRED: every capability predicate (isPlainKvCache, isRotating*,
|
|
@@ -364,6 +447,19 @@ export interface BatchableCache extends Cache {
|
|
|
364
447
|
projectedBytes(tokens: number): number;
|
|
365
448
|
}
|
|
366
449
|
|
|
450
|
+
/** Padding is interpreted by cache geometry, independently of scheduling.
|
|
451
|
+
* Left padding initializes an empty group; lengths count input columns before
|
|
452
|
+
* trailing padding. Finalize before ordinary decode or publishing checkpoints. */
|
|
453
|
+
export interface PrefillPadding {
|
|
454
|
+
readonly leftPadding?: readonly number[];
|
|
455
|
+
readonly lengths: readonly number[];
|
|
456
|
+
readonly rightPadding?: readonly number[];
|
|
457
|
+
}
|
|
458
|
+
export interface PaddedPrefillCache extends Cache {
|
|
459
|
+
preparePrefill(padding: PrefillPadding): void;
|
|
460
|
+
finalizePrefill(): void;
|
|
461
|
+
}
|
|
462
|
+
|
|
367
463
|
export function isBatchableCache(cache: Cache): cache is BatchableCache {
|
|
368
464
|
const candidate = cache as Partial<BatchableCache>;
|
|
369
465
|
return (
|
|
@@ -487,6 +583,13 @@ export class KVCache implements Cache {
|
|
|
487
583
|
}
|
|
488
584
|
|
|
489
585
|
/** Chronological (K, V) view sliced to offset (drafter donor read). */
|
|
586
|
+
captureDonorRows(): KvDonorRows {
|
|
587
|
+
const [keys, values] = this.temporalView();
|
|
588
|
+
const B = keys.shape[0]!, width = keys.shape[2]!;
|
|
589
|
+
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
590
|
+
starts: Array(B).fill(0), ends: Array(B).fill(width) };
|
|
591
|
+
}
|
|
592
|
+
|
|
490
593
|
temporalView(): [MlxArray, MlxArray] {
|
|
491
594
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
492
595
|
const [B, H, , D] = this.keys.shape as [number, number, number, number];
|
|
@@ -578,6 +681,7 @@ export class KVCache implements Cache {
|
|
|
578
681
|
* quantization is NYI upstream; sliding layers are window-capped
|
|
579
682
|
* anyway). Attention dispatches to quantizedSdpa for these. */
|
|
580
683
|
export class QuantizedKVCache implements Cache {
|
|
684
|
+
minimumReusableOffset?: number;
|
|
581
685
|
static readonly STEP = 256;
|
|
582
686
|
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
583
687
|
readonly ropeOffsetArr?: MlxArray;
|
|
@@ -587,6 +691,8 @@ export class QuantizedKVCache implements Cache {
|
|
|
587
691
|
|
|
588
692
|
constructor(readonly groupSize: number, readonly bits: number) {}
|
|
589
693
|
|
|
694
|
+
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
695
|
+
|
|
590
696
|
signature(): string { return `kv:quant:${this.bits}:${this.groupSize}`; }
|
|
591
697
|
|
|
592
698
|
bytesPerToken(): number {
|
|
@@ -774,6 +880,16 @@ export class QuantizedKVCache implements Cache {
|
|
|
774
880
|
/** Chronological (K, V) triples sliced to offset — the quantized twin of
|
|
775
881
|
* KVCache.temporalView (batched merge/extend/filter reads). Caller owns
|
|
776
882
|
* the returned views. */
|
|
883
|
+
captureDonorAttention(): KvDonorAttention {
|
|
884
|
+
const [keys, values] = this.temporalView();
|
|
885
|
+
const B = keys.packed.shape[0]!, width = keys.packed.shape[2]!;
|
|
886
|
+
return { width, dtype: keys.scales.dtype, offsets: Array(B).fill(this.offset),
|
|
887
|
+
starts: Array(B).fill(0), ends: Array(B).fill(width),
|
|
888
|
+
attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, this.groupSize, this.bits),
|
|
889
|
+
dispose() { disposeTriple(keys); disposeTriple(values); },
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
777
893
|
temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
778
894
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
779
895
|
const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
@@ -798,6 +914,7 @@ export class QuantizedKVCache implements Cache {
|
|
|
798
914
|
* entries, so decode attends over at most the window. RoPE offsets use
|
|
799
915
|
* the true position; masks use the buffer-clamped offset. */
|
|
800
916
|
export class RotatingKVCache implements Cache {
|
|
917
|
+
declare minimumReusableOffset?: number;
|
|
801
918
|
static readonly STEP = 256;
|
|
802
919
|
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
803
920
|
readonly ropeOffsetArr?: MlxArray;
|
|
@@ -1065,6 +1182,13 @@ export class RotatingKVCache implements Cache {
|
|
|
1065
1182
|
|
|
1066
1183
|
/** Chronological (K, V) view, valid length min(offset, maxSize)
|
|
1067
1184
|
* (port of optiq kv_view._read_cache_temporal). */
|
|
1185
|
+
captureDonorRows(): KvDonorRows {
|
|
1186
|
+
const [keys, values] = this.temporalView();
|
|
1187
|
+
const B = keys.shape[0]!, width = keys.shape[2]!;
|
|
1188
|
+
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
1189
|
+
starts: Array(B).fill(0), ends: Array(B).fill(width) };
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1068
1192
|
temporalView(): [MlxArray, MlxArray] {
|
|
1069
1193
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
1070
1194
|
const tk = this.#temporalOrder(this.keys);
|
|
@@ -1133,6 +1257,8 @@ export const disposeTriple = (t: ops.QuantizedTensor): void => {
|
|
|
1133
1257
|
* + SDPA patches are unnecessary here: our SharedKv carries
|
|
1134
1258
|
* groupSize/bits through the donor→sharer plumbing explicitly. */
|
|
1135
1259
|
export class RotatingQuantizedKVCache implements Cache {
|
|
1260
|
+
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
1261
|
+
declare minimumReusableOffset?: number;
|
|
1136
1262
|
static readonly STEP = 256;
|
|
1137
1263
|
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
1138
1264
|
readonly ropeOffsetArr?: MlxArray;
|
|
@@ -1390,6 +1516,16 @@ export class RotatingQuantizedKVCache implements Cache {
|
|
|
1390
1516
|
/** Chronological (K, V) triples cut to the valid window — the quantized
|
|
1391
1517
|
* twin of RotatingKVCache.temporalView (batched merge reads, Phase 3
|
|
1392
1518
|
* milestone 2). Caller owns the returned views. */
|
|
1519
|
+
captureDonorAttention(): KvDonorAttention {
|
|
1520
|
+
const [keys, values] = this.temporalView();
|
|
1521
|
+
const B = keys.packed.shape[0]!, width = keys.packed.shape[2]!;
|
|
1522
|
+
return { width, dtype: keys.scales.dtype, offsets: Array(B).fill(this.offset),
|
|
1523
|
+
starts: Array(B).fill(0), ends: Array(B).fill(width),
|
|
1524
|
+
attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, this.groupSize, this.bits),
|
|
1525
|
+
dispose() { disposeTriple(keys); disposeTriple(values); },
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1393
1529
|
temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
1394
1530
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
1395
1531
|
const valid = Math.min(this.offset, this.maxSize);
|
|
@@ -1504,28 +1640,6 @@ export class RotatingQuantizedKVCache implements Cache {
|
|
|
1504
1640
|
}
|
|
1505
1641
|
}
|
|
1506
1642
|
|
|
1507
|
-
/** One TurboQuant-encoded (K, V) storage tuple — the 5 arrays kv-store.ts
|
|
1508
|
-
* and toQuantized/fromKVCache pass around. Not `ops.QuantizedTensor`
|
|
1509
|
-
* (mlx's affine int4/int8 scheme): this is the rotation + Lloyd-Max
|
|
1510
|
-
* layout (docs/design/turboquant.md), asymmetric-affine for keys,
|
|
1511
|
-
* FWHT+Lloyd-Max for values. Field order is the kv-store.ts tensor-slot
|
|
1512
|
-
* contract — do not reorder without updating snapshotCache/loadKvCache. */
|
|
1513
|
-
export interface TurboQuantTensor {
|
|
1514
|
-
kIdx: MlxArray;
|
|
1515
|
-
kScales: MlxArray;
|
|
1516
|
-
kZeros: MlxArray;
|
|
1517
|
-
vPacked: MlxArray;
|
|
1518
|
-
vScales: MlxArray;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
export const disposeTurboQuant = (t: TurboQuantTensor): void => {
|
|
1522
|
-
t.kIdx.dispose();
|
|
1523
|
-
t.kScales.dispose();
|
|
1524
|
-
t.kZeros.dispose();
|
|
1525
|
-
t.vPacked.dispose();
|
|
1526
|
-
t.vScales.dispose();
|
|
1527
|
-
};
|
|
1528
|
-
|
|
1529
1643
|
/** TurboQuant KV cache — v1 (docs/design/turboquant.md): dequantize-
|
|
1530
1644
|
* on-fetch. Deliberately does NOT subclass KVCache/RotatingKVCache (or
|
|
1531
1645
|
* QuantizedKVCache) — a novel class fails every generated-file
|
|
@@ -1545,6 +1659,7 @@ export const disposeTurboQuant = (t: TurboQuantTensor): void => {
|
|
|
1545
1659
|
* a full-window dequant every step; the deferred-InvFWHT trick is a
|
|
1546
1660
|
* documented non-goal until the quality gate passes. */
|
|
1547
1661
|
export class TurboQuantKVCache implements Cache {
|
|
1662
|
+
minimumReusableOffset = 0;
|
|
1548
1663
|
readonly stateNeedsDispose = true;
|
|
1549
1664
|
static readonly STEP = 256;
|
|
1550
1665
|
/** Set only by compiled-decode trace adapters (see Cache) — TurboQuant
|
|
@@ -1557,10 +1672,23 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1557
1672
|
/** Validated lazily on first update, once head_dim is known. */
|
|
1558
1673
|
#headDim: number | null = null;
|
|
1559
1674
|
/** Experimental operation selection, captured once for this cache. */
|
|
1560
|
-
readonly #
|
|
1675
|
+
readonly #codec: KvCodec<TurboQuantTensor>;
|
|
1561
1676
|
|
|
1562
|
-
constructor(readonly kBits: number, readonly vBits: number
|
|
1677
|
+
constructor(readonly kBits: number, readonly vBits: number,
|
|
1678
|
+
readonly fusedDecode = turboQuantFusedDecode()) {
|
|
1679
|
+
this.#codec = new TurboQuantCodec(kBits, vBits, fusedDecode);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
captureDonorRows(): KvDonorRows {
|
|
1683
|
+
if (!this.#kv || this.#headDim === null) throw new Error("cache is empty");
|
|
1684
|
+
const [keys, values] = this.#decode(this.#kv, this.offset, this.#headDim, false);
|
|
1685
|
+
const B = keys.shape[0]!;
|
|
1686
|
+
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
1687
|
+
starts: Array(B).fill(0), ends: Array(B).fill(this.offset) };
|
|
1688
|
+
}
|
|
1689
|
+
captureDonorAttention(): KvDonorAttention { return decodedKvDonorAttention(this.captureDonorRows()); }
|
|
1563
1690
|
|
|
1691
|
+
get rotatedValueAttention(): RotatedValueAttentionState { return this; }
|
|
1564
1692
|
signature(): string { return `kv:turboquant:${this.kBits}:${this.vBits}`; }
|
|
1565
1693
|
|
|
1566
1694
|
bytesPerToken(): number {
|
|
@@ -1612,55 +1740,9 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1612
1740
|
return this.#kv ? this.#kv.kIdx.shape[2]! : 0;
|
|
1613
1741
|
}
|
|
1614
1742
|
|
|
1615
|
-
|
|
1616
|
-
#encode(k: MlxArray, v: MlxArray): TurboQuantTensor {
|
|
1617
|
-
const signed = this.kBits === 8;
|
|
1618
|
-
const kEnc = tq.encodeKeys(k, this.kBits, signed);
|
|
1619
|
-
const kIdxPacked = this.kBits < 8 ? tq.packBits(kEnc.indices, this.kBits) : kEnc.indices;
|
|
1620
|
-
if (kIdxPacked !== kEnc.indices) kEnc.indices.dispose();
|
|
1621
|
-
const vEnc = tq.encodeValues(v, this.vBits);
|
|
1622
|
-
const vIdxPacked = this.vBits < 8 ? tq.packBits(vEnc.indices, this.vBits) : vEnc.indices;
|
|
1623
|
-
if (vIdxPacked !== vEnc.indices) vEnc.indices.dispose();
|
|
1624
|
-
return {
|
|
1625
|
-
kIdx: kIdxPacked, kScales: kEnc.scales, kZeros: kEnc.zeros,
|
|
1626
|
-
vPacked: vIdxPacked, vScales: vEnc.scales,
|
|
1627
|
-
};
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
/** Decode a TurboQuantTensor's active [B,H,upTo,*] window back to
|
|
1631
|
-
* bf16 [B,H,upTo,headDim] (k, v). Caller owns the returned arrays.
|
|
1632
|
-
* deferV leaves V in the rotated domain (decodeValuesRotated) — the
|
|
1633
|
-
* caller un-rotates its attention output instead (tq.unrotateValues). */
|
|
1743
|
+
#encode(k: MlxArray, v: MlxArray): TurboQuantTensor { return this.#codec.encode(k, v); }
|
|
1634
1744
|
#decode(t: TurboQuantTensor, upTo: number, headDim: number, deferV = false): [MlxArray, MlxArray] {
|
|
1635
|
-
|
|
1636
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1637
|
-
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
1638
|
-
};
|
|
1639
|
-
if (this.#fusedDecode) {
|
|
1640
|
-
const inputs = [cut(t.kIdx), cut(t.kScales), cut(t.kZeros), cut(t.vPacked), cut(t.vScales)] as const;
|
|
1641
|
-
try {
|
|
1642
|
-
const decoded = tq.tryDecodePackedKv(inputs, this.kBits, this.vBits, headDim, deferV);
|
|
1643
|
-
if (decoded) return decoded;
|
|
1644
|
-
} finally { for (const input of inputs) input.dispose(); }
|
|
1645
|
-
}
|
|
1646
|
-
const kIdxCut = cut(t.kIdx);
|
|
1647
|
-
const kIdxUnpacked = this.kBits < 8 ? tq.unpackBits(kIdxCut, this.kBits, headDim) : kIdxCut;
|
|
1648
|
-
if (kIdxUnpacked !== kIdxCut) kIdxCut.dispose();
|
|
1649
|
-
const kScalesCut = cut(t.kScales);
|
|
1650
|
-
const kZerosCut = cut(t.kZeros);
|
|
1651
|
-
const k = tq.decodeKeys(kIdxUnpacked, kScalesCut, kZerosCut);
|
|
1652
|
-
for (const a of [kIdxUnpacked, kScalesCut, kZerosCut]) a.dispose();
|
|
1653
|
-
|
|
1654
|
-
const vPackedCut = cut(t.vPacked);
|
|
1655
|
-
const vIdxUnpacked = this.vBits < 8 ? tq.unpackBits(vPackedCut, this.vBits, headDim) : vPackedCut;
|
|
1656
|
-
if (vIdxUnpacked !== vPackedCut) vPackedCut.dispose();
|
|
1657
|
-
const vScalesCut = cut(t.vScales);
|
|
1658
|
-
const v = deferV
|
|
1659
|
-
? tq.decodeValuesRotated(vIdxUnpacked, vScalesCut, this.vBits)
|
|
1660
|
-
: tq.decodeValues(vIdxUnpacked, vScalesCut, this.vBits);
|
|
1661
|
-
for (const a of [vIdxUnpacked, vScalesCut]) a.dispose();
|
|
1662
|
-
|
|
1663
|
-
return [k, v];
|
|
1745
|
+
return this.#codec.decode(t, upTo, headDim, deferV);
|
|
1664
1746
|
}
|
|
1665
1747
|
|
|
1666
1748
|
/** Quantize the newly-appended k/v, append into growable packed
|
|
@@ -1805,6 +1887,7 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1805
1887
|
q.#headDim = D;
|
|
1806
1888
|
}
|
|
1807
1889
|
q.offset = cache.offset;
|
|
1890
|
+
q.minimumReusableOffset = (cache as Cache).minimumReusableOffset ?? 0;
|
|
1808
1891
|
cache.dispose();
|
|
1809
1892
|
return q;
|
|
1810
1893
|
}
|
|
@@ -1813,6 +1896,7 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1813
1896
|
if (this.#kv) disposeTurboQuant(this.#kv);
|
|
1814
1897
|
this.#kv = null;
|
|
1815
1898
|
this.offset = 0;
|
|
1899
|
+
this.minimumReusableOffset = 0;
|
|
1816
1900
|
}
|
|
1817
1901
|
}
|
|
1818
1902
|
|
|
@@ -1860,8 +1944,23 @@ export function quantizedSdpaUnfused(
|
|
|
1860
1944
|
vT = expand(vq);
|
|
1861
1945
|
}
|
|
1862
1946
|
|
|
1863
|
-
|
|
1947
|
+
// M4 Pro's native key matvec retains exact arithmetic when three GQA
|
|
1948
|
+
// heads share a batch. Restore score geometry before softmax/value work.
|
|
1949
|
+
const groupHeads = B <= 2 && H === 24 && KV === 4 && L === 3 && D === 256 &&
|
|
1950
|
+
N >= 8192 && groupSize === 64 && bits === 4 &&
|
|
1951
|
+
(q.dtype === Dtype.bfloat16 || q.dtype === Dtype.float32) &&
|
|
1952
|
+
deviceArchitecture() === "applegpu_g16s";
|
|
1953
|
+
let keyQueries = queries;
|
|
1954
|
+
if (groupHeads) {
|
|
1955
|
+
keyQueries = ops.reshape(queries, [B, KV, 2, 9, D]);
|
|
1956
|
+
owned.push(keyQueries);
|
|
1957
|
+
}
|
|
1958
|
+
let scores = ops.quantizedMatmulQT(keyQueries, kT, true, groupSize, bits);
|
|
1864
1959
|
owned.push(scores);
|
|
1960
|
+
if (groupHeads) {
|
|
1961
|
+
scores = ops.reshape(scores, [B, KV, nRep, L, N]);
|
|
1962
|
+
owned.push(scores);
|
|
1963
|
+
}
|
|
1865
1964
|
|
|
1866
1965
|
let maskArr: MlxArray | null = null;
|
|
1867
1966
|
let ownsMask = false;
|
|
@@ -1885,9 +1984,11 @@ export function quantizedSdpaUnfused(
|
|
|
1885
1984
|
}
|
|
1886
1985
|
}
|
|
1887
1986
|
if (maskArr) {
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1987
|
+
let masked: MlxArray;
|
|
1988
|
+
if (maskArr.dtype === Dtype.bool) {
|
|
1989
|
+
using ninf = ops.scalarLike(FINFO_MIN[scores.dtype] ?? -3.4e38, scores);
|
|
1990
|
+
masked = ops.where(maskArr, scores, ninf);
|
|
1991
|
+
} else masked = ops.add(scores, maskArr);
|
|
1891
1992
|
if (ownsMask) maskArr.dispose();
|
|
1892
1993
|
owned.push(masked);
|
|
1893
1994
|
scores = masked;
|
package/src/model/gemma4.ts
CHANGED
|
@@ -45,18 +45,19 @@ import {
|
|
|
45
45
|
RMSNorm,
|
|
46
46
|
RotatingKVCache,
|
|
47
47
|
RotatingQuantizedKVCache,
|
|
48
|
-
TurboQuantKVCache,
|
|
49
48
|
quantizedSdpa,
|
|
50
49
|
type Cache,
|
|
51
50
|
type Mask,
|
|
52
51
|
type SharedKv,
|
|
52
|
+
captureRopeOffsets,
|
|
53
53
|
type LoraWeights,
|
|
54
54
|
} from "./gemma4-base";
|
|
55
|
+
import { mapPackedTokens, type TokenGroup } from "./token-groups";
|
|
55
56
|
import { Checkpoint } from "../mlx/checkpoint";
|
|
56
57
|
import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
|
|
57
58
|
import { unrotateValues as tqUnrotateValues } from "../mlx/turboquant-ops";
|
|
58
59
|
import { CompiledFunction } from "../mlx/compile";
|
|
59
|
-
import {
|
|
60
|
+
import { runtimeFlag } from "../runtime-config";
|
|
60
61
|
|
|
61
62
|
/** Verbatim port of mlx_lm/models/gemma4_text.py:
|
|
62
63
|
* `@partial(mx.compile, shapeless=True) def geglu(gate, x): return nn.gelu_approx(gate) * x`
|
|
@@ -77,7 +78,7 @@ import { flagOn } from "../runtime-config";
|
|
|
77
78
|
* ~9-op path. `MLX_BUN_COMPILED_GEGLU=0` (--compiled-activations off) selects the
|
|
78
79
|
* uncompiled composition — same L1 parity, slower (the A/B opt-out). */
|
|
79
80
|
export function compiledGegluActive(): boolean {
|
|
80
|
-
return
|
|
81
|
+
return runtimeFlag("MLX_BUN_COMPILED_GEGLU", true);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
let _gegluClosure: CompiledFunction | null = null;
|
|
@@ -304,15 +305,11 @@ class Attention {
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
const offset = cache.offset;
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
// prerequisite for batched decode, where ropeOffsetArr carries per-row
|
|
313
|
-
// positions derived from the pre-write offset and MUST be identical for
|
|
314
|
-
// this step's K and Q.
|
|
315
|
-
const offsetArr = cache.ropeOffsetArr;
|
|
308
|
+
// Own the pre-write positions through every query and shared-KV
|
|
309
|
+
// consumer. Advancing a row cache may replace and release its borrowed
|
|
310
|
+
// offset array before Q's RoPE is built. The fetched shared state owns
|
|
311
|
+
// this retained handle and releases it with K/V at the end of the pass.
|
|
312
|
+
const offsetArr = captureRopeOffsets(cache);
|
|
316
313
|
|
|
317
314
|
const kNormed = this.kNorm!.forward(k);
|
|
318
315
|
const kT = ops.transposeAxes(kNormed, [0, 2, 1, 3]);
|
|
@@ -328,23 +325,29 @@ class Attention {
|
|
|
328
325
|
if (v !== k) v.dispose();
|
|
329
326
|
k.dispose();
|
|
330
327
|
|
|
331
|
-
if (cache
|
|
332
|
-
const
|
|
328
|
+
if (cache.attentionState) {
|
|
329
|
+
const attention = cache.attentionState.appendAndFetch(kRoped, vT);
|
|
330
|
+
kRoped.dispose(); vT.dispose();
|
|
331
|
+
shared = { kind: "view", attention, offset, offsetArr };
|
|
332
|
+
} else if (cache.quantizedAttention) {
|
|
333
|
+
const quantized = cache.quantizedAttention;
|
|
334
|
+
const [kq, vq] = quantized.updateAndFetchQuantized(kRoped, vT);
|
|
333
335
|
kRoped.dispose();
|
|
334
336
|
vT.dispose();
|
|
335
337
|
shared = {
|
|
336
338
|
kind: "quant", keys: kq, values: vq, offset,
|
|
337
|
-
groupSize:
|
|
339
|
+
groupSize: quantized.groupSize, bits: quantized.bits,
|
|
338
340
|
offsetArr,
|
|
339
341
|
};
|
|
340
|
-
} else if (cache
|
|
342
|
+
} else if (cache.rotatedValueAttention) {
|
|
341
343
|
// Deferred-V read: values stay rotated; the attention output is
|
|
342
344
|
// un-rotated below (shared.vRotated) — including by KV-shared
|
|
343
345
|
// consumer layers, which see the flag through sharedIn.
|
|
344
|
-
const [keys, values] = cache.updateAndFetchDeferredV(kRoped, vT);
|
|
346
|
+
const [keys, values] = cache.rotatedValueAttention.updateAndFetchDeferredV(kRoped, vT);
|
|
345
347
|
kRoped.dispose();
|
|
346
348
|
vT.dispose();
|
|
347
|
-
shared = { kind: "plain", keys, values, offset, offsetArr, vRotated: true
|
|
349
|
+
shared = { kind: "plain", keys, values, offset, offsetArr, vRotated: true,
|
|
350
|
+
restoreValues: cache.rotatedValueAttention.captureValueTransform?.() };
|
|
348
351
|
} else {
|
|
349
352
|
const [keys, values] = cache.updateAndFetch(kRoped, vT);
|
|
350
353
|
kRoped.dispose();
|
|
@@ -360,7 +363,9 @@ class Attention {
|
|
|
360
363
|
|
|
361
364
|
let attn: MlxArray;
|
|
362
365
|
const ta = getTrainingAttn();
|
|
363
|
-
if (shared.kind === "
|
|
366
|
+
if (shared.kind === "view") {
|
|
367
|
+
attn = shared.attention.attend(q, 1.0, mask);
|
|
368
|
+
} else if (shared.kind === "quant") {
|
|
364
369
|
attn = quantizedSdpa(q, shared.keys, shared.values, 1.0, mask, shared.groupSize, shared.bits);
|
|
365
370
|
} else if (
|
|
366
371
|
ta === "flash" && flashSupported(q) && (mask.mode === "causal" || mask.mode === "array")
|
|
@@ -383,7 +388,7 @@ class Attention {
|
|
|
383
388
|
if (shared.kind === "plain" && shared.vRotated) {
|
|
384
389
|
// Attention is linear in V: one InvFWHT on the [B,H,L,D] output
|
|
385
390
|
// replaces one per cached token (TurboQuant deferred-V).
|
|
386
|
-
attn = disposing(attn, tqUnrotateValues(attn));
|
|
391
|
+
attn = disposing(attn, shared.restoreValues ? shared.restoreValues(attn) : tqUnrotateValues(attn));
|
|
387
392
|
}
|
|
388
393
|
const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
389
394
|
attn.dispose();
|
|
@@ -851,6 +856,13 @@ export class Gemma4Model {
|
|
|
851
856
|
);
|
|
852
857
|
}
|
|
853
858
|
|
|
859
|
+
/** Computed model constants must retain their constructor arithmetic when
|
|
860
|
+
* a restored request enters compiled decode without a prefill first. */
|
|
861
|
+
materializeGraphConstants(): void {
|
|
862
|
+
const constants = this.layers.flatMap(layer => layer.attn.ropeFreqs ? [layer.attn.ropeFreqs] : []);
|
|
863
|
+
if (constants.length) ops.evalAll(constants);
|
|
864
|
+
}
|
|
865
|
+
|
|
854
866
|
/** ids [1, L] → final-norm hidden states [1, L, hidden]. */
|
|
855
867
|
forwardHidden(ids: MlxArray, cache: Cache[]): MlxArray {
|
|
856
868
|
let h = this.embed.encode(ids);
|
|
@@ -1000,13 +1012,7 @@ export class Gemma4Model {
|
|
|
1000
1012
|
for (let i = 0; i < this.numDonors; i++) {
|
|
1001
1013
|
const s = intermediates[i];
|
|
1002
1014
|
if (!s) continue;
|
|
1003
|
-
|
|
1004
|
-
s.keys.dispose();
|
|
1005
|
-
s.values.dispose();
|
|
1006
|
-
} else {
|
|
1007
|
-
for (const t of [s.keys, s.values])
|
|
1008
|
-
for (const a of [t.packed, t.scales, t.biases]) a.dispose();
|
|
1009
|
-
}
|
|
1015
|
+
Gemma4Model.disposeSharedKv(s);
|
|
1010
1016
|
}
|
|
1011
1017
|
for (const m of masks.values()) m.arr?.dispose();
|
|
1012
1018
|
|
|
@@ -1015,6 +1021,85 @@ export class Gemma4Model {
|
|
|
1015
1021
|
return h;
|
|
1016
1022
|
}
|
|
1017
1023
|
|
|
1024
|
+
/** Mixed token execution: attention retains each group's cache, positions and
|
|
1025
|
+
* SDPA shape; feed-forward work packs only real tokens. Kernel selection for
|
|
1026
|
+
* packed matmuls can differ from a solo GEMV, so this is an explicit Lab port
|
|
1027
|
+
* until its matching packed oracle and serving comparison are accepted. */
|
|
1028
|
+
forwardHiddenMixed(work: readonly TokenGroup[]): MlxArray[] {
|
|
1029
|
+
if (work.length === 1 && !work[0]!.captureLayer) return [this.forwardHidden(work[0]!.ids, work[0]!.cache)];
|
|
1030
|
+
const groups: Array<{ ids: MlxArray; cache: Cache[]; h: MlxArray;
|
|
1031
|
+
masks: Map<string, Mask>; perLayer: MlxArray | null; intermediates: (SharedKv | null)[] }> = [];
|
|
1032
|
+
const results: MlxArray[] = [];
|
|
1033
|
+
const packedMlp = runtimeFlag("MLX_BUN_MIXED_PACKED_MLP", true) && !work.some(group => group.preserveTokenGeometry);
|
|
1034
|
+
try {
|
|
1035
|
+
for (const { ids, cache } of work) {
|
|
1036
|
+
const group = { ids, cache, h: this.embed.encode(ids), masks: new Map<string, Mask>(),
|
|
1037
|
+
perLayer: null as MlxArray | null, intermediates: Array<SharedKv | null>(this.layers.length).fill(null) };
|
|
1038
|
+
groups.push(group);
|
|
1039
|
+
group.h = disposing(group.h, ops.mulScalar(group.h, this.embedScale));
|
|
1040
|
+
for (let i = 0; i < this.numDonors; i++) {
|
|
1041
|
+
const type = this.layers[i]!.layerType;
|
|
1042
|
+
if (!group.masks.has(type)) group.masks.set(type,
|
|
1043
|
+
cache[i]!.makeMask(ids.shape[1]!, type === "sliding_attention" ? this.windowSize : null));
|
|
1044
|
+
}
|
|
1045
|
+
if (this.perLayerWidth > 0) group.perLayer = this.computePerLayerInputs(ids, group.h);
|
|
1046
|
+
}
|
|
1047
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
1048
|
+
const layer = this.layers[i]!, ci = this.cacheIndex[i]!;
|
|
1049
|
+
const mids: MlxArray[] = [], inputs: MlxArray[] = [];
|
|
1050
|
+
try {
|
|
1051
|
+
for (const group of groups) {
|
|
1052
|
+
const sharedIn = ci === -1 ? group.intermediates[this.previousKvs[i]!]! : null;
|
|
1053
|
+
const { h, shared } = layer.forwardAttn(group.h, group.masks.get(layer.layerType)!,
|
|
1054
|
+
ci === -1 ? null : group.cache[ci]!, sharedIn);
|
|
1055
|
+
mids.push(h); group.intermediates[i] = shared;
|
|
1056
|
+
if (group.perLayer) {
|
|
1057
|
+
const [B, L] = group.ids.shape;
|
|
1058
|
+
using part = group.perLayer.slice([0, 0, i, 0], [B!, L!, i + 1, this.perLayerWidth]);
|
|
1059
|
+
inputs.push(ops.reshape(part, [B!, L!, this.perLayerWidth]));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// Per-layer inputs use the same token order as the hidden states.
|
|
1063
|
+
let packedInput: MlxArray | null = null;
|
|
1064
|
+
if (packedMlp && inputs.length) {
|
|
1065
|
+
const flat = inputs.map(input => ops.reshape(input, [1, -1, this.perLayerWidth]));
|
|
1066
|
+
try { packedInput = ops.concatAxis(flat, 1); }
|
|
1067
|
+
finally { for (const input of flat) input.dispose(); }
|
|
1068
|
+
}
|
|
1069
|
+
using pli = packedInput;
|
|
1070
|
+
const outputs: MlxArray[] = [];
|
|
1071
|
+
try {
|
|
1072
|
+
if (packedMlp) outputs.push(...mapPackedTokens(mids, packed => layer.forwardMlp(packed, pli)));
|
|
1073
|
+
else for (const [row, mid] of mids.entries()) outputs.push(layer.forwardMlp(mid, inputs[row] ?? null));
|
|
1074
|
+
} catch (error) { for (const output of outputs) output.dispose(); throw error; }
|
|
1075
|
+
for (const [index, group] of groups.entries()) {
|
|
1076
|
+
group.h.dispose(); group.h = outputs[index]!;
|
|
1077
|
+
}
|
|
1078
|
+
for (const [index, group] of groups.entries()) work[index]!.captureLayer?.(i, group.h);
|
|
1079
|
+
} finally {
|
|
1080
|
+
for (const mid of mids) mid.dispose();
|
|
1081
|
+
for (const input of inputs) input.dispose();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
for (const [index, group] of groups.entries()) {
|
|
1085
|
+
const hidden = this.finalNorm.forward(group.h); results.push(hidden);
|
|
1086
|
+
work[index]!.captureLayer?.(this.layers.length, hidden);
|
|
1087
|
+
}
|
|
1088
|
+
return results;
|
|
1089
|
+
} catch (error) { for (const result of results) result.dispose(); throw error; }
|
|
1090
|
+
finally {
|
|
1091
|
+
for (const group of groups) {
|
|
1092
|
+
group.h.dispose(); group.perLayer?.dispose();
|
|
1093
|
+
for (let i = 0; i < this.numDonors; i++) {
|
|
1094
|
+
const shared = group.intermediates[i];
|
|
1095
|
+
if (shared) Gemma4Model.disposeSharedKv(shared);
|
|
1096
|
+
}
|
|
1097
|
+
for (const mask of group.masks.values()) mask.arr?.dispose();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
|
|
1018
1103
|
// --- Segmented-backward support (docs/design/orpo-training.md
|
|
1019
1104
|
// §4 Phase B). These are ADDITIVE — forwardLayers is untouched. The segmented
|
|
1020
1105
|
// driver builds masks + per-layer inputs ONCE, then drives runLayerRange per
|
|
@@ -1056,7 +1141,10 @@ export class Gemma4Model {
|
|
|
1056
1141
|
/** Dispose a fetched donor K/V (plain or quantized) — mirrors forwardLayers'
|
|
1057
1142
|
* donor cleanup. */
|
|
1058
1143
|
private static disposeSharedKv(s: SharedKv): void {
|
|
1059
|
-
|
|
1144
|
+
s.offsetArr?.dispose();
|
|
1145
|
+
if (s.kind === "view") {
|
|
1146
|
+
s.attention.dispose();
|
|
1147
|
+
} else if (s.kind === "plain") {
|
|
1060
1148
|
s.keys.dispose();
|
|
1061
1149
|
s.values.dispose();
|
|
1062
1150
|
} else {
|
|
@@ -34,7 +34,10 @@ export const FINGERPRINT = "9f812d2eb461fcbe";
|
|
|
34
34
|
export let generatedForwardUses = 0;
|
|
35
35
|
|
|
36
36
|
function disposeSharedKv(s: SharedKv): void {
|
|
37
|
-
if (s.kind === "
|
|
37
|
+
if (s.kind === "view") {
|
|
38
|
+
s.attention.dispose();
|
|
39
|
+
s.offsetArr?.dispose();
|
|
40
|
+
} else if (s.kind === "plain") {
|
|
38
41
|
s.keys.dispose();
|
|
39
42
|
s.values.dispose();
|
|
40
43
|
} else {
|
|
@@ -34,7 +34,10 @@ export const FINGERPRINT = "c9dd67ed5a525231";
|
|
|
34
34
|
export let generatedForwardUses = 0;
|
|
35
35
|
|
|
36
36
|
function disposeSharedKv(s: SharedKv): void {
|
|
37
|
-
if (s.kind === "
|
|
37
|
+
if (s.kind === "view") {
|
|
38
|
+
s.attention.dispose();
|
|
39
|
+
s.offsetArr?.dispose();
|
|
40
|
+
} else if (s.kind === "plain") {
|
|
38
41
|
s.keys.dispose();
|
|
39
42
|
s.values.dispose();
|
|
40
43
|
} else {
|