ollama/x/create/classify.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

166 lines
5.1 KiB
Go

package create
import (
"fmt"
"strings"
"github.com/ollama/ollama/x/quant"
)
// SourceKind is the overarching dtype for a given safetensors model
type SourceKind int
const (
SourceFloat SourceKind = iota // bf16/fp16/fp32 — quantizable on request
SourceBlockFP8 // HF block-FP8 — auto-converted to mxfp8
SourcePrequantized // already quantized — copied through
)
func (k SourceKind) String() string {
switch k {
case SourceFloat:
return "float"
case SourceBlockFP8:
return "block-fp8"
case SourcePrequantized:
return "prequantized"
default:
return "unknown"
}
}
// Classification is the decision about a source model: its kind and the
// quantization that will actually be applied. An empty Quantize means the
// tensors are stored at source precision (no quantization).
type Classification struct {
Kind SourceKind
Quantize string
}
// Classify decides a source model's kind and resolves the effective
// quantization from the user's requested type, rejecting requests that are not
// allowed for the kind.
func Classify(inv Inventory, requested string) (Classification, error) {
requested, err := normalizeRequested(requested)
if err != nil {
return Classification{}, err
}
if name, ok := firstUnsupportedFP8(inv); ok {
return Classification{}, fmt.Errorf("unsupported fp8 source: tensor %s is F8_E5M2; only F8_E4M3 block-FP8 sources are supported", name)
}
switch detectKind(inv) {
case SourceFloat:
return Classification{Kind: SourceFloat, Quantize: requested}, nil
case SourcePrequantized:
if requested != "" {
return Classification{}, fmt.Errorf("cannot requantize an already-quantized source model (requested %q): only bf16/fp16/fp32 sources can be quantized", requested)
}
return Classification{Kind: SourcePrequantized}, nil
case SourceBlockFP8:
rows, cols, ok := inv.Config.HFFP8WeightBlockSize()
if !ok {
return Classification{}, fmt.Errorf("fp8 source model is missing weight_block_size metadata")
}
if rows != 128 || cols != 128 {
return Classification{}, fmt.Errorf("unsupported fp8 source block size %dx%d (only 128x128 is supported)", rows, cols)
}
if requested != "" {
return Classification{}, fmt.Errorf("cannot quantize an fp8 source model (requested %q): fp8 sources are converted to mxfp8 automatically; only bf16/fp16/fp32 sources can be quantized", requested)
}
return Classification{Kind: SourceBlockFP8, Quantize: "mxfp8"}, nil
}
return Classification{}, fmt.Errorf("could not classify source model in %s", inv.Dir)
}
// normalizeRequested validates the user's quantize value and returns its
// canonical form ("" for no quantization).
func normalizeRequested(requested string) (string, error) {
if strings.TrimSpace(requested) == "" {
return "", nil
}
c := quant.Canonical(requested)
if c == "" {
return "", fmt.Errorf("unsupported quantize type %q: supported types are int4, int8, nvfp4, mxfp4, mxfp8", requested)
}
return c, nil
}
// detectKind sorts a source into Float, BlockFP8, or Prequantized using only
// the inventory's tensor names, dtypes, and config. Prequantized is detected
// from the tensors themselves, so a model whose quantization config sidecar is
// missing (e.g. a ModelOpt checkpoint without hf_quant_config.json) is still
// recognized as already-quantized and not mistaken for a float model.
func detectKind(inv Inventory) SourceKind {
var hasMLXScales, hasPacked, hasNVFP4Scale, hasFP8Weight bool
for name, t := range inv.Tensors {
switch {
case strings.HasSuffix(name, ".scales"):
hasMLXScales = true
case strings.HasSuffix(name, ".weight_packed"):
hasPacked = true
case strings.HasSuffix(name, ".weight_scale"):
// An NVFP4 per-block scale sits on a packed (U8) weight. A
// block-FP8 source also has a scale companion, but its weight is
// F8_E4M3 — so the base weight's dtype disambiguates the two.
if bt, ok := inv.Tensors[strings.TrimSuffix(name, "_scale")]; ok && isPackedDtype(bt.Dtype) {
hasNVFP4Scale = true
}
}
if strings.HasSuffix(name, ".weight") && isE4M3Dtype(t.Dtype) {
hasFP8Weight = true
}
}
switch {
case hasMLXScales || hasPacked || hasNVFP4Scale:
return SourcePrequantized
case hasFP8Weight:
return SourceBlockFP8
default:
return SourceFloat
}
}
// firstUnsupportedFP8 returns the name of the first F8_E5M2 weight in the
// source, if any. We decode only E4M3, so an E5M2 source must be rejected
// explicitly rather than silently mishandled.
func firstUnsupportedFP8(inv Inventory) (string, bool) {
for name, t := range inv.Tensors {
if strings.HasSuffix(name, ".weight") && isE5M2Dtype(t.Dtype) {
return name, true
}
}
return "", false
}
func isPackedDtype(dtype string) bool {
switch strings.ToUpper(dtype) {
case "U8", "U32": // current .weight_scale producers ship U8; U32 covers a future word-packed source
return true
default:
return false
}
}
func isE4M3Dtype(dtype string) bool {
switch strings.ToUpper(dtype) {
case "F8_E4M3", "F8_E4M3FN":
return true
default:
return false
}
}
func isE5M2Dtype(dtype string) bool {
switch strings.ToUpper(dtype) {
case "F8_E5M2", "F8_E5M2FNUZ":
return true
default:
return false
}
}