XVI · Apple MLX acceleration (welvet.mlx) / CHAPTER 22
Apple MLX: SyncToMLX and forward
github.com/openfluke/welvet/nativemlxdarwin arm64
Why it exists
Run a Go Dense layer on Apple MLX GPU with an explicit SyncToMLX lifetime: forward-only, no Mount/TrainMSE twin.
What it is
Install welvet.mlx, SyncToMLX an identity Dense, set BackendMLX, and forward a Float32 tensor.
User-owned sync
MLX does not use nativemetal.Mount. Weights stay in Go Stores until you call SyncToMLX (caps-check + capture). Then set Exec.Backend = core.BackendMLX (or use core.PreferMLX() when the dylib + Metal GPU are ready) and call the ordinary layer Forward.
must(layer.SyncToMLX())
layer.Exec.Backend = core.BackendMLX
_, post, err := dense.Forward(layer, x)Requirements and honesty
Go Welvet v1.7.0 adds the independent welvet.mlx 1.0.0 library at github.com/openfluke/welvet.mlx. Install into the Go tree:
cd ../welvet.mlx
./scripts/macos_install_go.sh
Needs Apple silicon macOS, cgo, shipped libwelvet_mlx.dylib, and a Metal GPU. nativemlx.Available() is false without Metal: there is no CPU fallback labeled as MLX and no silent fallthrough to welvet.metal. Other hosts print SKIP.
Forward-only: no TrainMSE / NormalBP / SyncToGo train loop on MLX. SyncFromMLX exists for API symmetry (mirror → Go) but forward does not mutate W. Caps reject unsupported layer×dtype mounts.
Sibling contract: docs/MLX_ONLY.md. Caps tour: §111. Dense + Sequential: §112. Live matrices: §113. Metal train twin remains §82-88.
Go example
examples/110-apple-mlx/main.go
cd welvet/examples/110-apple-mlx && source ../env.sh && go run .// SyncToMLX one Dense layer, forward on Apple MLX GPU, release nothing explicit
// (shared DefaultSession lives for the process).
package main
import (
"fmt"
"log"
"runtime"
"github.com/openfluke/welvet/core"
"github.com/openfluke/welvet/layers/dense"
"github.com/openfluke/welvet/nativemlx"
"github.com/openfluke/welvet/quant"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
fmt.Println("SKIP: Apple MLX needs Apple silicon macOS and cgo")
return nil
}
fmt.Println("version", nativemlx.Version())
fmt.Println("available", nativemlx.Available())
fmt.Println("prefer", core.PreferMLX())
if !nativemlx.Available() {
fmt.Println("SKIP: need libwelvet_mlx.dylib + Metal GPU (../welvet.mlx/scripts/macos_install_go.sh)")
return nil
}
l, err := dense.NewConfigured(2, 2, core.ActivationLinear, core.DTypeFloat32, quant.FormatNone, []float32{1, 0, 0, 1})
if err != nil {
return err
}
if err := l.SyncToMLX(); err != nil {
return err
}
l.Exec.Backend = core.BackendMLX
x := &core.Tensor[float32]{Shape: []int{1, 2}, Data: []float32{0.5, -0.25}}
_, post, err := dense.Forward(l, x)
if err != nil {
return err
}
fmt.Printf("backend=mlx synced=%v\ninput=%v output=%v shape=%v\n", l.MLXSynced(), x.Data, post.Data, post.Shape)
return nil
}
Output
version 1.0.0
available true
prefer mlx
backend=mlx synced=true
input=[0.5 -0.25] output=[0.5 -0.25] shape=[1 2]
Validate (harness)
cd welvet/examples/110-apple-mlx && source ../env.sh && go run .