Files
ollama--ollama/llm/server.go
T
2026-07-13 11:56:09 +08:00

308 lines
8.4 KiB
Go

package llm
import (
"context"
"encoding/json"
"errors"
"log/slog"
"os"
"slices"
"strings"
"time"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/format"
"github.com/ollama/ollama/fs/ggml"
"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
DraftModelPath string
}
// LoadModel will load a model from disk. The model must be in the GGML format.
//
// 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) (*ggml.GGML, error) {
if _, err := os.Stat(model); err != nil {
return nil, err
}
f, err := os.Open(model)
if err != nil {
return nil, err
}
defer f.Close()
return ggml.Decode(f, maxArraySize)
}
// NewLlamaServer creates a new llama-server runner for the given model.
// All GGML models are served via the upstream llama-server subprocess.
func NewLlamaServer(systemInfo ml.SystemInfo, gpus []ml.DeviceInfo, modelPath string, f *ggml.GGML, 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
Grammar string // set before sending the request to the subprocess
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
// 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
// Image generation fields
Width int32 `json:"width,omitempty"`
Height int32 `json:"height,omitempty"`
Steps int32 `json:"steps,omitempty"`
Seed int64 `json:"seed,omitempty"`
}
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"`
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"`
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"`
// Image contains base64-encoded image data for image generation
Image string `json:"image,omitempty"`
// Step is the current step in image generation
Step int `json:"step,omitempty"`
// TotalSteps is the total number of steps for image generation
TotalSteps int `json:"total_steps,omitempty"`
}