Files
Daniel HiltgenandGitHub c053d1e985 MLX: make Qwen3.8 Flash Next memory efficient (#18078)
* MLX: reduce Qwen3.8 Flash Next memory use

For NVFP4 creation, retain source precision for sensitive QSA projections
and non-expert MTP weights. Keep routed experts and PLE at NVFP4, and
promote other eligible weights to MXFP8.

Keep NVFP4 PLE tables host-backed, transferring only selected packed rows
and their scales to MLX. Honor scalar and per-expert NVFP4 global scales
in MoE, and per-shard global scales in host-backed PLE.

Use compressed QSA block selections in fused indexed Metal attention for
long prefill batches, retaining the graph fallback. Co-locate component
loading and forward methods, with explicit quantization and cache ownership.

* mlx: skip materializing host-backed tensors

Let models exclude tensors already owned by host storage from MLX loading.
Avoid allocating and discarding the full Qwen PLE tables at startup while
preserving the resident fallback.

* address comments
2026-11-06 13:39:21 -08:00

124 lines
3.9 KiB
Go

package mlxrunner
import (
"encoding/binary"
"maps"
"math"
"slices"
"testing"
"github.com/ollama/ollama/fs/safetensors"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/mlx"
"github.com/ollama/ollama/mlx/mlxtest"
"github.com/ollama/ollama/mlxrunner/batch"
"github.com/ollama/ollama/mlxrunner/cache"
"github.com/ollama/ollama/mlxrunner/model"
)
func TestLoadTensorsSkipsNamedTensors(t *testing.T) {
t.Setenv("OLLAMA_MODELS", t.TempDir())
var data []*safetensors.TensorData
for _, name := range []string{"skipped.weight", "skipped.weight.scale", "skipped.weight.global_scale", "target.weight", "draft.weight"} {
data = append(data, safetensors.NewTensorDataFromBytes(name, "F32", []int32{1}, binary.LittleEndian.AppendUint32(nil, math.Float32bits(2))))
}
layer, err := manifest.NewLayer(safetensors.BuildPackedSafetensorsReader(data), manifest.MediaTypeImageTensor)
if err != nil {
t.Fatal(err)
}
root := &model.Root{Manifest: &manifest.Manifest{Layers: []manifest.Layer{layer, layer}}}
for _, tc := range []struct {
name string
skip []string
want []string
}{
{"all tensors", nil, []string{"draft.weight", "skipped.weight", "skipped.weight.global_scale", "skipped.weight_scale", "target.weight"}},
{"skip named tensors", []string{"skipped.weight", "skipped.weight.scale", "skipped.weight.global_scale"}, []string{"draft.weight", "target.weight"}},
} {
t.Run(tc.name, func(t *testing.T) {
mlxtest.Run(t, func(t *mlxtest.T) {
tensors, err := loadTensorsFromManifest(root, tc.skip)
if err != nil {
t.Fatal(err)
}
if got := slices.Sorted(maps.Keys(tensors)); !slices.Equal(got, tc.want) {
t.Fatalf("loaded tensors = %v, want %v", got, tc.want)
}
for name, arr := range tensors {
if got := arr.Floats(); !slices.Equal(got, []float32{2}) {
t.Errorf("%s = %v, want [2]", name, got)
}
}
})
})
}
}
func TestRunnerCloseReleasesGenerationCache(t *testing.T) {
mlxtest.Run(t, func(t *mlxtest.T) {
activeMemory := func() int {
mlx.DefaultStream().Synchronize()
mlx.ClearCache()
return mlx.ActiveMemory()
}
baseline := activeMemory()
pc := newPrefixCache([]cache.Cache{cache.NewKVCache(), nil, cache.NewRotatingKVCache(4)})
defer pc.close()
r := &Runner{cache: pc}
for _, inputs := range [][]int32{{1, 2, 3, 4}, {5, 6, 7, 8}} {
mlx.Scoped(func() {
session := pc.begin(inputs, nil)
defer session.close()
n := len(session.remaining)
b := &batch.Batch{
InputIDs: mlx.FromValues(session.remaining, 1, n),
SeqOffsets: []int32{int32(pc.minCacheOffset())},
SeqQueryLens: []int32{int32(n)},
}
keys := mlx.Zeros(mlx.DTypeFloat32, 1, 1, n, 8)
values := mlx.Zeros(mlx.DTypeFloat32, 1, 1, n, 8)
for _, kv := range pc.caches {
if kv != nil {
kv.(cache.Attention).Update(b, keys, values)
mlx.Eval(kv.State()...)
}
}
})
}
if pc.minCacheOffset() == 0 || pc.pagedOutBytes == 0 || activeMemory() <= baseline {
t.Fatal("requests did not populate live and paged-out cache state")
}
r.Close()
if r.cache != nil {
t.Error("closed runner retains generation cache")
}
if pc.root != nil || len(pc.activePath) != 0 || pc.pagedOutBytes != 0 {
t.Error("closed runner retains prefix snapshots")
}
for i, kv := range pc.caches {
if kv != nil && kv.Offset() != 0 {
t.Errorf("cache %d retains offset %d after close", i, kv.Offset())
}
}
if got := activeMemory(); got != baseline {
t.Errorf("close retained %d active bytes above baseline %d", got-baseline, baseline)
}
r.Close()
if got := activeMemory(); got != baseline {
t.Errorf("repeated close retained %d active bytes above baseline %d", got-baseline, baseline)
}
})
}
func TestRunnerCloseEmpty(t *testing.T) {
for _, r := range []*Runner{{}, {cache: newPrefixCache(nil)}} {
r.Close()
r.Close()
if r.cache != nil {
t.Error("closed runner retains empty generation cache")
}
}
}