ollama/create/draft.go
Daniel Hiltgen a00d083bb5
mlx: System one support (#18701)
* mlxrunner: restore System One scoring

Carry the MLX implementation and its tests from the original System One
commit. The API/compiler and llama-server implementation already exist on
the release branch, so retain those versions. Adapt the optional model test
to the renamed decision package and its explicit rendering/context setup.

(cherry picked from commit 26e546ddb3)

* Enable MLX System One and add regression coverage

* mlxrunner: add Laya support for System One

Import Laya checkpoints and implement bidirectional decision scoring.

* mlxrunner: release cached buffers after System One scoring

* decision: match Tev1 and Nimble publisher prompt formats

Select Tev1 prompts using renderer metadata and fix JSON formatting, with regression coverage for GGUF and MLX.

* mlx: support Clef and Clef Flash decision models

Add model import, quantization safeguards, and text/image scoring with the Clef joint decision head.

* support vision capabilities for decision models

* Make Clef scoring cancellable between prefill chunks

Preserve joint-head inputs and test cancellation, cleanup, and recovery.

* mlx: separate decision preparation and readout from forward

Let serial and batched scoring share model-owned preparation and readout
without duplicating architecture-specific logic in the runner.

* mlx: add Strands Decider support

* implement caching

---------

Co-authored-by: ParthSareen <parth.sareen@ollama.com>
2026-10-03 13:17:21 -07:00

102 lines
3.8 KiB
Go

package create
import (
"context"
"fmt"
"strings"
)
// CreateDraftLayers imports a draft (speculative-decoding / MTP assistant)
// safetensors model into prefixed tensor and config blobs and returns the
// layers WITHOUT writing a manifest — the caller folds them into the target
// model's manifest. A draft never stands alone; it always accompanies a target
// model named on the Modelfile's FROM line.
//
// It runs the same read → classify → plan → write pipeline as Create. Output
// tensor names keep their source form, namespaced by tensorPrefix (e.g.
// "draft.") so they cannot collide with the target's tensors; config blobs are
// named under configPrefix (e.g. "draft/"). store and fn must be non-nil.
func CreateDraftLayers(ctx context.Context, modelDir, tensorPrefix, configPrefix, quantize string, validation MLXValidationOptions, store BlobStore, fn func(status string)) ([]LayerInfo, error) {
defer releaseMLXCache()
return createDraftLayers(ctx, modelDir, tensorPrefix, configPrefix, quantize, validation, store, fn)
}
func createDraftLayers(ctx context.Context, modelDir, tensorPrefix, configPrefix, quantize string, validation MLXValidationOptions, store BlobStore, fn func(status string)) ([]LayerInfo, error) {
if tensorPrefix == "" {
return nil, fmt.Errorf("draft tensor prefix must not be empty")
}
if configPrefix == "" {
return nil, fmt.Errorf("draft config prefix must not be empty")
}
if err := checkContext(ctx); err != nil {
return nil, err
}
inv, err := ReadInventory(modelDir)
if err != nil {
return nil, fmt.Errorf("read draft model: %w", err)
}
if err := validateMLXSource(inv.Config, true, validation); err != nil {
return nil, err
}
if err := checkContext(ctx); err != nil {
return nil, err
}
class, err := Classify(inv, quantize)
if err != nil {
return nil, err
}
policy, err := newTensorImportTransform(inv)
if err != nil {
return nil, fmt.Errorf("build draft quantization policy for %q: %w", inv.Config.Architecture(), err)
}
specs, err := Plan(inv, class, draftPolicy{policy})
if err != nil {
return nil, fmt.Errorf("plan draft model: %w", err)
}
specs = prefixSpecs(specs, tensorPrefix)
fn(fmt.Sprintf("importing draft (%d tensors%s)", len(inv.Tensors), quantizeStatus(class)))
layers, err := WriteBlobs(ctx, specs, modelDir, store)
if err != nil {
return nil, err
}
configLayers, _, err := importConfigBlobs(ctx, modelDir, configPrefix, nil, store, fn)
if err != nil {
return nil, err
}
return append(layers, configLayers...), nil
}
// prefixSpecs returns specs with prefix prepended to every output blob name and
// output tensor name, leaving the source references (which point at the source
// files) untouched. Scale/bias keys derive from the tensor name, so they inherit
// the prefix automatically.
func prefixSpecs(specs []BlobSpec, prefix string) []BlobSpec {
out := make([]BlobSpec, len(specs))
for i, spec := range specs {
tensors := make([]TensorSpec, len(spec.Tensors))
for j, ts := range spec.Tensors {
ts.Name = prefix + ts.Name
tensors[j] = ts
}
out[i] = BlobSpec{Name: prefix + spec.Name, Tensors: tensors, Metadata: spec.Metadata}
}
return out
}
// draftPolicy wraps an architecture policy to give a draft model's output head
// (tied token embedding or separate lm_head) the requested type directly: draft
// quality only affects acceptance, so the target head's 8-bit promotion buys
// nothing. It is given unprefixed source names; planning runs before prefixSpecs.
type draftPolicy struct{ inner quantizePolicy }
func (p draftPolicy) quantizationType(name string, shape []int32, requested string) string {
if isEmbedTokensWeight(name) || strings.HasSuffix(name, "lm_head.weight") {
if q := normalizeQuantType(requested); isAligned(shape, q) {
return q
}
return ""
}
return p.inner.quantizationType(name, shape, requested)
}