mirror of
https://github.com/ollama/ollama.git
synced 2026-07-07 00:08:13 +00:00
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.
149 lines
4.6 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|