mlx-bun 0.3.0 → 0.4.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/docs/README.md +4 -1
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/design/batching.md +1321 -49
- package/docs/design/decode-speed-program.md +331 -1163
- package/docs/design/generic-model-support.md +79 -3
- package/docs/design/kv-cache.md +198 -54
- package/docs/design/speculative-decoding.md +387 -24
- package/docs/design/turboquant.md +123 -13
- package/docs/design/unified-engine-frontier-plan.md +144 -142
- package/docs/planning/PRODUCT_ROADMAP.md +16 -0
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/reference/benchmarks.md +2457 -6
- package/docs/reference/cli.md +41 -2
- package/docs/reference/distribution.md +16 -9
- package/docs/reference/environment.md +55 -5
- package/docs/reference/models.md +13 -4
- package/docs/reference/server-api.md +79 -25
- package/docs/reference/server-config.md +178 -84
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +1 -1
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/batch-group.ts +368 -224
- 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 +106 -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/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 +77 -18
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/model-serving.ts +1 -1
- package/src/backends/mlx/prefill-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +41 -3
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +35 -66
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- 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 +165 -0
- package/src/cli.ts +11 -36
- package/src/contracts/execution.ts +11 -0
- package/src/contracts/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +18 -0
- package/src/engine/execution-plan.ts +18 -16
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +10 -5
- package/src/fit.ts +29 -15
- package/src/generate.ts +77 -173
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +70 -12
- package/src/kv-store.ts +70 -16
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/lab/paged-kv/paged-kv.ts +25 -11
- package/src/mlx/array.ts +2 -1
- 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 +93 -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 +55 -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 +162 -80
- package/src/model/gemma4.ts +34 -26
- 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 +67 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5.ts +82 -42
- 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/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +42 -44
- package/src/sampler.ts +36 -25
- package/src/serve/chat-stage.ts +2 -2
- package/src/serve/generation-gateway.ts +39 -30
- package/src/serve/model-host.ts +3 -4
- package/src/serve/request-plan.ts +4 -3
- package/src/serve/request-prep.ts +2 -4
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/server.ts +64 -108
- package/src/spec/assistant-rows.ts +92 -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 +53 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +92 -20
- 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 +219 -0
- package/src/spec/qwen-mtp-source.ts +81 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +70 -68
- package/src/spec/source.ts +80 -15
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +43 -101
- package/src/ssd-cache.ts +33 -21
- package/src/ssd-durability.ts +11 -7
- package/src/web/app.js +57 -57
- 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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
2
|
+
import type { MlxArray } from "../../mlx/array";
|
|
3
|
+
import * as ops from "../../mlx/ops";
|
|
4
|
+
import { legacyCacheCodecs, type CacheCodecProvider, type CacheHeaderEntry,
|
|
5
|
+
type SnapshotContext } from "../../kv-store";
|
|
6
|
+
import { disposeResources } from "../../engine/resources";
|
|
7
|
+
import type { CheckpointAttachment } from "./checkpoint-state";
|
|
8
|
+
type TensorSlot = SnapshotContext["slots"][number];
|
|
9
|
+
|
|
10
|
+
/** Reuse the backend cache codecs for method-owned companion state. Tensor
|
|
11
|
+
* slots address attachment indices here; the persistence tier supplies the
|
|
12
|
+
* outer file offsets and checksums. No host serialization of tensors occurs. */
|
|
13
|
+
export function captureCacheAttachment(schema: string, caches: readonly Cache[],
|
|
14
|
+
metadata: CheckpointAttachment["metadata"], codecs: CacheCodecProvider = legacyCacheCodecs): CheckpointAttachment {
|
|
15
|
+
const tensors: MlxArray[] = [], headers: Array<CacheHeaderEntry | null> = [];
|
|
16
|
+
try {
|
|
17
|
+
for (const cache of caches) {
|
|
18
|
+
if (!cache.offset) { headers.push(null); continue; }
|
|
19
|
+
const slots: TensorSlot[] = [];
|
|
20
|
+
const liveSlice = (array: MlxArray, length: number, axis = 2) => {
|
|
21
|
+
const end = [...array.shape]; end[axis] = length;
|
|
22
|
+
return array.slice(array.shape.map(() => 0), end);
|
|
23
|
+
};
|
|
24
|
+
const push = (array: MlxArray, owned: boolean) => {
|
|
25
|
+
const value = owned ? array : ops.copyOf(array);
|
|
26
|
+
const off = tensors.length; tensors.push(value);
|
|
27
|
+
slots.push({ off, bytes: value.nbytes, shape: [...value.shape], dtype: value.dtype, hash: "" });
|
|
28
|
+
};
|
|
29
|
+
const context: SnapshotContext = { slots, push, liveSlice,
|
|
30
|
+
liveMlaSlice: (array, length) => liveSlice(array, length, 1),
|
|
31
|
+
pushTriple(triple, length) {
|
|
32
|
+
for (const array of [triple.packed, triple.scales, triple.biases])
|
|
33
|
+
push(length === null ? array : liveSlice(array, length), length !== null);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const entry = codecs.forCache(cache).snapshot(cache, context);
|
|
37
|
+
if (cache.minimumReusableOffset !== undefined) entry.minimumReusableOffset = cache.minimumReusableOffset;
|
|
38
|
+
headers.push(entry);
|
|
39
|
+
}
|
|
40
|
+
return { schema, metadata: { ...metadata, cacheCodecs: codecs.id, cacheHeaders: JSON.stringify(headers) }, tensors };
|
|
41
|
+
} catch (error) { disposeResources(tensors); throw error; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Returned caches own immutable aliases. The caller keeps an external SSD
|
|
45
|
+
* lease until its normal state materialization boundary has completed. */
|
|
46
|
+
export function restoreCacheAttachment(attachment: CheckpointAttachment, makeCache: () => Cache[],
|
|
47
|
+
codecs: CacheCodecProvider = legacyCacheCodecs): Cache[] {
|
|
48
|
+
if (attachment.metadata.cacheCodecs !== codecs.id) throw new Error("companion cache codec mismatch");
|
|
49
|
+
const headers = JSON.parse(attachment.metadata.cacheHeaders as string) as Array<CacheHeaderEntry | null>;
|
|
50
|
+
const caches = makeCache(), pending: MlxArray[] = [];
|
|
51
|
+
const arr = (slot: TensorSlot) => {
|
|
52
|
+
const value = ops.copyOf(attachment.tensors[slot.off]!); pending.push(value); return value;
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
for (const [index, entry] of headers.entries()) {
|
|
56
|
+
if (!entry) continue;
|
|
57
|
+
const cache = codecs.forHeader(entry).load(entry, { path: attachment.schema, arr, grownArr: arr,
|
|
58
|
+
triple: (slots, at) => ({ packed: arr(slots[at]!), scales: arr(slots[at + 1]!), biases: arr(slots[at + 2]!) }),
|
|
59
|
+
});
|
|
60
|
+
pending.length = 0;
|
|
61
|
+
cache.minimumReusableOffset = entry.minimumReusableOffset ?? 0;
|
|
62
|
+
const previous = caches[index]; caches[index] = cache;
|
|
63
|
+
previous?.dispose();
|
|
64
|
+
}
|
|
65
|
+
return caches;
|
|
66
|
+
} catch (error) { disposeResources([...pending, ...caches]); throw error; }
|
|
67
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TurboQuantKVCache, RotatingKVCache, RotatingQuantizedKVCache, isBatchableCache, type BatchableCache, type Cache } from "../../model/gemma4-base";
|
|
2
|
+
import { DelayedRotatingQuantizedKVCache } from "../../model/delayed-rotating-quantized-kv";
|
|
3
|
+
import { RotatingAffineLayout } from "../../model/rotating-kv-layout";
|
|
4
|
+
import { BatchedTurboQuantKVCache } from "../../model/batched-turboquant-kv";
|
|
5
|
+
import { targetRowLayoutFactory, type TargetRowLayout } from "./target-layout-capability";
|
|
6
|
+
import { PagedKVCache } from "../../lab/paged-kv/paged-kv";
|
|
7
|
+
import { PagedKvRows } from "../../lab/paged-kv/paged-kv-rows";
|
|
8
|
+
|
|
9
|
+
/** Bind encoded state to its row layout. Execution owners only move rows. */
|
|
10
|
+
export function ownedCacheLayout(cache: Cache): BatchableCache | undefined {
|
|
11
|
+
if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize);
|
|
12
|
+
if (isBatchableCache(cache)) return cache.makeEmptyBatch();
|
|
13
|
+
if (cache instanceof TurboQuantKVCache) return new BatchedTurboQuantKVCache(cache.kBits, cache.vBits);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Prefill reuses decode's layouts; shape selection belongs to storage. */
|
|
18
|
+
export function prefillCacheLayout(cache: Cache): BatchableCache {
|
|
19
|
+
if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize);
|
|
20
|
+
if (isBatchableCache(cache)) return cache.makeEmptyBatch();
|
|
21
|
+
if (cache instanceof RotatingKVCache) return new DelayedRotatingQuantizedKVCache(cache.maxSize, 64, 4, Infinity, () => {});
|
|
22
|
+
if (cache instanceof RotatingQuantizedKVCache) return new RotatingAffineLayout(cache.maxSize, cache.groupSize, cache.bits);
|
|
23
|
+
return targetCacheLayout(cache);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Target transaction layouts retain each source cache's numerical codec. */
|
|
27
|
+
export function targetCacheLayout(cache: Cache): TargetRowLayout {
|
|
28
|
+
const make = targetRowLayoutFactory(cache);
|
|
29
|
+
if (!make) throw new Error(`No target row layout for ${cache.signature()}`);
|
|
30
|
+
return make();
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
2
|
+
import * as ops from "../../mlx/ops";
|
|
3
|
+
import { leaseCacheStates } from "./state-views";
|
|
4
|
+
import { withResource } from "../../engine/resources";
|
|
5
|
+
import type { MlxArray } from "../../mlx/array";
|
|
6
|
+
import { disposeResources, cleanupFailure } from "../../engine/resources";
|
|
7
|
+
|
|
8
|
+
/** Generation-method state accompanying the target cache. The method owns
|
|
9
|
+
* its schema and alignment; storage owns these immutable tensor snapshots.
|
|
10
|
+
* Attachments require an exact prefix boundary and are never trimmed. */
|
|
11
|
+
export interface CheckpointAttachment {
|
|
12
|
+
schema: string;
|
|
13
|
+
metadata: Record<string, string | number | boolean>;
|
|
14
|
+
tensors: MlxArray[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function attachmentBytes(attachments: readonly CheckpointAttachment[] = []): number {
|
|
18
|
+
return attachments.reduce((sum, entry) =>
|
|
19
|
+
sum + entry.tensors.reduce((bytes, tensor) => bytes + tensor.nbytes, 0), 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function disposeAttachments(attachments: readonly CheckpointAttachment[] = []): void {
|
|
23
|
+
disposeResources(attachments.flatMap((entry) => entry.tensors));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cloneAttachments(attachments: readonly CheckpointAttachment[] = []): CheckpointAttachment[] {
|
|
27
|
+
const held: MlxArray[] = [];
|
|
28
|
+
try {
|
|
29
|
+
return attachments.map((entry) => ({
|
|
30
|
+
schema: entry.schema, metadata: { ...entry.metadata },
|
|
31
|
+
tensors: entry.tensors.map((tensor) => {
|
|
32
|
+
const copy = tensor.slice(tensor.shape.map(() => 0), [...tensor.shape]);
|
|
33
|
+
held.push(copy);
|
|
34
|
+
return copy;
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(held)); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Shared cache port for MLX execution methods and their companion state. */
|
|
41
|
+
export type MlxPrefixCache = import("../../contracts/prefix-cache").PrefixCache<
|
|
42
|
+
import("../../model/gemma4").Cache[], CheckpointAttachment[]
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
/** Resolve snapshot copies before storage retains them. A deferred compact
|
|
46
|
+
* row still owns its full parent batch; persistence may wait until idle.
|
|
47
|
+
* This only settles GPU state and performs no serialization or disk I/O. */
|
|
48
|
+
export function materializeCheckpoint(caches: Cache[], attachments: readonly CheckpointAttachment[] = []): void {
|
|
49
|
+
withResource(leaseCacheStates(caches), state => {
|
|
50
|
+
const tensors = [...state, ...attachments.flatMap(attachment => attachment.tensors)];
|
|
51
|
+
if (tensors.length) ops.evalAll(tensors);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { SpillQueue, type SpillItem } from "../../kv-store";
|
|
2
|
+
import { cacheBytes } from "../../prompt-cache";
|
|
3
|
+
import { disposeResources } from "../../engine/resources";
|
|
4
|
+
import type { ContinuationStore, OrdinaryContinuationState } from "./continuation";
|
|
5
|
+
|
|
6
|
+
type Metadata = Parameters<ContinuationStore["storeGenerationCheckpoint"]>[2];
|
|
7
|
+
interface Attempt { key: string; completed: boolean; pending: number; failed: boolean; version: number; retired: boolean }
|
|
8
|
+
interface Job { attempt: Attempt; metadata?: Metadata; cleanup?: number }
|
|
9
|
+
|
|
10
|
+
/** One coordinator per checkpoint store. Queue owns snapshots through disk
|
|
11
|
+
* completion; per-key attempts order cleanup before subsequent writes. */
|
|
12
|
+
export class ContinuationPersistence {
|
|
13
|
+
readonly #attempts = new Map<string, Attempt>();
|
|
14
|
+
readonly #jobs = new WeakMap<SpillItem["caches"], Job>();
|
|
15
|
+
readonly #queue: SpillQueue;
|
|
16
|
+
readonly #cleanupFailures = new Map<string, Set<number>>();
|
|
17
|
+
readonly #cleanupPending = new Map<string, Set<number>>();
|
|
18
|
+
readonly #cleanupSucceeded = new Map<string, number>();
|
|
19
|
+
#nextBarrier = 0;
|
|
20
|
+
constructor(readonly store: ContinuationStore, options: {
|
|
21
|
+
maxBytes: number;
|
|
22
|
+
/** All blocking tensor serialization runs through the owner's idle gate. */
|
|
23
|
+
runStep: <T>(step: () => T) => Promise<T>;
|
|
24
|
+
}) {
|
|
25
|
+
this.#queue = new SpillQueue(options.maxBytes, cacheBytes, async item => {
|
|
26
|
+
const job = this.#jobs.get(item.caches)!;
|
|
27
|
+
if (job.cleanup) {
|
|
28
|
+
await options.runStep(() => store.removeGenerationCheckpoints(job.attempt.key));
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const current = () => this.#attempts.get(job.attempt.key) === job.attempt && !job.attempt.completed;
|
|
32
|
+
if (!current()) return true;
|
|
33
|
+
const stored = await store.storeGenerationCheckpoint(item.tokens, item.caches, job.metadata!, options.runStep);
|
|
34
|
+
// No newer queued write can run until this callback returns. An old
|
|
35
|
+
// in-flight rename can therefore be removed without deleting new work.
|
|
36
|
+
if (!current()) await options.runStep(() => store.removeGenerationCheckpoints(job.attempt.key));
|
|
37
|
+
return stored;
|
|
38
|
+
}, disposeResources, item => !this.#jobs.get(item.caches)?.cleanup);
|
|
39
|
+
}
|
|
40
|
+
/** A completed response is ineligible immediately, before queued unlink. */
|
|
41
|
+
canRestore(key: string): boolean { return !this.#cleanupPending.has(key) && !this.#cleanupFailures.has(key); }
|
|
42
|
+
begin(key: string): Attempt {
|
|
43
|
+
const attempt = { key, completed: false, pending: 0, failed: false, version: 0, retired: false };
|
|
44
|
+
this.#attempts.set(key, attempt);
|
|
45
|
+
this.#queue.cancelWhere(item => !this.#jobs.get(item.caches)?.cleanup && this.#jobs.get(item.caches)?.attempt.key === key);
|
|
46
|
+
return attempt;
|
|
47
|
+
}
|
|
48
|
+
/** Takes ownership even when a completed/superseded attempt declines it. */
|
|
49
|
+
enqueue(attempt: Attempt, state: Omit<OrdinaryContinuationState, "seed">, metadata: Metadata): void {
|
|
50
|
+
if (attempt.completed || this.#attempts.get(attempt.key) !== attempt) {
|
|
51
|
+
disposeResources(state.caches); return;
|
|
52
|
+
}
|
|
53
|
+
this.#jobs.set(state.caches, { attempt, metadata: { ...metadata } });
|
|
54
|
+
attempt.pending++;
|
|
55
|
+
const version = ++attempt.version;
|
|
56
|
+
let completion: Promise<boolean>;
|
|
57
|
+
try { completion = this.#queue.enqueue({ caches: state.caches, tokens: [...state.cacheTokens], ns: attempt.key }); }
|
|
58
|
+
catch (error) { attempt.pending--; disposeResources(state.caches); throw error; }
|
|
59
|
+
void completion.then(stored => {
|
|
60
|
+
attempt.pending--;
|
|
61
|
+
if (version === attempt.version && !attempt.completed) attempt.failed = !stored;
|
|
62
|
+
this.#releaseSettled(attempt);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
complete(attempt: Attempt): void {
|
|
66
|
+
attempt.completed = true;
|
|
67
|
+
this.#queue.cancelWhere(item => !this.#jobs.get(item.caches)?.cleanup && this.#jobs.get(item.caches)?.attempt === attempt);
|
|
68
|
+
if (this.#attempts.get(attempt.key) === attempt) {
|
|
69
|
+
this.#attempts.delete(attempt.key);
|
|
70
|
+
const barrier = ++this.#nextBarrier;
|
|
71
|
+
const pending = this.#cleanupPending.get(attempt.key) ?? new Set<number>();
|
|
72
|
+
pending.add(barrier); this.#cleanupPending.set(attempt.key, pending);
|
|
73
|
+
const caches: SpillItem["caches"] = [];
|
|
74
|
+
this.#jobs.set(caches, { attempt, cleanup: barrier });
|
|
75
|
+
void this.#queue.enqueue({ caches, tokens: [], ns: attempt.key }).then(stored => {
|
|
76
|
+
const key = attempt.key;
|
|
77
|
+
const outstanding = this.#cleanupPending.get(key)!;
|
|
78
|
+
outstanding.delete(barrier);
|
|
79
|
+
if (!outstanding.size) this.#cleanupPending.delete(key);
|
|
80
|
+
const failures = this.#cleanupFailures.get(key) ?? new Set<number>();
|
|
81
|
+
if (stored) {
|
|
82
|
+
this.#cleanupSucceeded.set(key, Math.max(barrier, this.#cleanupSucceeded.get(key) ?? 0));
|
|
83
|
+
// A successful unlink covers older failures, never a newer barrier.
|
|
84
|
+
for (const failed of failures) if (failed <= barrier) failures.delete(failed);
|
|
85
|
+
} else if (barrier > (this.#cleanupSucceeded.get(key) ?? 0)) failures.add(barrier);
|
|
86
|
+
if (failures.size) this.#cleanupFailures.set(key, failures);
|
|
87
|
+
else this.#cleanupFailures.delete(key);
|
|
88
|
+
if (!outstanding.size) this.#cleanupSucceeded.delete(key);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Request cancellation/disposal preserves queued and durable state. */
|
|
93
|
+
release(attempt: Attempt): void { attempt.retired = true; this.#releaseSettled(attempt); }
|
|
94
|
+
#releaseSettled(attempt: Attempt): void {
|
|
95
|
+
if (attempt.retired && attempt.pending === 0 && !attempt.failed && this.#attempts.get(attempt.key) === attempt)
|
|
96
|
+
this.#attempts.delete(attempt.key);
|
|
97
|
+
}
|
|
98
|
+
get stats() { return { pendingCount: this.#queue.pendingCount, pendingBytes: this.#queue.pendingBytes,
|
|
99
|
+
dropped: this.#queue.droppedCount, failed: this.#queue.failedCount }; }
|
|
100
|
+
async flush(): Promise<{ durable: boolean; pendingBytes: number; failed: number }> {
|
|
101
|
+
await this.#queue.drain();
|
|
102
|
+
const active = [...this.#attempts.values()].filter(attempt => !attempt.completed);
|
|
103
|
+
return { durable: this.#cleanupFailures.size === 0 && active.every(attempt => attempt.pending === 0 && !attempt.failed),
|
|
104
|
+
pendingBytes: this.#queue.pendingBytes, failed: this.#cleanupFailures.size + active.filter(attempt => attempt.failed).length };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { snapshotGenerationPolicy } from "./request-policy";
|
|
2
|
+
import type { GenerateOptions } from "../../generate";
|
|
3
|
+
import type { ResolvedExecution } from "../../contracts/execution";
|
|
4
|
+
import { makeStepSampler } from "../../sampler";
|
|
5
|
+
import { createRowSampling } from "./row-sampling";
|
|
6
|
+
import { bindContinuationPolicy, type OrdinaryContinuation } from "./continuation";
|
|
7
|
+
|
|
8
|
+
/** Optional policy and sampler recovery for the existing ordinary driver.
|
|
9
|
+
* KV layout and delayed conversion remain owned by cache maintenance and persistence. */
|
|
10
|
+
export function createOrdinaryContinuationRequest(input: Omit<Parameters<typeof bindContinuationPolicy>[0], "namespace"> & {
|
|
11
|
+
interval: number;
|
|
12
|
+
persistence: import("./continuation-persistence").ContinuationPersistence;
|
|
13
|
+
onToken: Parameters<typeof createRowSampling>[1];
|
|
14
|
+
}) {
|
|
15
|
+
const options: GenerateOptions = snapshotGenerationPolicy(input.options);
|
|
16
|
+
if (!Number.isSafeInteger(input.interval) || input.interval < 1) throw new Error("continuation interval must be positive");
|
|
17
|
+
if (options.grammar || options.logprobs || options.topLogprobs || options.fill || options.pagedKv ||
|
|
18
|
+
input.execution.method !== "autoregressive")
|
|
19
|
+
throw new Error("ordinary continuation requires replayable ordinary sampling");
|
|
20
|
+
const make = (seed = options.seed, history: readonly number[] = input.prompt) => createRowSampling(makeStepSampler(
|
|
21
|
+
{ ...options, seed }, { tokenRepresentation: "device", grammarWait: "external", historyUpdate: "after-sample",
|
|
22
|
+
initialHistory: [...history] }), input.onToken);
|
|
23
|
+
let sampling = make();
|
|
24
|
+
let policy: ReturnType<typeof bindContinuationPolicy> | undefined;
|
|
25
|
+
const continuation: OrdinaryContinuation = {
|
|
26
|
+
interval: input.interval,
|
|
27
|
+
restore(namespace) { policy = bindContinuationPolicy({ ...input, options, namespace }); return policy.restore(); },
|
|
28
|
+
resumeSampling(state) {
|
|
29
|
+
const next = make(state.seed, [...state.cacheTokens, state.pendingToken]);
|
|
30
|
+
const previous = sampling; sampling = next; previous.dispose();
|
|
31
|
+
},
|
|
32
|
+
captureOwned: state => policy!.captureOwned(state),
|
|
33
|
+
complete: () => policy?.complete(),
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
continuation,
|
|
37
|
+
sample: ((...args) => sampling.sample(...args)) as ReturnType<typeof createRowSampling>["sample"],
|
|
38
|
+
// History-free greedy sampling needs no recovery work in the fast path.
|
|
39
|
+
plainGreedy: sampling.plainGreedy,
|
|
40
|
+
onToken: input.onToken,
|
|
41
|
+
dispose: () => { policy?.release(); sampling.dispose(); },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ContinuationPersistence } from "./continuation-persistence";
|
|
2
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
3
|
+
import type { GenerateOptions } from "../../generate";
|
|
4
|
+
import type { ResolvedExecution } from "../../contracts/execution";
|
|
5
|
+
import type { SsdCacheStore } from "../../ssd-cache";
|
|
6
|
+
import { generationCheckpointKey } from "../../serve/checkpoint-identity";
|
|
7
|
+
import { disposeResources } from "../../engine/resources";
|
|
8
|
+
|
|
9
|
+
export interface OrdinaryContinuationState {
|
|
10
|
+
caches: Cache[];
|
|
11
|
+
cacheTokens: number[];
|
|
12
|
+
generatedTokens: number;
|
|
13
|
+
pendingToken: number;
|
|
14
|
+
seed: number;
|
|
15
|
+
}
|
|
16
|
+
/** Request-owned policy. Restore and captureOwned transfer cache ownership.
|
|
17
|
+
* No store or key knowledge enters the driver. */
|
|
18
|
+
export interface OrdinaryContinuation {
|
|
19
|
+
readonly interval: number;
|
|
20
|
+
restore(namespace: string): OrdinaryContinuationState | null;
|
|
21
|
+
resumeSampling(state: Omit<OrdinaryContinuationState, "caches">): void;
|
|
22
|
+
captureOwned(state: Omit<OrdinaryContinuationState, "seed">): void;
|
|
23
|
+
complete(): void;
|
|
24
|
+
}
|
|
25
|
+
export type ContinuationStore = Pick<SsdCacheStore, "findGenerationCheckpoint" | "restore" |
|
|
26
|
+
"storeGenerationCheckpoint" | "removeGenerationCheckpoints">;
|
|
27
|
+
|
|
28
|
+
/** Shared persistence policy for compatibility and grouped ordinary drivers. */
|
|
29
|
+
export function bindContinuationPolicy(input: {
|
|
30
|
+
store: ContinuationStore;
|
|
31
|
+
restore: (entry: NonNullable<ReturnType<ContinuationStore["findGenerationCheckpoint"]>>) => ReturnType<ContinuationStore["restore"]>;
|
|
32
|
+
prompt: number[]; options: GenerateOptions; execution: ResolvedExecution;
|
|
33
|
+
identity: unknown; namespace: string;
|
|
34
|
+
persistence?: ContinuationPersistence;
|
|
35
|
+
}) {
|
|
36
|
+
const { store, options, prompt, namespace } = input;
|
|
37
|
+
const key = generationCheckpointKey(prompt, options, namespace, input.execution, input.identity);
|
|
38
|
+
let seed = options.seed ?? 0;
|
|
39
|
+
const attempt = input.persistence?.begin(key);
|
|
40
|
+
return {
|
|
41
|
+
key,
|
|
42
|
+
restore(): OrdinaryContinuationState | null {
|
|
43
|
+
if (input.persistence && !input.persistence.canRestore(key)) return null;
|
|
44
|
+
const entry = store.findGenerationCheckpoint(prompt, key, namespace);
|
|
45
|
+
const restored = entry ? input.restore(entry) : null;
|
|
46
|
+
if (!restored) return null;
|
|
47
|
+
const metadata = restored.header.generationCheckpoint;
|
|
48
|
+
if (!metadata || metadata.originalPromptTokens !== prompt.length ||
|
|
49
|
+
metadata.generatedTokens !== restored.tokens.length - prompt.length) {
|
|
50
|
+
disposeResources(restored.caches);
|
|
51
|
+
throw new Error("generation checkpoint has inconsistent continuation metadata");
|
|
52
|
+
}
|
|
53
|
+
seed = metadata.seed;
|
|
54
|
+
return { caches: restored.caches, cacheTokens: restored.tokens,
|
|
55
|
+
generatedTokens: metadata.generatedTokens, pendingToken: metadata.pendingToken, seed };
|
|
56
|
+
},
|
|
57
|
+
async capture(state: Omit<OrdinaryContinuationState, "seed">): Promise<void> {
|
|
58
|
+
const stored = await store.storeGenerationCheckpoint(state.cacheTokens, state.caches, {
|
|
59
|
+
key, cacheNs: namespace, originalPromptTokens: prompt.length,
|
|
60
|
+
generatedTokens: state.generatedTokens, pendingToken: state.pendingToken,
|
|
61
|
+
seed, seedWasExplicit: options.seedWasExplicit === true,
|
|
62
|
+
});
|
|
63
|
+
if (stored) console.log(`[generation-checkpoint] saved ${state.generatedTokens} emitted tokens`);
|
|
64
|
+
},
|
|
65
|
+
captureOwned(state: Omit<OrdinaryContinuationState, "seed">): void {
|
|
66
|
+
if (!input.persistence || !attempt) { disposeResources(state.caches); throw new Error("owned continuation requires persistence coordinator"); }
|
|
67
|
+
input.persistence.enqueue(attempt, state, { key, cacheNs: namespace, originalPromptTokens: prompt.length,
|
|
68
|
+
generatedTokens: state.generatedTokens, pendingToken: state.pendingToken,
|
|
69
|
+
seed, seedWasExplicit: options.seedWasExplicit === true });
|
|
70
|
+
},
|
|
71
|
+
release() { if (input.persistence && attempt) input.persistence.release(attempt); },
|
|
72
|
+
complete() {
|
|
73
|
+
if (input.persistence && attempt) input.persistence.complete(attempt);
|
|
74
|
+
else store.removeGenerationCheckpoints(key);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { readAssistantDonors } from "./assistant-target";
|
|
1
2
|
import type { RuntimeModel } from "../../model/factory";
|
|
2
3
|
import { Gemma4Model, type Cache } from "../../model/gemma4";
|
|
3
4
|
import { Qwen35Model } from "../../model/qwen3_5";
|
|
4
5
|
import type { MlxArray } from "../../mlx/array";
|
|
5
6
|
import * as ops from "../../mlx/ops";
|
|
6
|
-
import type { TargetView
|
|
7
|
+
import type { TargetView } from "../../spec/source";
|
|
7
8
|
|
|
8
9
|
/** Class checks and live cache layout stay at the compatibility boundary.
|
|
9
10
|
* A replacement graph supplies the same ports without becoming these classes. */
|
|
@@ -14,27 +15,11 @@ export function bindLegacyDraftTarget(model: RuntimeModel, caches: Cache[]): Tar
|
|
|
14
15
|
if (model.layers[i]!.layerType === "sliding_attention") sliding = i;
|
|
15
16
|
else full = i;
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
const assistant: AssistantTarget | undefined = sliding >= 0 && full >= 0 ? {
|
|
24
|
-
position: () => caches[0]!.offset - 1,
|
|
25
|
-
embedScaled(token) {
|
|
26
|
-
const ids = ops.fromInt32([token], [1, 1]);
|
|
27
|
-
let embedded: MlxArray | undefined;
|
|
28
|
-
try { embedded = model.embed.encode(ids); return ops.mulScalar(embedded, model.embedScale); }
|
|
29
|
-
finally { ids.dispose(); embedded?.dispose(); }
|
|
30
|
-
},
|
|
31
|
-
readDonors() {
|
|
32
|
-
const slidingView = view(sliding);
|
|
33
|
-
try { return { sliding: slidingView, full: view(full) }; }
|
|
34
|
-
catch (error) { for (const array of slidingView) array.dispose(); throw error; }
|
|
35
|
-
},
|
|
36
|
-
} : undefined;
|
|
37
|
-
return Object.freeze({ identity: model, assistant, gemmaTaps: Object.freeze({
|
|
18
|
+
return Object.freeze({ identity: model, assistantRows: sliding >= 0 && full >= 0 ? {
|
|
19
|
+
hiddenSize: model.config.text.hiddenSize,
|
|
20
|
+
embed(ids: MlxArray) { using embedded = model.embed.encode(ids); return ops.mulScalar(embedded, model.embedScale); },
|
|
21
|
+
readDonors: () => readAssistantDonors(caches[sliding]!, caches[full]!),
|
|
22
|
+
} : undefined, gemmaTaps: Object.freeze({
|
|
38
23
|
layerCount: model.layers.length,
|
|
39
24
|
projection: Object.freeze({ embed: model.embed, logitsFromHidden: model.logitsFromHidden.bind(model) }),
|
|
40
25
|
}) });
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import * as ops from "../../mlx/ops";
|
|
3
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
4
|
+
import { fullRowPadding } from "../../model/full-prefill-row";
|
|
5
|
+
import { disposeResources } from "../../engine/resources";
|
|
6
|
+
|
|
7
|
+
/** Assemble full-attention tensors at the model's B while storage retains
|
|
8
|
+
* per-row formats and positions. Returned arrays own their captured state. */
|
|
9
|
+
export function appendFullKvRows(rows: readonly Cache[], leftPad: readonly number[], width: number,
|
|
10
|
+
k: MlxArray, v: MlxArray, deferredValues = false): [MlxArray, MlxArray] {
|
|
11
|
+
const parts: [MlxArray, MlxArray][] = [], padded: MlxArray[] = [];
|
|
12
|
+
try {
|
|
13
|
+
for (const [index, row] of rows.entries()) {
|
|
14
|
+
using kr = k.slice([index, 0, 0, 0], [index + 1, k.shape[1]!, k.shape[2]!, k.shape[3]!]);
|
|
15
|
+
using vr = v.slice([index, 0, 0, 0], [index + 1, v.shape[1]!, v.shape[2]!, v.shape[3]!]);
|
|
16
|
+
parts.push(deferredValues && row.rotatedValueAttention
|
|
17
|
+
? row.rotatedValueAttention.updateAndFetchDeferredV(kr, vr) : row.updateAndFetch(kr, vr));
|
|
18
|
+
}
|
|
19
|
+
const combine = (field: 0 | 1) => {
|
|
20
|
+
const views = parts.map((part, row) => {
|
|
21
|
+
const a = part[field], before = leftPad[row]! - fullRowPadding(rows[row]!), after = width - before - a.shape[2]!;
|
|
22
|
+
if (!before && !after) return a;
|
|
23
|
+
using prefix = ops.zeros([1, a.shape[1]!, before, a.shape[3]!], a.dtype);
|
|
24
|
+
using suffix = ops.zeros([1, a.shape[1]!, after, a.shape[3]!], a.dtype);
|
|
25
|
+
const result = ops.concatAxis([prefix, a, suffix], 2); padded.push(result); return result;
|
|
26
|
+
});
|
|
27
|
+
return views.length === 1 ? ops.contiguous(views[0]!) : ops.concatAxis(views, 0);
|
|
28
|
+
};
|
|
29
|
+
const keys = combine(0);
|
|
30
|
+
try { return [keys, combine(1)]; } catch (error) { keys.dispose(); throw error; }
|
|
31
|
+
} finally { disposeResources([...parts.flat(), ...padded]); }
|
|
32
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { captureKvDonorAttention, combineKvDonorAttention } from "../../model/kv-attention-view";
|
|
2
|
+
import type { MlxArray } from "../../mlx/array";
|
|
3
|
+
import * as ops from "../../mlx/ops";
|
|
4
|
+
import type { Cache, KvDonorRows } from "../../model/gemma4-base";
|
|
5
|
+
import { fullRowInner, fullRowPadding } from "../../model/full-prefill-row";
|
|
6
|
+
import { disposeResources } from "../../engine/resources";
|
|
7
|
+
|
|
8
|
+
/** Read full-attention rows in their existing physical columns. Valid token
|
|
9
|
+
* coverage excludes prefill padding; no append, conversion or compaction. */
|
|
10
|
+
export function captureFullKvDonorRows(rows: readonly Cache[], leftPad: readonly number[], width: number): KvDonorRows {
|
|
11
|
+
const parts: KvDonorRows[] = [], padded: MlxArray[] = [];
|
|
12
|
+
try {
|
|
13
|
+
for (const row of rows) parts.push(fullRowInner(row).captureDonorRows!());
|
|
14
|
+
const combine = (field: "keys" | "values") => {
|
|
15
|
+
const views = parts.map((part, index) => {
|
|
16
|
+
const a = part[field], before = leftPad[index]! - fullRowPadding(rows[index]!), after = width - before - a.shape[2]!;
|
|
17
|
+
if (!before && !after) return a;
|
|
18
|
+
using prefix = ops.zeros([1, a.shape[1]!, before, a.shape[3]!], a.dtype);
|
|
19
|
+
using suffix = ops.zeros([1, a.shape[1]!, after, a.shape[3]!], a.dtype);
|
|
20
|
+
const result = ops.concatAxis([prefix, a, suffix], 2); padded.push(result); return result;
|
|
21
|
+
});
|
|
22
|
+
return views.length === 1 ? ops.contiguous(views[0]!) : ops.concatAxis(views, 0);
|
|
23
|
+
};
|
|
24
|
+
const offsets = rows.map(row => row.offset), starts = [...leftPad];
|
|
25
|
+
const keys = combine("keys");
|
|
26
|
+
try { return { keys, values: combine("values"), offsets, starts, ends: offsets.map((offset, row) => starts[row]! + offset) }; }
|
|
27
|
+
catch (error) { keys.dispose(); throw error; }
|
|
28
|
+
} finally { disposeResources([...parts.flatMap(part => [part.keys, part.values]), ...padded]); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/** Capture full rows in global columns while each encoded view retains its
|
|
33
|
+
* own physical width and numerical attention operation. */
|
|
34
|
+
export function captureFullKvDonorAttention(rows: readonly Cache[], leftPad: readonly number[], width: number): import("../../model/gemma4-base").KvDonorAttention {
|
|
35
|
+
const captured: import("../../model/gemma4-base").KvDonorAttention[] = [];
|
|
36
|
+
try {
|
|
37
|
+
for (const [index, row] of rows.entries()) {
|
|
38
|
+
const view = captureKvDonorAttention(fullRowInner(row));
|
|
39
|
+
const pad = leftPad[index]! - fullRowPadding(row);
|
|
40
|
+
captured.push({ width, dtype: view.dtype, offsets: [row.offset],
|
|
41
|
+
starts: [leftPad[index]!], ends: [leftPad[index]! + row.offset],
|
|
42
|
+
attend(q, scale, mask) {
|
|
43
|
+
if (!mask.arr) return view.attend(q, scale, mask);
|
|
44
|
+
const from = mask.arr.shape.map(() => 0), to = [...mask.arr.shape];
|
|
45
|
+
from[from.length - 1] = pad; to[to.length - 1] = pad + view.width;
|
|
46
|
+
using arr = mask.arr.slice(from, to);
|
|
47
|
+
return view.attend(q, scale, { mode: mask.mode, arr });
|
|
48
|
+
},
|
|
49
|
+
dispose: () => view.dispose(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return combineKvDonorAttention(captured);
|
|
53
|
+
} catch (error) { disposeResources(captured); throw error; }
|
|
54
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { KVCache, type Cache, type PaddedPrefillCache, type PrefillPadding } from "../../model/gemma4-base";
|
|
2
|
+
import { FullPrefillRow, fullRowInner } from "../../model/full-prefill-row";
|
|
3
|
+
import { cloneKvCaches } from "../../kv-store";
|
|
4
|
+
import { FullKvPositions, SpeculativeTransitioningKvRows, type KvRowTransition, type SpeculativeTransitionedKvLayout } from "./transitioning-kv-rows";
|
|
5
|
+
|
|
6
|
+
type FullLayout = SpeculativeTransitionedKvLayout & PaddedPrefillCache & {
|
|
7
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class PrefillPositions extends FullKvPositions {
|
|
11
|
+
override sync(rows: readonly Cache[], preserve: boolean): void {
|
|
12
|
+
const offsets = rows.map(row => row instanceof FullPrefillRow ? row.positionOffset : row.offset);
|
|
13
|
+
this.mergeRows(preserve
|
|
14
|
+
? [{ planes: [], rowOffsets: offsets, leftPad: [...this.leftPad] }]
|
|
15
|
+
: rows.map((row, index) => ({ planes: [], rowOffsets: [offsets[index]!],
|
|
16
|
+
leftPad: [row instanceof FullPrefillRow ? row.leftPadding : 0] })));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Full-attention padding composes with the existing precision transition
|
|
21
|
+
* and rollback lifecycle. Codec-specific attention stays in each adapter. */
|
|
22
|
+
export abstract class FullTransitioningKvRows<Layout extends FullLayout>
|
|
23
|
+
extends SpeculativeTransitioningKvRows<Layout> implements PaddedPrefillCache {
|
|
24
|
+
constructor(transition: KvRowTransition<Layout>, row?: Cache) {
|
|
25
|
+
super({ ...transition,
|
|
26
|
+
converted: row => transition.converted(fullRowInner(row)),
|
|
27
|
+
extractRow: row => row instanceof FullPrefillRow ? row.extract() : cloneKvCaches([row])[0]!,
|
|
28
|
+
packRows(layout, rows) {
|
|
29
|
+
layout.mergeRows(rows.map(fullRowInner));
|
|
30
|
+
if (rows.some(row => row instanceof FullPrefillRow)) {
|
|
31
|
+
layout.preparePrefill({ lengths: rows.map(() => 0),
|
|
32
|
+
leftPadding: rows.map(row => row instanceof FullPrefillRow ? row.leftPadding : 0) });
|
|
33
|
+
const ends = rows.map(row => row instanceof FullPrefillRow ? row.padding.endOffsets?.[0] : undefined);
|
|
34
|
+
layout.restorePrefillEnds(ends.some(end => end !== undefined) ? ends.map(end => end ?? Infinity) : undefined);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
}, row, new PrefillPositions());
|
|
38
|
+
}
|
|
39
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
40
|
+
if (this.packed) { this.packed.preparePrefill(padding); return; }
|
|
41
|
+
if (!this.rows.length) this.rows = padding.lengths.map(() => new KVCache());
|
|
42
|
+
this.rows = this.rows.map(row => row instanceof FullPrefillRow ? row : new FullPrefillRow(row as KVCache));
|
|
43
|
+
for (const [index, row] of this.rows.entries()) (row as FullPrefillRow).preparePrefill({
|
|
44
|
+
lengths: [padding.lengths[index]!],
|
|
45
|
+
...(padding.leftPadding ? { leftPadding: [padding.leftPadding[index]!] } : {}),
|
|
46
|
+
...(padding.rightPadding ? { rightPadding: [padding.rightPadding[index]!] } : {}),
|
|
47
|
+
});
|
|
48
|
+
this.syncPositions();
|
|
49
|
+
}
|
|
50
|
+
finalizePrefill(): void {
|
|
51
|
+
if (this.packed) { this.packed.finalizePrefill(); return; }
|
|
52
|
+
for (const row of this.rows) if (row instanceof FullPrefillRow) row.finalizePrefill();
|
|
53
|
+
this.syncPositions();
|
|
54
|
+
}
|
|
55
|
+
override filterRows(keep: readonly number[]): void {
|
|
56
|
+
if (!this.packed && keep.length) {
|
|
57
|
+
const pads = [...this.leftPad], shared = Math.min(...keep.map(row => pads[row]!));
|
|
58
|
+
for (const index of keep) {
|
|
59
|
+
const row = this.rows[index]!;
|
|
60
|
+
if (row instanceof FullPrefillRow) row.removeLeftPadding(Math.max(0, row.leftPadding - (pads[index]! - shared)));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
super.filterRows(keep);
|
|
64
|
+
}
|
|
65
|
+
}
|