Files
Daniel Hiltgen 98acec40ae create: add server-side MLX imports and drop GGUF conversion (#14969)
* 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
2026-09-14 20:32:36 -07:00

287 lines
8.1 KiB
Go

package llm
import (
"context"
"encoding/json"
"errors"
"log/slog"
"slices"
"strings"
"time"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/format"
"github.com/ollama/ollama/fs/gguf"
"github.com/ollama/ollama/ml"
)
var ErrLoadRequiredFull = errors.New("unable to load full model on GPU")
type filteredEnv []string
func (e filteredEnv) LogValue() slog.Value {
var attrs []slog.Attr
for _, env := range e {
if key, value, ok := strings.Cut(env, "="); ok {
if filteredEnvLogKey(key) {
attrs = append(attrs, slog.String(key, filteredEnvLogValue(key, value)))
}
}
}
return slog.GroupValue(attrs...)
}
func filteredEnvLogKey(key string) bool {
return strings.HasPrefix(key, "CUDA_") ||
strings.HasPrefix(key, "ROCR_") ||
strings.HasPrefix(key, "ROCM_") ||
strings.HasPrefix(key, "HIP_") ||
strings.HasPrefix(key, "HSA_") ||
strings.HasPrefix(key, "GGML_") ||
slices.Contains([]string{
"PATH",
"LD_LIBRARY_PATH",
"DYLD_LIBRARY_PATH",
}, key)
}
func filteredEnvLogValue(key, value string) string {
for _, token := range []string{"API", "KEY", "TOKEN", "SECRET", "PASSWORD", "PASS", "CREDENTIAL", "AUTH"} {
if strings.Contains(strings.ToUpper(key), token) {
return "[redacted]"
}
}
return value
}
type LlamaServer interface {
ModelPath() string
Load(ctx context.Context, systemInfo ml.SystemInfo, gpus []ml.DeviceInfo, requireFull bool) ([]ml.DeviceID, error)
Ping(ctx context.Context) error
WaitUntilRunning(ctx context.Context) error
Completion(ctx context.Context, req CompletionRequest, fn func(CompletionResponse)) error
Chat(ctx context.Context, req ChatRequest, fn func(ChatResponse)) error
ApplyChatTemplate(ctx context.Context, req ChatRequest) (string, error)
Embedding(ctx context.Context, input string) ([]float32, int, error)
Tokenize(ctx context.Context, content string) ([]int, error)
Detokenize(ctx context.Context, tokens []int) (string, error)
Close() error
MemorySize() (total, vram uint64)
VRAMByGPU(id ml.DeviceID) uint64
Pid() int
GetPort() int
GetDeviceInfos(ctx context.Context) []ml.DeviceInfo
HasExited() bool
ContextLength() int
}
type LlamaServerConfig struct {
DisableJinja bool
ContextShift bool
EnableMTP bool
ManifestDigest string
DraftModelPath string
DraftModelShardPaths []string
}
// LoadModel loads GGUF model metadata from disk.
//
// It collects array values for arrays with a size less than or equal to
// maxArraySize. If maxArraySize is 0, the default value of 1024 is used. If
// the maxArraySize is negative, all arrays are collected.
func LoadModel(model string, maxArraySize int, shards ...string) (*gguf.Model, error) {
return gguf.ReadModel(model, maxArraySize, shards...)
}
// NewLlamaServer creates a new llama-server runner for the given model.
// All GGUF models are served via the upstream llama-server subprocess.
func NewLlamaServer(systemInfo ml.SystemInfo, gpus []ml.DeviceInfo, modelPath string, f *gguf.Model, adapters, projectors []string, opts api.Options, numParallel int, config LlamaServerConfig) (LlamaServer, error) {
slog.Info("using llama-server for model", "model", modelPath)
// Verify the requested context size is <= the model training size
trainCtx := f.KV().ContextLength()
if opts.NumCtx > int(trainCtx) && trainCtx > 0 {
slog.Warn("requested context size too large for model", "num_ctx", opts.NumCtx, "n_ctx_train", trainCtx)
opts.NumCtx = int(trainCtx)
}
kvct := strings.ToLower(envconfig.KvCacheType())
return NewLlamaServerRunner(gpus, modelPath, f, adapters, projectors, opts, numParallel, kvct, config)
}
// Server status types
type ServerStatus int
const (
ServerStatusReady ServerStatus = iota
ServerStatusNoSlotsAvailable
ServerStatusLaunched
ServerStatusLoadingModel
ServerStatusNotResponding
ServerStatusError
)
func (s ServerStatus) String() string {
switch s {
case ServerStatusReady:
return "llm server ready"
case ServerStatusNoSlotsAvailable:
return "llm busy - no slots available"
case ServerStatusLaunched:
return "llm server launched"
case ServerStatusLoadingModel:
return "llm server loading model"
case ServerStatusNotResponding:
return "llm server not responding"
default:
return "llm server error"
}
}
type ServerStatusResponse struct {
Status ServerStatus `json:"status"`
Progress float32 `json:"progress"`
}
// Request/Response types
const (
llamaServerStreamInitialBufferSize = 64 * 1024
// llamaServerStreamMaxBufferSize bounds a single runner response stream line.
llamaServerStreamMaxBufferSize = 8 * format.MegaByte
)
type MediaKind string
const (
MediaKindUnknown MediaKind = ""
MediaKindImage MediaKind = "image"
MediaKindAudio MediaKind = "audio"
)
type MediaData struct {
Data []byte `json:"data"`
ID int `json:"id"`
Kind MediaKind
}
type Message struct {
Role string
Content string
Thinking string
Media []MediaData
ToolCalls []api.ToolCall
ToolName string
ToolCallID string
}
func MessageFromAPI(msg api.Message) Message {
media := make([]MediaData, len(msg.Images))
for i, data := range msg.Images {
media[i] = NewMediaData(i, data)
}
return Message{
Role: msg.Role,
Content: msg.Content,
Thinking: msg.Thinking,
Media: media,
ToolCalls: msg.ToolCalls,
ToolName: msg.ToolName,
ToolCallID: msg.ToolCallID,
}
}
type CompletionRequest struct {
Prompt string
Format json.RawMessage
Media []MediaData
Options *api.Options
Shift bool
Truncate bool
PreservedTokens []string // parser tokens to render as text; ignored by non-llama-server runners
ToolCallTag string // raw generic tool parser tag, if any
LeadingBOS string // textual BOS emitted by Go rendering, if any
// IncludeIntermediateMetrics adds cumulative metrics to non-final responses; final responses always include metrics.
IncludeIntermediateMetrics bool
// Logprobs specifies whether to include log probabilities in the response
Logprobs bool
// TopLogprobs specifies the number of most likely alternative tokens to return (0-20)
TopLogprobs int
}
type ChatRequest struct {
Messages []api.Message
Tools api.Tools
Format json.RawMessage
Options *api.Options
Think *api.ThinkValue
Shift bool
Logprobs bool
TopLogprobs int
}
type ChatResponse struct {
Message api.Message `json:"message"`
DoneReason DoneReason `json:"done_reason"`
Done bool `json:"done"`
PromptEvalCount int `json:"prompt_eval_count"`
PromptEvalCachedCount *int `json:"prompt_eval_cached_count,omitempty"`
PromptEvalDuration time.Duration `json:"prompt_eval_duration"`
EvalCount int `json:"eval_count"`
EvalDuration time.Duration `json:"eval_duration"`
Logprobs []Logprob `json:"logprobs,omitempty"`
}
// DoneReason represents the reason why a completion response is done
type DoneReason int
const (
DoneReasonStop DoneReason = iota
DoneReasonLength
DoneReasonConnectionClosed
)
func (d DoneReason) String() string {
switch d {
case DoneReasonLength:
return "length"
case DoneReasonStop:
return "stop"
default:
return ""
}
}
// TokenLogprob represents log probability information for a single token alternative.
type TokenLogprob struct {
Token string `json:"token"`
Logprob float64 `json:"logprob"`
}
// Logprob contains log probability information for a generated token.
type Logprob struct {
TokenLogprob
TopLogprobs []TokenLogprob `json:"top_logprobs,omitempty"`
}
type CompletionResponse struct {
Content string `json:"content"`
DoneReason DoneReason `json:"done_reason"`
Done bool `json:"done"`
PromptEvalCount int `json:"prompt_eval_count"`
PromptEvalCachedCount *int `json:"prompt_eval_cached_count,omitempty"`
PromptEvalDuration time.Duration `json:"prompt_eval_duration"`
EvalCount int `json:"eval_count"`
EvalDuration time.Duration `json:"eval_duration"`
// Logprobs contains log probability information if requested
Logprobs []Logprob `json:"logprobs,omitempty"`
}