mirror of
https://github.com/ollama/ollama.git
synced 2026-09-22 22:55:24 -04: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.
199 lines
3.8 KiB
Go
199 lines
3.8 KiB
Go
package mlxthread
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"sync/atomic"
|
|
)
|
|
|
|
var ErrStopped = errors.New("mlx thread stopped")
|
|
|
|
type Thread struct {
|
|
name string
|
|
|
|
jobs chan job
|
|
done chan struct{}
|
|
stopping atomic.Bool
|
|
}
|
|
|
|
type job struct {
|
|
fn func() error
|
|
result chan result
|
|
stop bool
|
|
}
|
|
|
|
type result struct {
|
|
err error
|
|
panic *panicError
|
|
}
|
|
|
|
// panicError carries a value recovered from the worker goroutine together with
|
|
// the stack captured at recovery, before the original stack unwinds. Because it
|
|
// implements error, re-panicking with it makes the runtime print the original
|
|
// worker location in the fatal trace instead of this package's re-panic site.
|
|
type panicError struct {
|
|
value any
|
|
stack []byte
|
|
}
|
|
|
|
func (p *panicError) Error() string {
|
|
return fmt.Sprintf("%v\n\nmlx worker stack:\n%s", p.value, p.stack)
|
|
}
|
|
|
|
// Start creates a long-lived worker goroutine locked to one OS thread.
|
|
func Start(name string, init func() error) (*Thread, error) {
|
|
t := &Thread{
|
|
name: name,
|
|
jobs: make(chan job),
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
initResult := make(chan result, 1)
|
|
go t.loop(init, initResult)
|
|
|
|
res := <-initResult
|
|
if res.panic != nil {
|
|
panic(res.panic)
|
|
}
|
|
if res.err != nil {
|
|
return nil, res.err
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// Do runs fn on the locked OS thread.
|
|
//
|
|
// Context cancellation only applies while the work is queued. Once the worker
|
|
// accepts a job, the job runs until fn returns or reaches its own cancellation
|
|
// checks.
|
|
func (t *Thread) Do(ctx context.Context, fn func() error) error {
|
|
res, err := t.enqueue(ctx, fn, false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.panic != nil {
|
|
panic(res.panic)
|
|
}
|
|
return res.err
|
|
}
|
|
|
|
func Call[T any](ctx context.Context, t *Thread, fn func() (T, error)) (T, error) {
|
|
var value T
|
|
err := t.Do(ctx, func() error {
|
|
var err error
|
|
value, err = fn()
|
|
return err
|
|
})
|
|
return value, err
|
|
}
|
|
|
|
// Stop runs cleanup on the locked OS thread and then shuts the worker down.
|
|
func (t *Thread) Stop(ctx context.Context, cleanup func()) error {
|
|
ctx = contextOrBackground(ctx)
|
|
|
|
if !t.stopping.CompareAndSwap(false, true) {
|
|
select {
|
|
case <-t.done:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
res, err := t.enqueue(ctx, func() error {
|
|
if cleanup != nil {
|
|
cleanup()
|
|
}
|
|
return nil
|
|
}, true, true)
|
|
if err != nil {
|
|
if !errors.Is(err, ErrStopped) {
|
|
t.stopping.Store(false)
|
|
}
|
|
return err
|
|
}
|
|
if res.panic != nil {
|
|
panic(res.panic)
|
|
}
|
|
if res.err != nil {
|
|
return res.err
|
|
}
|
|
|
|
select {
|
|
case <-t.done:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (t *Thread) loop(init func() error, initResult chan<- result) {
|
|
runtime.LockOSThread()
|
|
// Deliberately do not unlock. MLX thread-local state belongs to this worker
|
|
// until shutdown so it cannot leak back to arbitrary Go goroutines.
|
|
|
|
res := run(init)
|
|
initResult <- res
|
|
if res.err != nil || res.panic != nil {
|
|
close(t.done)
|
|
return
|
|
}
|
|
|
|
for {
|
|
j := <-t.jobs
|
|
res := run(j.fn)
|
|
j.result <- res
|
|
if j.stop {
|
|
close(t.done)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Thread) enqueue(ctx context.Context, fn func() error, stop, allowStopping bool) (result, error) {
|
|
ctx = contextOrBackground(ctx)
|
|
if err := ctx.Err(); err != nil {
|
|
return result{}, err
|
|
}
|
|
|
|
if !allowStopping && t.stopping.Load() {
|
|
return result{}, ErrStopped
|
|
}
|
|
|
|
resultCh := make(chan result, 1)
|
|
j := job{fn: fn, result: resultCh, stop: stop}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return result{}, ctx.Err()
|
|
case <-t.done:
|
|
return result{}, ErrStopped
|
|
case t.jobs <- j:
|
|
}
|
|
|
|
return <-resultCh, nil
|
|
}
|
|
|
|
func run(fn func() error) (res result) {
|
|
defer func() {
|
|
if v := recover(); v != nil {
|
|
res.panic = &panicError{value: v, stack: debug.Stack()}
|
|
}
|
|
}()
|
|
if fn != nil {
|
|
res.err = fn()
|
|
}
|
|
return res
|
|
}
|
|
|
|
func contextOrBackground(ctx context.Context) context.Context {
|
|
if ctx != nil {
|
|
return ctx
|
|
}
|
|
return context.Background()
|
|
}
|