mirror of
https://github.com/ollama/ollama.git
synced 2026-09-20 22:00:29 -04:00
The MLX runner is the only Go inference runner left and is no longer experimental, so its packages leave x/. The bindings become a top-level mlx package beside the carried patches in mlx/compat, mirroring how llama/ holds the llama.cpp integration, and the runner becomes mlxrunner with the architectures nested under the package they implement. Subpackages move with their parent unless listed. x/mlxrunner/mlx mlx x/internal/mlxthread mlx/mlxthread x/internal/mlxthreadtest mlx/mlxthread/mlxthreadtest x/internal/mlxtest mlx/mlxtest x/quant mlx/quant mlx/compat/*.patch mlx/compat/mlx-c (MLX patches go in mlx/compat/mlx) x/mlxrunner mlxrunner x/models/nn mlxrunner/nn x/models/<arch> mlxrunner/model/<arch> x/mlxrunner/imports.go mlxrunner/model/architectures (new package) x/create create x/safetensors fs/safetensors x/tokenizer mlxrunner/tokenizer Every package keeps its name, so the Go changes are the import path rewrites the moves force, and the CMake, Dockerfile, CI cache keys, drift check and Darwin payload script follow the new paths. Four edits are not paths: the runner's blank architecture imports become the package mlxrunner/model/architectures, so the list to extend for a new model sits beside the architecture directories; a depguard rule keeps the two test harnesses out of non-test code, as the x/internal placement used to; the CI change filter's two entries for the long-deleted x/imagegen/mlx now name the bindings' CMake project and the carried patches, so a change to either builds the payload; and the tokenizer parity test reads its fixtures from its own testdata instead of walking out of x/. x/server and x/imagegen/manifest stay for the next two commits.
285 lines
7.7 KiB
Go
285 lines
7.7 KiB
Go
package cache
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/ollama/ollama/mlx"
|
|
"github.com/ollama/ollama/mlx/mlxtest"
|
|
"github.com/ollama/ollama/mlxrunner/batch"
|
|
)
|
|
|
|
// newKVBatch builds a B=1 batch at SeqOffsets=off with all-real
|
|
// queries (SeqQueryLens=L) — the standard single-sequence cache
|
|
// test shape.
|
|
func newKVBatch(off, L int) *batch.Batch {
|
|
return &batch.Batch{
|
|
InputIDs: mlx.Zeros(mlx.DTypeInt32, 1, L),
|
|
SeqOffsets: []int32{int32(off)},
|
|
SeqQueryLens: []int32{int32(L)},
|
|
}
|
|
}
|
|
|
|
func TestKVCacheSnapshotRestoreNeedBase(t *testing.T) {
|
|
mlxtest.Run(t, func(t *mlxtest.T) {
|
|
c := NewKVCache()
|
|
|
|
for range 10 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
|
|
// Snapshot [5, 10).
|
|
snap := c.Snapshot(5)
|
|
|
|
// Free the cache completely — offset is now 0.
|
|
c.Free()
|
|
|
|
// Restore should fail because cache doesn't have data up to fromOffset=5.
|
|
if c.Restore(snap, 10) {
|
|
t.Fatal("expected Restore to fail with no base data")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestKVCacheDataSurvivesSnapshotRestore verifies that actual array data
|
|
// is preserved through a snapshot→free→restore cycle.
|
|
func TestKVCacheDataSurvivesSnapshotRestore(t *testing.T) {
|
|
mlxtest.Run(t, func(t *mlxtest.T) {
|
|
c := NewKVCache()
|
|
|
|
for range 10 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
|
|
snap := c.Snapshot(0)
|
|
if snap == nil {
|
|
t.Fatal("Snapshot returned nil")
|
|
}
|
|
|
|
// Free and restore to a fresh cache.
|
|
c2 := NewKVCache()
|
|
if !c2.Restore(snap, 10) {
|
|
t.Fatal("Restore failed")
|
|
}
|
|
if c2.Offset() != 10 {
|
|
t.Fatalf("offset = %d, want 10", c2.Offset())
|
|
}
|
|
|
|
// Verify State() returns arrays with correct sequence dimension.
|
|
state := c2.State()
|
|
if len(state) != 2 {
|
|
t.Fatalf("State() returned %d arrays, want 2", len(state))
|
|
}
|
|
// keys shape: [B, H, seqLen, Dk]
|
|
if state[0].Dim(2) != 10 {
|
|
t.Fatalf("keys seq dim = %d, want 10", state[0].Dim(2))
|
|
}
|
|
if state[1].Dim(2) != 10 {
|
|
t.Fatalf("values seq dim = %d, want 10", state[1].Dim(2))
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestKVCacheSplitPreservesData verifies that split produces two snapshots
|
|
// that can be sequentially restored to rebuild the original cache state.
|
|
func TestKVCacheSplitPreservesData(t *testing.T) {
|
|
mlxtest.Run(t, func(t *mlxtest.T) {
|
|
c := NewKVCache()
|
|
|
|
for range 10 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
|
|
snap := c.Snapshot(0)
|
|
parent, child := c.Split(snap, 5)
|
|
if parent == nil || child == nil {
|
|
t.Fatal("Split returned nil")
|
|
}
|
|
|
|
// Restore parent → offset=5, seq dim=5.
|
|
c2 := NewKVCache()
|
|
if !c2.Restore(parent, 5) {
|
|
t.Fatal("Restore(parent) failed")
|
|
}
|
|
if c2.Offset() != 5 {
|
|
t.Fatalf("offset after parent = %d, want 5", c2.Offset())
|
|
}
|
|
state := c2.State()
|
|
if state[0].Dim(2) != 5 {
|
|
t.Fatalf("keys seq dim after parent = %d, want 5", state[0].Dim(2))
|
|
}
|
|
|
|
// Restore child on top → offset=10, seq dim=10.
|
|
if !c2.Restore(child, 10) {
|
|
t.Fatal("Restore(child) failed")
|
|
}
|
|
if c2.Offset() != 10 {
|
|
t.Fatalf("offset after child = %d, want 10", c2.Offset())
|
|
}
|
|
state = c2.State()
|
|
if state[0].Dim(2) != 10 {
|
|
t.Fatalf("keys seq dim after child = %d, want 10", state[0].Dim(2))
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestKVCacheSplitMergeRoundTripData verifies that splitting and merging back
|
|
// produces a snapshot equivalent to the original.
|
|
func TestKVCacheSplitMergeRoundTripData(t *testing.T) {
|
|
mlxtest.Run(t, func(t *mlxtest.T) {
|
|
c := NewKVCache()
|
|
|
|
for range 10 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
|
|
snap := c.Snapshot(0)
|
|
parent, child := c.Split(snap, 6)
|
|
merged := c.Merge(parent, child)
|
|
if merged == nil {
|
|
t.Fatal("Merge returned nil")
|
|
}
|
|
|
|
c2 := NewKVCache()
|
|
if !c2.Restore(merged, 10) {
|
|
t.Fatal("Restore(merged) failed")
|
|
}
|
|
if c2.Offset() != 10 {
|
|
t.Fatalf("offset = %d, want 10", c2.Offset())
|
|
}
|
|
|
|
state := c2.State()
|
|
if state[0].Dim(2) != 10 {
|
|
t.Fatalf("keys seq dim = %d, want 10", state[0].Dim(2))
|
|
}
|
|
if state[1].Dim(2) != 10 {
|
|
t.Fatalf("values seq dim = %d, want 10", state[1].Dim(2))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRotatingKVCacheRestoreOutsideWindow(t *testing.T) {
|
|
mlxtest.Run(t, func(t *mlxtest.T) {
|
|
c := NewRotatingKVCache(4)
|
|
|
|
// Feed 10 tokens (window size 4, so positions 0-5 are evicted).
|
|
for range 10 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
|
|
// Offset 3 is outside the window.
|
|
if c.Restore(nil, 3) {
|
|
t.Fatal("Restore(nil, 3) should fail when outside window")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestRotatingKVCacheSnapshotPreservesWindow verifies that after restoring
|
|
// from a snapshot, the rotating cache has the correct window of data.
|
|
func TestRotatingKVCacheSnapshotPreservesWindow(t *testing.T) {
|
|
mlxtest.Run(t, func(t *mlxtest.T) {
|
|
c := NewRotatingKVCache(4)
|
|
|
|
// Feed 10 tokens one at a time. Window size 4, so only last 4 are kept.
|
|
for range 10 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
|
|
snap := c.Snapshot(0)
|
|
if snap == nil {
|
|
t.Fatal("Snapshot returned nil")
|
|
}
|
|
|
|
// Feed 5 more tokens.
|
|
for range 5 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
|
|
// Restore to offset 10.
|
|
if !c.Restore(snap, 10) {
|
|
t.Fatal("Restore failed")
|
|
}
|
|
if c.Offset() != 10 {
|
|
t.Fatalf("offset = %d, want 10", c.Offset())
|
|
}
|
|
|
|
state := c.State()
|
|
if len(state) != 2 {
|
|
t.Fatalf("State() returned %d arrays, want 2", len(state))
|
|
}
|
|
// Seq dim should be min(offset, maxSize) = min(10, 4) = 4.
|
|
seqDim := state[0].Dim(2)
|
|
if seqDim != 4 {
|
|
t.Fatalf("keys seq dim = %d, want 4 (window size)", seqDim)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestRotatingKVCacheRestoreFromSnapshot verifies that restoring from a
|
|
// snapshot correctly preserves the write position (idx), so subsequent
|
|
// single-token updates land in the right buffer slot.
|
|
func TestRotatingKVCacheRestoreFromSnapshot(t *testing.T) {
|
|
mlxtest.Run(t, func(t *mlxtest.T) {
|
|
c := NewRotatingKVCache(4)
|
|
|
|
// Fill the window: 6 tokens into a size-4 window.
|
|
// After this, idx has wrapped and the buffer has rotated.
|
|
for range 6 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
if c.Offset() != 6 {
|
|
t.Fatalf("offset = %d, want 6", c.Offset())
|
|
}
|
|
|
|
snap := c.Snapshot(0)
|
|
|
|
// Mutate the cache further so live state diverges from snapshot.
|
|
for range 3 {
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
}
|
|
|
|
// Restore to snapshot state.
|
|
if !c.Restore(snap, 6) {
|
|
t.Fatal("Restore failed")
|
|
}
|
|
if c.Offset() != 6 {
|
|
t.Fatalf("offset after restore = %d, want 6", c.Offset())
|
|
}
|
|
|
|
// Feed one more token. If idx was restored correctly, this should
|
|
// produce a valid window of size 4 at offset 7.
|
|
k := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
v := mlx.Zeros(mlx.DTypeFloat16, 1, 4, 1, 8)
|
|
c.Update(newKVBatch(c.Offset(), k.Dim(2)), k, v)
|
|
|
|
if c.Offset() != 7 {
|
|
t.Fatalf("offset after post-restore update = %d, want 7", c.Offset())
|
|
}
|
|
state := c.State()
|
|
if len(state) != 2 {
|
|
t.Fatalf("State() returned %d arrays, want 2", len(state))
|
|
}
|
|
seqDim := state[0].Dim(2)
|
|
if seqDim != 4 {
|
|
t.Fatalf("keys seq dim = %d, want 4 (window size)", seqDim)
|
|
}
|
|
})
|
|
}
|