Book contents +

XVI · Apple MLX acceleration (welvet.mlx) / CHAPTER 22

Apple MLX: SyncToMLX and forward

Samuel Watson · Welvet feature bookOriginal source ↗

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

Run: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

exit 0 · last run via go run .

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 .