ollama/x/create/classify_test.go
Patrick Devine 964ea42c09
mlx: x/create rewrite (#16919)
This is a rewrite of the create functionality for the MLX engine.

The core idea behind the create functionality is to break the import/convert into a pipeline of distinct phases:

* Read (scan the safetensors directory for the various bits of metadata)
* Classify (determine what the import type)
* Plan (determine any transforms that need to be done)
* Write (transform any data as necessary and write out the blobs)
* Create the manifest

Each architecture has a "policy" which determines how to convert the model correctly. A number of different formats for safetensors are supported including:

* nvfp4 (two formats: model optimized, torch)
* fp8 datatypes (convert to mxfp8)
* standard bf16 based weights

A number of cleanups/simplifications have been done including:

* using the baked in names for the tensors instead of munging them into something else
* unified 3d expert tensors (instead of separate per expert tensors)
* fewer unnecessary transforms to the various tensors in a model (keep a model as close to the source as possible)
* unified capability checking
* draft model handling (for MTP) is done on the same path

Image generation has been intentionally removed.
2026-07-03 18:30:45 -07:00

149 lines
4.6 KiB
Go

package create
import (
"strings"
"testing"
)
func newInventory(cfg sourceModelConfig, tensors map[string]string) Inventory {
m := make(map[string]SourceTensor)
for name, dtype := range tensors {
m[name] = SourceTensor{Name: name, Dtype: dtype, Shape: []int32{128, 128}, File: "model.safetensors"}
}
return Inventory{Dir: "test", Config: cfg, Tensors: m}
}
func fp8BlockConfig(rows, cols int32) sourceModelConfig {
return sourceModelConfig{
QuantizationConfig: sourceQuantization{QuantMethod: "fp8", WeightBlockSize: []int32{rows, cols}},
}
}
func TestClassify(t *testing.T) {
tests := []struct {
name string
cfg sourceModelConfig
tensors map[string]string
requested string
wantKind SourceKind
wantQuant string
}{
{
name: "float, no quantize",
tensors: map[string]string{"model.embed.weight": "BF16", "model.layers.0.weight": "BF16"},
wantKind: SourceFloat,
},
{
name: "float, quantize int4",
tensors: map[string]string{"model.layers.0.weight": "BF16"},
requested: "int4",
wantKind: SourceFloat,
wantQuant: "int4",
},
{
name: "float, quantize alias fp8 resolves to int8",
tensors: map[string]string{"model.layers.0.weight": "F32"},
requested: "fp8",
wantKind: SourceFloat,
wantQuant: "int8",
},
{
name: "mlx prequantized (.scales)",
tensors: map[string]string{"model.layers.0.weight": "U32", "model.layers.0.scales": "BF16"},
wantKind: SourcePrequantized,
},
{
// ModelOpt NVFP4 whose hf_quant_config.json sidecar is absent:
// recognized from the packed weight + scale companion (finding #7).
name: "modelopt nvfp4 without config sidecar",
tensors: map[string]string{"model.layers.0.weight": "U8", "model.layers.0.weight_scale": "F8_E4M3"},
wantKind: SourcePrequantized,
},
{
name: "compressed-tensors nvfp4 (.weight_packed)",
tensors: map[string]string{"model.layers.0.weight_packed": "U8", "model.layers.0.weight_scale": "F8_E4M3"},
wantKind: SourcePrequantized,
},
{
name: "block-fp8 auto-converts to mxfp8",
cfg: fp8BlockConfig(128, 128),
tensors: map[string]string{"model.layers.0.weight": "F8_E4M3", "model.layers.0.weight_scale_inv": "F32"},
wantKind: SourceBlockFP8,
wantQuant: "mxfp8",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Classify(newInventory(tt.cfg, tt.tensors), tt.requested)
if err != nil {
t.Fatalf("Classify() error = %v", err)
}
if got.Kind != tt.wantKind {
t.Errorf("Kind = %v, want %v", got.Kind, tt.wantKind)
}
if got.Quantize != tt.wantQuant {
t.Errorf("Quantize = %q, want %q", got.Quantize, tt.wantQuant)
}
})
}
}
func TestClassifyErrors(t *testing.T) {
tests := []struct {
name string
cfg sourceModelConfig
tensors map[string]string
requested string
wantErr string
}{
{
name: "invalid quantize type",
tensors: map[string]string{"model.layers.0.weight": "BF16"},
requested: "int3",
wantErr: "unsupported quantize type",
},
{
name: "mlx prequantized rejects requantize",
tensors: map[string]string{"model.layers.0.weight": "U32", "model.layers.0.scales": "BF16"},
requested: "int4",
wantErr: "cannot requantize",
},
{
name: "modelopt nvfp4 rejects requantize",
tensors: map[string]string{"model.layers.0.weight": "U8", "model.layers.0.weight_scale": "F8_E4M3"},
requested: "nvfp4",
wantErr: "cannot requantize",
},
{
name: "block-fp8 rejects quantize flag",
cfg: fp8BlockConfig(128, 128),
tensors: map[string]string{"model.layers.0.weight": "F8_E4M3", "model.layers.0.weight_scale_inv": "F32"},
requested: "nvfp4",
wantErr: "cannot quantize an fp8 source",
},
{
name: "block-fp8 missing block size",
tensors: map[string]string{"model.layers.0.weight": "F8_E4M3", "model.layers.0.weight_scale_inv": "F32"},
wantErr: "missing weight_block_size",
},
{
name: "block-fp8 unsupported block size",
cfg: fp8BlockConfig(64, 64),
tensors: map[string]string{"model.layers.0.weight": "F8_E4M3", "model.layers.0.weight_scale_inv": "F32"},
wantErr: "unsupported fp8 source block size",
},
{
name: "e5m2 fp8 unsupported",
tensors: map[string]string{"model.layers.0.weight": "F8_E5M2"},
wantErr: "F8_E5M2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Classify(newInventory(tt.cfg, tt.tensors), tt.requested)
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("Classify() error = %v, want substring %q", err, tt.wantErr)
}
})
}
}