mirror of
https://github.com/ollama/ollama.git
synced 2026-09-21 05:28:00 -05:00
* create: add server-side MLX imports and drop GGUF conversion Support safetensors imports through the MLX create pipeline both locally and on the server, including remote upload/staging, draft layer handling, cancellation propagation, transfer limits, and shared manifest/blob writing. Limit GGUF create to wrapping existing GGUF inputs into Ollama manifests. Remove the in-tree safetensors-to-GGUF converter, server quantization path, and converter-only dependencies so GGUF conversion and quantization stay in llama.cpp tooling. Keep the MLX path focused on supported safetensors model creation with validation before MLX work, and expose that flow without the --experimental CLI gate. * address comments * add client side gguf create fast path * address comments * rebase adjustments
353 lines
8.9 KiB
Go
353 lines
8.9 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/ollama/ollama/envconfig"
|
|
fsgguf "github.com/ollama/ollama/fs/gguf"
|
|
"github.com/ollama/ollama/manifest"
|
|
"github.com/ollama/ollama/version"
|
|
)
|
|
|
|
// A blob's metadata block is extracted once into a file beside the model
|
|
// store, keyed by blob digest. Values are held verbatim rather than derived, so
|
|
// reading them differently later needs no invalidation, and values left out are
|
|
// recorded by key so an absent key stays distinct from an uncopied one.
|
|
|
|
// Comfortably above per-layer arrays, which scale with block count, and far
|
|
// below any tokenizer vocabulary.
|
|
const ggufMetadataMaxArray = 4096
|
|
|
|
type ggufMetadata struct {
|
|
OllamaVersion string `json:"ollama_version,omitempty"`
|
|
|
|
// Values keyed exactly as they appear in the file. Never omitempty: absent
|
|
// on load means the file is not ours, and an all-omitted file is still
|
|
// usable.
|
|
KV map[string]any `json:"kv"`
|
|
// Keys present in the file whose values were too large to copy.
|
|
Omitted []string `json:"omitted,omitempty"`
|
|
}
|
|
|
|
// Valid reports whether the key is present. Keys are architecture qualified the
|
|
// same way fs/gguf does it.
|
|
func (m ggufMetadata) Valid(key string) bool {
|
|
_, ok := m.lookup(key)
|
|
return ok
|
|
}
|
|
|
|
func (m ggufMetadata) String(key string) string {
|
|
v, _ := m.lookup(key)
|
|
s, _ := v.(string)
|
|
return s
|
|
}
|
|
|
|
func (m ggufMetadata) Int(key string) int64 {
|
|
n, ok := m.number(key)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
i, err := n.Int64()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return i
|
|
}
|
|
|
|
func (m ggufMetadata) Bool(key string, defaultValue ...bool) bool {
|
|
v, _ := m.lookup(key)
|
|
if value, valid := v.(bool); valid {
|
|
return value
|
|
}
|
|
if len(defaultValue) > 0 {
|
|
return defaultValue[0]
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m ggufMetadata) number(key string) (json.Number, bool) {
|
|
v, _ := m.lookup(key)
|
|
n, ok := v.(json.Number)
|
|
return n, ok
|
|
}
|
|
|
|
// Keys returns every key the file carried, including omitted ones.
|
|
func (m ggufMetadata) Keys() []string {
|
|
keys := make([]string, 0, len(m.KV)+len(m.Omitted))
|
|
for k := range m.KV {
|
|
keys = append(keys, k)
|
|
}
|
|
return append(keys, m.Omitted...)
|
|
}
|
|
|
|
func (m ggufMetadata) lookup(key string) (any, bool) {
|
|
if !strings.HasPrefix(key, "general.") && !strings.HasPrefix(key, "tokenizer.") {
|
|
arch, _ := m.KV["general.architecture"].(string)
|
|
key = arch + "." + key
|
|
}
|
|
v, ok := m.KV[key]
|
|
return v, ok
|
|
}
|
|
|
|
func ggufMetadataPath(digest string) (string, error) {
|
|
if err := manifest.ValidateDigest(digest); err != nil {
|
|
return "", fmt.Errorf("%w: %q", err, digest)
|
|
}
|
|
return filepath.Join(ggufMetadataDir(), strings.ReplaceAll(digest, ":", "-")+".json"), nil
|
|
}
|
|
|
|
func ggufMetadataDir() string {
|
|
return filepath.Join(envconfig.Models(), "metadata")
|
|
}
|
|
|
|
// readGGUFMetadata extracts the blob when there is no usable metadata file. Best
|
|
// effort throughout: a bad file is re-extracted, a failed write is dropped.
|
|
func readGGUFMetadata(digest string) (ggufMetadata, error) {
|
|
path, err := ggufMetadataPath(digest)
|
|
if err != nil {
|
|
return ggufMetadata{}, err
|
|
}
|
|
if md, ok := loadGGUFMetadata(path); ok {
|
|
return md, nil
|
|
}
|
|
|
|
blob, err := manifest.BlobsPath(digest)
|
|
if err != nil {
|
|
return ggufMetadata{}, err
|
|
}
|
|
md, err := extractGGUFMetadata(blob)
|
|
if err != nil {
|
|
return ggufMetadata{}, err
|
|
}
|
|
|
|
writeGGUFMetadata(path, blob, md)
|
|
return md, nil
|
|
}
|
|
|
|
func loadGGUFMetadata(path string) (ggufMetadata, bool) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
slog.Debug("could not read gguf metadata file", "path", path, "error", err)
|
|
}
|
|
return ggufMetadata{}, false
|
|
}
|
|
|
|
md, err := decodeGGUFMetadata(data)
|
|
if err != nil {
|
|
slog.Debug("ignoring unusable gguf metadata file", "path", path, "error", err)
|
|
return ggufMetadata{}, false
|
|
}
|
|
return md, true
|
|
}
|
|
|
|
func writeGGUFMetadata(path, blob string, md ggufMetadata) {
|
|
data, err := json.Marshal(md)
|
|
if err != nil {
|
|
slog.Debug("could not encode gguf metadata", "path", path, "error", err)
|
|
return
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
slog.Debug("could not create gguf metadata dir", "path", path, "error", err)
|
|
return
|
|
}
|
|
|
|
tmp, err := os.CreateTemp(filepath.Dir(path), ".gguf-metadata-*.tmp")
|
|
if err != nil {
|
|
slog.Debug("could not create gguf metadata temp file", "path", path, "error", err)
|
|
return
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
|
|
if _, err = tmp.Write(data); err == nil {
|
|
err = tmp.Sync()
|
|
}
|
|
if cerr := tmp.Close(); err == nil {
|
|
err = cerr
|
|
}
|
|
if err == nil {
|
|
// Can fail transiently on Windows while another process holds the path.
|
|
err = os.Rename(tmp.Name(), path)
|
|
}
|
|
if err == nil {
|
|
// Blob removal precedes metadata removal. Rechecking after publication
|
|
// ensures either this writer or a concurrent remover cleans up the file.
|
|
if _, statErr := os.Stat(blob); errors.Is(statErr, os.ErrNotExist) {
|
|
if removeErr := os.Remove(path); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
|
|
slog.Debug("could not remove gguf metadata file", "path", path, "error", removeErr)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
if err != nil {
|
|
slog.Debug("could not write gguf metadata file", "path", path, "error", err)
|
|
}
|
|
}
|
|
|
|
// removeGGUFMetadata drops metadata for deleted blobs. Pass only digests whose
|
|
// last reference is gone.
|
|
func removeGGUFMetadata(digests ...string) {
|
|
for _, digest := range digests {
|
|
path, err := ggufMetadataPath(digest)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
slog.Debug("could not remove gguf metadata file", "path", path, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func pruneGGUFMetadata() {
|
|
dir := ggufMetadataDir()
|
|
entries, err := os.ReadDir(dir)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return
|
|
}
|
|
if err != nil {
|
|
slog.Debug("could not read gguf metadata dir", "path", dir, "error", err)
|
|
return
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if entry.IsDir() || !strings.HasSuffix(name, ".json") {
|
|
continue
|
|
}
|
|
|
|
digest := strings.TrimSuffix(name, ".json")
|
|
blob, err := manifest.BlobsPath(digest)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(blob); err == nil || !errors.Is(err, os.ErrNotExist) {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, name)
|
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
slog.Debug("could not remove gguf metadata file", "path", path, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// extractGGUFMetadata reads the whole block; proving a key absent means reaching
|
|
// the end. Normalized through JSON so extracted and loaded metadata hold the
|
|
// same value types.
|
|
func extractGGUFMetadata(path string) (ggufMetadata, error) {
|
|
md, err := scanGGUFMetadata(path)
|
|
if err != nil {
|
|
return ggufMetadata{}, err
|
|
}
|
|
md.OllamaVersion = version.Version
|
|
|
|
data, err := json.Marshal(md)
|
|
if err != nil {
|
|
return ggufMetadata{}, err
|
|
}
|
|
return decodeGGUFMetadata(data)
|
|
}
|
|
|
|
func decodeGGUFMetadata(data []byte) (ggufMetadata, error) {
|
|
// UseNumber keeps integers exact; the default would widen them to float64.
|
|
dec := json.NewDecoder(bytes.NewReader(data))
|
|
dec.UseNumber()
|
|
var md ggufMetadata
|
|
if err := dec.Decode(&md); err != nil {
|
|
return ggufMetadata{}, err
|
|
}
|
|
if md.KV == nil {
|
|
return ggufMetadata{}, fmt.Errorf("no metadata")
|
|
}
|
|
return md, nil
|
|
}
|
|
|
|
func scanGGUFMetadata(path string) (ggufMetadata, error) {
|
|
metadata, err := fsgguf.ReadFileMetadata(path, ggufMetadataMaxArray)
|
|
if err != nil {
|
|
return ggufMetadata{}, err
|
|
}
|
|
|
|
md := ggufMetadata{KV: make(map[string]any)}
|
|
omitted := metadata.OmittedKeys()
|
|
for key, value := range metadata.Values() {
|
|
if slices.Contains(omitted, key) {
|
|
continue
|
|
}
|
|
if omitValue(value) {
|
|
omitted = append(omitted, key)
|
|
continue
|
|
}
|
|
md.KV[key] = value
|
|
}
|
|
slices.Sort(omitted)
|
|
md.Omitted = omitted
|
|
return md, nil
|
|
}
|
|
|
|
// omitValue reports values left out of the metadata file: long arrays, which are the
|
|
// tokenizer, and non-finite floats, which JSON cannot represent.
|
|
func omitValue(v any) bool {
|
|
switch v := v.(type) {
|
|
case float32:
|
|
return !finite(float64(v))
|
|
case float64:
|
|
return !finite(v)
|
|
case []float32:
|
|
for _, f := range v {
|
|
if !finite(float64(f)) {
|
|
return true
|
|
}
|
|
}
|
|
case []float64:
|
|
for _, f := range v {
|
|
if !finite(f) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return oversized(v)
|
|
}
|
|
|
|
func finite(f float64) bool {
|
|
return !math.IsInf(f, 0) && !math.IsNaN(f)
|
|
}
|
|
|
|
func oversized(v any) bool {
|
|
switch v := v.(type) {
|
|
case []string:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []int8:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []int16:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []int32:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []int64:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []uint8:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []uint16:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []uint32:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []uint64:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []float32:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []float64:
|
|
return len(v) > ggufMetadataMaxArray
|
|
case []bool:
|
|
return len(v) > ggufMetadataMaxArray
|
|
}
|
|
return false
|
|
}
|