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
287 lines
8.1 KiB
Go
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"`
|
|
}
|