mirror of
https://github.com/ollama/ollama.git
synced 2026-10-24 04:21:28 +00:00
The MLX runner is the only Go inference runner left and is no longer experimental, so its packages leave x/. The bindings become a top-level mlx package beside the carried patches in mlx/compat, mirroring how llama/ holds the llama.cpp integration, and the runner becomes mlxrunner with the architectures nested under the package they implement. Subpackages move with their parent unless listed. x/mlxrunner/mlx mlx x/internal/mlxthread mlx/mlxthread x/internal/mlxthreadtest mlx/mlxthread/mlxthreadtest x/internal/mlxtest mlx/mlxtest x/quant mlx/quant mlx/compat/*.patch mlx/compat/mlx-c (MLX patches go in mlx/compat/mlx) x/mlxrunner mlxrunner x/models/nn mlxrunner/nn x/models/<arch> mlxrunner/model/<arch> x/mlxrunner/imports.go mlxrunner/model/architectures (new package) x/create create x/safetensors fs/safetensors x/tokenizer mlxrunner/tokenizer Every package keeps its name, so the Go changes are the import path rewrites the moves force, and the CMake, Dockerfile, CI cache keys, drift check and Darwin payload script follow the new paths. Four edits are not paths: the runner's blank architecture imports become the package mlxrunner/model/architectures, so the list to extend for a new model sits beside the architecture directories; a depguard rule keeps the two test harnesses out of non-test code, as the x/internal placement used to; the CI change filter's two entries for the long-deleted x/imagegen/mlx now name the bindings' CMake project and the carried patches, so a change to either builds the payload; and the tokenizer parity test reads its fixtures from its own testdata instead of walking out of x/. x/server and x/imagegen/manifest stay for the next two commits.
203 lines
5.2 KiB
Go
203 lines
5.2 KiB
Go
package mlx
|
|
|
|
import (
|
|
"fmt"
|
|
)
|
|
|
|
// Array lifetimes
|
|
//
|
|
// Every handle belongs to a scope, a set of arrays freed together. A
|
|
// function scope is entered with Scoped, ScopedArrays, ScopedEval, or
|
|
// ScopedAsyncEval and ends when the function returns; a held scope is
|
|
// created with NewScope and ends when its holder closes it. A function
|
|
// scope frees what was created in it or detached into it; a held scope
|
|
// frees what was attached to it. An array moves between scopes in three
|
|
// ways only: by being returned from a function scope to the caller's scope,
|
|
// by Attach into a held scope, or by Detach from a held scope back into the
|
|
// current one for a caller that still reads them.
|
|
//
|
|
// MLX frees a buffer once no handle and no queued graph refers to it, so a
|
|
// graph is built in one function scope and evaluated after that scope ends,
|
|
// and the eval frees each intermediate as it consumes it. The function that
|
|
// finishes a graph opens the scope, returns the arrays that leave it, and
|
|
// discards or closes inside it whatever the graph consumed. An operation
|
|
// that only adds to its caller's graph, a model forward, a cache update, a
|
|
// sampling distribution, builds into the open scope and never opens its
|
|
// own; one that finishes a graph of its own, a cache copy, a sample, opens
|
|
// one like any other finisher. Whoever needs the values evaluates them once
|
|
// the scope has ended: the finisher itself with ScopedEval or
|
|
// ScopedAsyncEval, or its caller with Eval or AsyncEval. A holder holds
|
|
// what outlives the function that produced it, and only a holder gives its
|
|
// arrays up.
|
|
|
|
type Scope struct {
|
|
arrays []*Array
|
|
parent *Scope
|
|
// noEscape refuses to let an array move out of the scope.
|
|
noEscape bool
|
|
}
|
|
|
|
// Function scopes
|
|
|
|
// Scoped runs fn in a function scope. Arrays created or released inside it
|
|
// are freed when fn returns.
|
|
func Scoped(fn func()) {
|
|
s := enterScope()
|
|
defer exitScope(s)
|
|
fn()
|
|
}
|
|
|
|
// ScopedArrays runs fn in a function scope and moves the arrays it returns to
|
|
// the caller's scope.
|
|
func ScopedArrays(fn func() []*Array) []*Array {
|
|
s := enterScope()
|
|
defer exitScope(s)
|
|
ts := fn()
|
|
escape(ts...)
|
|
return ts
|
|
}
|
|
|
|
// ScopedEval runs fn in a function scope, moves the arrays it returns to the
|
|
// caller's scope, ends the scope, and then evaluates them.
|
|
func ScopedEval(fn func() []*Array) []*Array {
|
|
ts := ScopedArrays(fn)
|
|
Eval(ts...)
|
|
return ts
|
|
}
|
|
|
|
// ScopedAsyncEval is ScopedEval with an asynchronous evaluation.
|
|
func ScopedAsyncEval(fn func() []*Array) []*Array {
|
|
ts := ScopedArrays(fn)
|
|
AsyncEval(ts...)
|
|
return ts
|
|
}
|
|
|
|
// Held scopes
|
|
|
|
func NewScope() *Scope {
|
|
return &Scope{}
|
|
}
|
|
|
|
// Attach takes arrays from the function scope that built them or from the
|
|
// root. An array some held scope already holds, this one included, is that
|
|
// holder's to discard or detach first: a second Attach means two owners.
|
|
func (s *Scope) Attach(arrays ...*Array) {
|
|
for _, t := range arrays {
|
|
if t == nil {
|
|
continue
|
|
}
|
|
if t.scope != rootScope && t.scope.parent == nil {
|
|
panic(fmt.Sprintf("mlx: array %q is already held", t.name))
|
|
}
|
|
s.take(t)
|
|
}
|
|
}
|
|
|
|
// Detach moves arrays back to the current scope, for a caller that still
|
|
// reads them.
|
|
func (s *Scope) Detach(arrays ...*Array) {
|
|
for _, t := range arrays {
|
|
if t == nil {
|
|
continue
|
|
}
|
|
if t.scope != s {
|
|
panic(fmt.Sprintf("mlx: array %q is not held by this scope", t.name))
|
|
}
|
|
currentScope.take(t)
|
|
}
|
|
}
|
|
|
|
func (s *Scope) Discard(arrays ...*Array) {
|
|
for _, t := range arrays {
|
|
if t == nil {
|
|
continue
|
|
}
|
|
if t.scope != s {
|
|
panic(fmt.Sprintf("mlx: array %q is not held by this scope", t.name))
|
|
}
|
|
s.remove(t)
|
|
t.free()
|
|
}
|
|
}
|
|
|
|
// Close frees whatever the scope still holds. A nil *Scope holds nothing.
|
|
func (s *Scope) Close() {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.end()
|
|
}
|
|
|
|
// Internals
|
|
|
|
var (
|
|
rootScope = &Scope{}
|
|
currentScope = rootScope
|
|
)
|
|
|
|
func enterScope() *Scope {
|
|
s := &Scope{parent: currentScope}
|
|
currentScope = s
|
|
return s
|
|
}
|
|
|
|
// exitScope ends s.
|
|
func exitScope(s *Scope) {
|
|
if currentScope != s {
|
|
panic("mlx: scope exited out of order")
|
|
}
|
|
currentScope = s.parent
|
|
s.end()
|
|
}
|
|
|
|
// escape moves arrays of the current scope to the caller's scope so they
|
|
// outlive the current one. Arrays held elsewhere are left where they are.
|
|
func escape(arrays ...*Array) {
|
|
if currentScope.parent == nil {
|
|
return
|
|
}
|
|
for _, t := range arrays {
|
|
if t != nil && t.scope == currentScope {
|
|
currentScope.parent.take(t)
|
|
}
|
|
}
|
|
}
|
|
|
|
// end frees the arrays in s.
|
|
func (s *Scope) end() {
|
|
for _, t := range s.arrays {
|
|
t.free()
|
|
}
|
|
s.arrays = nil
|
|
}
|
|
|
|
// take moves t into s, out of the scope it was in.
|
|
func (s *Scope) take(t *Array) {
|
|
if from := t.scope; from != nil {
|
|
if from == s {
|
|
return
|
|
}
|
|
if !t.valid() {
|
|
panic(fmt.Sprintf("mlx: array %q used after its scope ended", t.name))
|
|
}
|
|
if from.noEscape {
|
|
panic(fmt.Sprintf("mlx: array %q escaped a scope that allows no escape", t.name))
|
|
}
|
|
from.remove(t)
|
|
}
|
|
t.scope = s
|
|
s.arrays = append(s.arrays, t)
|
|
}
|
|
|
|
// remove drops t from s's list. An array usually leaves the scope that just
|
|
// built it, so the search runs from the end; order in the list is free.
|
|
func (s *Scope) remove(t *Array) {
|
|
for i := len(s.arrays) - 1; i >= 0; i-- {
|
|
if s.arrays[i] == t {
|
|
last := len(s.arrays) - 1
|
|
s.arrays[i] = s.arrays[last]
|
|
s.arrays = s.arrays[:last]
|
|
return
|
|
}
|
|
}
|
|
}
|