mirror of
https://github.com/ollama/ollama.git
synced 2026-09-21 13:38:14 -05:00
* glimmer: implement the Muse Glimmer model MLX model (language + vision encoder) with DFlash draft wiring, llama-server DFlash support and rope-interleave fix, renderer and parser, tokenizer fixes, and the import quantization policy. * mlxrunner: report committed prefill chunks after the sweep and eval The drafter's flush evaluates its report, and an eval that runs while the chunk's construction handles are still live cannot free any intermediate buffer. On media chunks that retention keeps the whole vision tower resident and grinds the Metal allocator at its limit until the request dies. Pin the report's inputs across the sweep, report after the chunk materializes, and release media items after the report so a drafter can still capture the rows its deferred flush embeds. * ci: retry CUDA pre-release download
337 lines
9.6 KiB
Go
337 lines
9.6 KiB
Go
package glimmer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ollama/ollama/x/internal/mlxtest"
|
|
"github.com/ollama/ollama/x/mlxrunner/cache"
|
|
"github.com/ollama/ollama/x/mlxrunner/mlx"
|
|
"github.com/ollama/ollama/x/models/nn"
|
|
"github.com/ollama/ollama/x/tokenizer"
|
|
)
|
|
|
|
func testConfig() Config {
|
|
return Config{
|
|
HiddenSize: 6656,
|
|
NumHiddenLayers: 4,
|
|
IntermediateSize: 19968,
|
|
NumAttentionHeads: 32,
|
|
NumKeyValueHeads: 2,
|
|
HeadDim: 128,
|
|
VocabSize: 202048,
|
|
RMSNormEps: 1e-5,
|
|
PostNormEps: 1e-8,
|
|
RopeTheta: 500000,
|
|
MaxPositionEmbeddings: 16384,
|
|
SlidingWindow: 2048,
|
|
LayerTypes: []string{"sliding_attention", "sliding_attention", "sliding_attention", "full_attention"},
|
|
NoRopeLayers: []int32{1, 1, 1, 0},
|
|
NormalizeTokenEmbeddings: true,
|
|
UseQKNorm: true,
|
|
QKScaleFactor: 43.7840518911,
|
|
UseAttentionOutputGate: true,
|
|
OutputMultiplier: 0.19611613,
|
|
OutputSoftCapTemp: 20,
|
|
}
|
|
}
|
|
|
|
func marshalConfig(t *testing.T, cfg Config) []byte {
|
|
t.Helper()
|
|
data, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func TestParseConfig(t *testing.T) {
|
|
cfg, err := parseConfig(marshalConfig(t, testConfig()))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if got, want := cfg.AttentionScale, float32(1/math.Sqrt(128)); math.Abs(float64(got-want)) > 1e-7 {
|
|
t.Errorf("AttentionScale = %g, want %g", got, want)
|
|
}
|
|
if got, want := cfg.QueryScale, float32(float64(cfg.QKScaleFactor)/math.Sqrt(128)); math.Abs(float64(got-want)) > 1e-7 {
|
|
t.Errorf("QueryScale = %g, want %g", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseOfficialHFConfig(t *testing.T) {
|
|
data := []byte(`{
|
|
"text_config": {
|
|
"hidden_size": 6656,
|
|
"num_hidden_layers": 4,
|
|
"intermediate_size": 19968,
|
|
"num_attention_heads": 32,
|
|
"num_key_value_heads": 2,
|
|
"head_dim": 128,
|
|
"vocab_size": 202048,
|
|
"rms_norm_eps": 0.00001,
|
|
"post_norm_eps": 1e-8,
|
|
"max_position_embeddings": 131072,
|
|
"sliding_window": 2048,
|
|
"layer_types": ["sliding_attention", "sliding_attention", "sliding_attention", "full_attention"],
|
|
"layer_rope_theta": [500000, 500000, 500000, 0],
|
|
"qk_scale_factor": 3.87,
|
|
"output_multiplier": 0.19611613513818404,
|
|
"final_logit_softcapping": 20,
|
|
"rope_parameters": {"rope_theta": 500000}
|
|
},
|
|
"vision_config": {
|
|
"hidden_size": 1536,
|
|
"intermediate_size": 8960,
|
|
"num_attention_heads": 16,
|
|
"num_hidden_layers": 4,
|
|
"patch_size": 14,
|
|
"patch_temporal": 2,
|
|
"merge_size": 2,
|
|
"pos_emb_height": 32,
|
|
"pos_emb_width": 32,
|
|
"layer_types": ["window_attention", "window_attention", "window_attention", "full_attention"]
|
|
},
|
|
"image_token_id": 200092,
|
|
"out_hidden_size": 6144,
|
|
"projector_hidden_size": 4096
|
|
}`)
|
|
|
|
cfg, err := parseConfig(data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.QueryScale != 3.87 {
|
|
t.Errorf("QueryScale = %g, want 3.87", cfg.QueryScale)
|
|
}
|
|
if cfg.MaxPositionEmbeddings != 131072 {
|
|
t.Errorf("MaxPositionEmbeddings = %d, want 131072", cfg.MaxPositionEmbeddings)
|
|
}
|
|
if !slices.Equal(cfg.NoRopeLayers, []int32{1, 1, 1, 0}) {
|
|
t.Errorf("NoRopeLayers = %v, want [1 1 1 0]", cfg.NoRopeLayers)
|
|
}
|
|
if cfg.NormalizeTokenEmbeddings {
|
|
t.Error("NormalizeTokenEmbeddings = true, want false for pre-normalized HF embeddings")
|
|
}
|
|
if !cfg.NormalizeVisionEmbeddings {
|
|
t.Error("NormalizeVisionEmbeddings = false, want true")
|
|
}
|
|
if cfg.VisionRoPEInterleaved {
|
|
t.Error("VisionRoPEInterleaved = true, want half-rotation layout")
|
|
}
|
|
if cfg.VisionSparseAttentionFactor != 4 || cfg.VisionOutputDim != 6144 || cfg.VisionAdapterDim != 4096 {
|
|
t.Errorf("vision config = factor %d, output %d, adapter %d", cfg.VisionSparseAttentionFactor, cfg.VisionOutputDim, cfg.VisionAdapterDim)
|
|
}
|
|
}
|
|
|
|
func TestValidateTokenizerEOS(t *testing.T) {
|
|
const tokenizerJSON = `{
|
|
"model": {
|
|
"type": "BPE",
|
|
"vocab": {"x": 0},
|
|
"merges": []
|
|
},
|
|
"added_tokens": [
|
|
{"id": 1, "content": "<|start|>", "special": true},
|
|
{"id": 2, "content": "<|message|>", "special": true},
|
|
{"id": 3, "content": "<|eom|>", "special": true},
|
|
{"id": 4, "content": "<|eot|>", "special": true},
|
|
{"id": 5, "content": "<|end_of_text|>", "special": true}
|
|
]
|
|
}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
eos string
|
|
want string
|
|
}{
|
|
{name: "official boundaries", eos: `[5, 4]`},
|
|
{name: "message boundary is terminal", eos: `[5, 3, 4]`, want: "<|eom|> must be a non-terminal"},
|
|
{name: "end of turn is not terminal", eos: `[5]`, want: "<|eot|> must be an EOS token"},
|
|
{name: "end of text is not terminal", eos: `[4]`, want: "<|end_of_text|> must be an EOS token"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tok, err := tokenizer.LoadFromBytesWithConfig([]byte(tokenizerJSON), &tokenizer.TokenizerConfig{
|
|
GenerationConfigJSON: []byte(`{"eos_token_id":` + tt.eos + `}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = validateTokenizer(tok)
|
|
if tt.want == "" {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
|
t.Fatalf("validateTokenizer() error = %v, want containing %q", err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseConfigRejectsInvalidSchedules(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*Config)
|
|
want string
|
|
}{
|
|
{"layer count", func(cfg *Config) { cfg.LayerTypes = cfg.LayerTypes[:3] }, "layer_types has 3 entries"},
|
|
{"layer type", func(cfg *Config) { cfg.LayerTypes[0] = "linear_attention" }, `unsupported value "linear_attention"`},
|
|
{"RoPE flag", func(cfg *Config) { cfg.NoRopeLayers[0] = 2 }, "no_rope_layers[0] has unsupported value 2"},
|
|
{"sliding window", func(cfg *Config) { cfg.SlidingWindow = 0 }, "invalid sliding_window: 0"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := testConfig()
|
|
tt.mutate(&cfg)
|
|
_, err := parseConfig(marshalConfig(t, cfg))
|
|
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
|
t.Fatalf("parseConfig() error = %v, want containing %q", err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewCachesMatchesAttentionSchedule(t *testing.T) {
|
|
cfg := testConfig()
|
|
m := Model{
|
|
Config: &cfg,
|
|
Layers: []*Layer{
|
|
{IsSliding: true},
|
|
{IsSliding: true},
|
|
{IsSliding: true},
|
|
{IsSliding: false},
|
|
},
|
|
}
|
|
|
|
caches := m.NewCaches()
|
|
for i := range 3 {
|
|
if _, ok := caches[i].(*cache.RotatingKVCache); !ok {
|
|
t.Errorf("cache %d = %T, want *cache.RotatingKVCache", i, caches[i])
|
|
}
|
|
}
|
|
if _, ok := caches[3].(*cache.KVCache); !ok {
|
|
t.Errorf("cache 3 = %T, want *cache.KVCache", caches[3])
|
|
}
|
|
}
|
|
|
|
func TestUnembedReturnsFloat32Logits(t *testing.T) {
|
|
mlxtest.Setup(t)
|
|
|
|
input := mlx.FromValues([]float32{1}, 1, 1, 1).AsType(mlx.DTypeBFloat16)
|
|
weight := mlx.FromValues([]float32{1, 2}, 2, 1).AsType(mlx.DTypeBFloat16)
|
|
m := Model{
|
|
LMHead: nn.NewLinear(weight, nil),
|
|
Config: &Config{
|
|
OutputMultiplier: 0.19611613,
|
|
OutputSoftCapTemp: 20,
|
|
},
|
|
}
|
|
|
|
if got := m.Unembed(input).DType(); got != mlx.DTypeFloat32 {
|
|
t.Fatalf("Unembed() dtype = %v, want %v", got, mlx.DTypeFloat32)
|
|
}
|
|
}
|
|
|
|
func TestComputeImageSizeMatchesReference(t *testing.T) {
|
|
tests := []struct {
|
|
width, height int
|
|
targetWidth, targetHeight int
|
|
tokens int
|
|
}{
|
|
{1, 1, 28, 28, 1},
|
|
{100, 100, 112, 112, 16},
|
|
{640, 480, 644, 476, 391},
|
|
{480, 640, 476, 644, 391},
|
|
{1920, 1080, 1932, 1092, 2691},
|
|
{1080, 1920, 1092, 1932, 2691},
|
|
{4000, 4000, 1792, 1792, 4096},
|
|
{8192, 512, 7168, 448, 4096},
|
|
{512, 8192, 448, 7168, 4096},
|
|
{1234, 987, 1260, 1008, 1620},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("%dx%d", tt.width, tt.height), func(t *testing.T) {
|
|
width, height, tokens := computeImageSize(tt.width, tt.height, 28, maxImageTokens)
|
|
if width != tt.targetWidth || height != tt.targetHeight || tokens != tt.tokens {
|
|
t.Fatalf("computeImageSize(%d, %d) = (%d, %d, %d), want (%d, %d, %d)",
|
|
tt.width, tt.height, width, height, tokens,
|
|
tt.targetWidth, tt.targetHeight, tt.tokens)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComputeImageSizeDeterministicTieBreak(t *testing.T) {
|
|
width, height, tokens := computeImageSize(128, 128, 28, maxImageTokens)
|
|
if width != 140 || height != 140 || tokens != 25 {
|
|
t.Fatalf("computeImageSize() = %dx%d (%d tokens), want 140x140 (25 tokens)", width, height, tokens)
|
|
}
|
|
}
|
|
|
|
func TestSparseVisionPermutation(t *testing.T) {
|
|
permutation, lengths := sparseVisionPermutation(3, 5, 2, 3)
|
|
wantPermutation := []int32{0, 1, 2, 5, 6, 7, 3, 4, 8, 9, 10, 11, 12, 13, 14}
|
|
wantLengths := []int{6, 4, 3, 2}
|
|
if !slices.Equal(permutation, wantPermutation) {
|
|
t.Fatalf("permutation = %v, want %v", permutation, wantPermutation)
|
|
}
|
|
if !slices.Equal(lengths, wantLengths) {
|
|
t.Fatalf("lengths = %v, want %v", lengths, wantLengths)
|
|
}
|
|
}
|
|
|
|
func TestApplyVisionRoPELayouts(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
interleaved bool
|
|
want []float32
|
|
}{
|
|
{
|
|
name: "official half rotation",
|
|
want: []float32{
|
|
1*0.5 - 3*0.75,
|
|
2*0.25 - 4*0.125,
|
|
3*0.5 + 1*0.75,
|
|
4*0.25 + 2*0.125,
|
|
},
|
|
},
|
|
{
|
|
name: "legacy interleaved rotation",
|
|
interleaved: true,
|
|
want: []float32{
|
|
1*0.5 - 2*0.75,
|
|
1*0.75 + 2*0.5,
|
|
3*0.25 - 4*0.125,
|
|
3*0.125 + 4*0.25,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mlxtest.Setup(t)
|
|
|
|
x := mlx.FromValues([]float32{1, 2, 3, 4}, 1, 1, 1, 4)
|
|
cos := mlx.FromValues([]float32{0.5, 0.25}, 1, 2)
|
|
sin := mlx.FromValues([]float32{0.75, 0.125}, 1, 2)
|
|
got := applyVisionRoPE(x, cos, sin, tt.interleaved)
|
|
mlx.Eval(got)
|
|
if !slices.Equal(got.Floats(), tt.want) {
|
|
t.Fatalf("applyVisionRoPE() = %v, want %v", got.Floats(), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|