mirror of
https://github.com/ollama/ollama.git
synced 2026-09-21 05:28:00 -05: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.
758 lines
20 KiB
Go
758 lines
20 KiB
Go
package mlx
|
|
|
|
// #include "generated.h"
|
|
import "C"
|
|
|
|
import (
|
|
"reflect"
|
|
"unsafe"
|
|
)
|
|
|
|
// Nvfp4MaxProduct is the product of the maximum E4M3 and E2M1 values. Every
|
|
// globalScale below is in MLX's representation: a checkpoint's multiplier m
|
|
// (ModelOpt's weight_scale_2) is held as m*Nvfp4MaxProduct, so Nvfp4MaxProduct
|
|
// itself is the identity. Wrappers that hand the scale to MLX pass it through;
|
|
// wrappers that apply it themselves divide it back out.
|
|
const Nvfp4MaxProduct = 448 * 6
|
|
|
|
// scaleAndCast applies a global scale to an output and casts back, fusing the
|
|
// divide, multiply and cast into one kernel. Eagerly this is several: MLX
|
|
// promotes binary operands with an astype, so a bf16 input times a float32
|
|
// scale round-trips through a full-size float32 intermediate.
|
|
var scaleAndCast = Compile2(
|
|
"GlobalScaleOutput",
|
|
func(out, scale *Array) *Array {
|
|
return Mul(out, DivScalar(scale, Nvfp4MaxProduct)).AsType(out.DType())
|
|
},
|
|
Shapeless(),
|
|
)
|
|
|
|
// Quantization operations
|
|
|
|
func Quantize(w *Array, groupSize, bits int, mode string) (weights, scales, biases *Array) {
|
|
cMode := C.CString(mode)
|
|
defer C.free(unsafe.Pointer(cMode))
|
|
optGroupSize := C.mlx_optional_int{value: C.int(groupSize), has_value: true}
|
|
optBits := C.mlx_optional_int{value: C.int(bits), has_value: true}
|
|
res := mlxCheck(C.mlx_vector_array_new())
|
|
defer freeVectorArray(res)
|
|
var globalScale C.mlx_array
|
|
mlxCheck(C.mlx_quantize(&res, w.ctx, optGroupSize, optBits, cMode, globalScale, DefaultStream().ctx))
|
|
|
|
vecSize := int(mlxCheck(C.mlx_vector_array_size(res)))
|
|
w0 := New("QUANTIZE_W")
|
|
mlxCheck(C.mlx_vector_array_get(&w0.ctx, res, 0))
|
|
w1 := New("QUANTIZE_S")
|
|
mlxCheck(C.mlx_vector_array_get(&w1.ctx, res, 1))
|
|
if vecSize >= 3 {
|
|
w2 := New("QUANTIZE_B")
|
|
mlxCheck(C.mlx_vector_array_get(&w2.ctx, res, 2))
|
|
return w0, w1, w2
|
|
}
|
|
return w0, w1, nil
|
|
}
|
|
|
|
func FromFP8(x *Array, dtype DType) *Array {
|
|
out := New("FROM_FP8")
|
|
mlxCheck(C.mlx_from_fp8(&out.ctx, x.ctx, C.mlx_dtype(dtype), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func ToFP8(x *Array) *Array {
|
|
out := New("TO_FP8")
|
|
mlxCheck(C.mlx_to_fp8(&out.ctx, x.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
// Dequantize applies globalScale itself rather than forwarding it: MLX's own
|
|
// argument accepts only a scalar, and callers pass per-expert banks.
|
|
func Dequantize(w, scales, biases *Array, groupSize, bits int, mode string, globalScale *Array) *Array {
|
|
cMode := C.CString(mode)
|
|
defer C.free(unsafe.Pointer(cMode))
|
|
optGroupSize := C.mlx_optional_int{value: C.int(groupSize), has_value: true}
|
|
optBits := C.mlx_optional_int{value: C.int(bits), has_value: true}
|
|
optDtype := C.mlx_optional_dtype{has_value: false}
|
|
|
|
var b C.mlx_array
|
|
if biases != nil {
|
|
b = biases.ctx
|
|
}
|
|
|
|
out := New("DEQUANTIZE")
|
|
var noGlobalScale C.mlx_array
|
|
mlxCheck(C.mlx_dequantize(&out.ctx, w.ctx, scales.ctx, b, optGroupSize, optBits, cMode, noGlobalScale, optDtype, DefaultStream().ctx))
|
|
if globalScale != nil {
|
|
gs := globalScale
|
|
for gs.NumDims() < out.NumDims() {
|
|
gs = ExpandDims(gs, -1)
|
|
}
|
|
out = scaleAndCast(out, gs)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// QuantizedMatmul applies globalScale to the output; MLX's quantized_matmul
|
|
// has none of its own.
|
|
func QuantizedMatmul(x, w, scales, biases *Array, transpose bool, groupSize, bits int, mode string, globalScale *Array) *Array {
|
|
cMode := C.CString(mode)
|
|
defer C.free(unsafe.Pointer(cMode))
|
|
optGroupSize := C.mlx_optional_int{value: C.int(groupSize), has_value: true}
|
|
optBits := C.mlx_optional_int{value: C.int(bits), has_value: true}
|
|
|
|
var b C.mlx_array
|
|
if biases != nil {
|
|
b = biases.ctx
|
|
}
|
|
|
|
out := New("QUANTIZED_MATMUL")
|
|
mlxCheck(C.mlx_quantized_matmul(&out.ctx, x.ctx, w.ctx, scales.ctx, b, C.bool(transpose), optGroupSize, optBits, cMode, DefaultStream().ctx))
|
|
if globalScale != nil {
|
|
out = scaleAndCast(out, globalScale)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GatherQMM multiplies x against the quantized weight banks selected by
|
|
// lhsIndices and rhsIndices, with one globalScale entry per expert. Metal
|
|
// applies it inside the gather kernel, other backends scale the gathered
|
|
// output rows in the wrapper.
|
|
func GatherQMM(x, w, scales *Array, biases, lhsIndices, rhsIndices *Array, transpose bool, groupSize, bits int, mode string, globalScale *Array, sortedIndices bool) *Array {
|
|
cMode := C.CString(mode)
|
|
defer C.free(unsafe.Pointer(cMode))
|
|
optGroupSize := C.mlx_optional_int{value: C.int(groupSize), has_value: true}
|
|
optBits := C.mlx_optional_int{value: C.int(bits), has_value: true}
|
|
|
|
var b, lhs, rhs, gs C.mlx_array
|
|
if biases != nil {
|
|
b = biases.ctx
|
|
}
|
|
if lhsIndices != nil {
|
|
lhs = lhsIndices.ctx
|
|
}
|
|
if rhsIndices != nil {
|
|
rhs = rhsIndices.ctx
|
|
}
|
|
// The wrapper fallback needs rhs indices to map output rows to experts;
|
|
// without them the native path reports the unsupported combination.
|
|
applyWrapperScale := globalScale != nil && !MetalIsAvailable() && rhsIndices != nil
|
|
if globalScale != nil && !applyWrapperScale {
|
|
gs = globalScale.ctx
|
|
}
|
|
|
|
out := New("GATHER_QMM")
|
|
mlxCheck(C.mlx_gather_qmm(&out.ctx, x.ctx, w.ctx, scales.ctx, b, lhs, rhs, C.bool(transpose), optGroupSize, optBits, cMode, gs, C.bool(sortedIndices), DefaultStream().ctx))
|
|
if applyWrapperScale {
|
|
out = mulGatherQMMGlobalScale(out, globalScale, rhsIndices)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// mulGatherQMMGlobalScale scales gathered output rows by the per-expert
|
|
// global scale.
|
|
func mulGatherQMMGlobalScale(out, globalScale, rhsIndices *Array) *Array {
|
|
// The indices address the weight's flattened batch dimensions, so the bank
|
|
// has to be flat too before taking from it.
|
|
perExpert := Reshape(globalScale, int32(globalScale.Size()))
|
|
scaleRows := Take(perExpert, rhsIndices, 0)
|
|
// out is the broadcast index shape plus the two matmul axes, and rhsIndices
|
|
// may be shorter, so pad left before appending those axes.
|
|
for scaleRows.NumDims() < out.NumDims()-2 {
|
|
scaleRows = ExpandDims(scaleRows, 0)
|
|
}
|
|
scaleRows = ExpandDims(ExpandDims(scaleRows, -1), -1)
|
|
return scaleAndCast(out, scaleRows)
|
|
}
|
|
|
|
// Missing tensor ops
|
|
|
|
func Arange(start, stop, step float64, dtype DType) *Array {
|
|
out := New("ARANGE")
|
|
mlxCheck(C.mlx_arange(&out.ctx, C.double(start), C.double(stop), C.double(step), C.mlx_dtype(dtype), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Tile(a *Array, reps []int32) *Array {
|
|
cReps := make([]C.int, len(reps))
|
|
for i, r := range reps {
|
|
cReps[i] = C.int(r)
|
|
}
|
|
out := New("TILE")
|
|
mlxCheck(C.mlx_tile(&out.ctx, a.ctx, unsafe.SliceData(cReps), C.size_t(len(reps)), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
// BroadcastTo broadcasts an array to the given shape.
|
|
func BroadcastTo(a *Array, shape ...int32) *Array {
|
|
cShape := make([]C.int, len(shape))
|
|
for i, s := range shape {
|
|
cShape[i] = C.int(s)
|
|
}
|
|
out := New("BROADCAST_TO")
|
|
mlxCheck(C.mlx_broadcast_to(&out.ctx, a.ctx, unsafe.SliceData(cShape), C.size_t(len(shape)), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Tri(n, m int32, k int) *Array {
|
|
out := New("TRI")
|
|
mlxCheck(C.mlx_tri(&out.ctx, C.int(n), C.int(m), C.int(k), C.mlx_dtype(DTypeFloat32), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Where(condition, a, b *Array) *Array {
|
|
out := New("WHERE")
|
|
mlxCheck(C.mlx_where(&out.ctx, condition.ctx, a.ctx, b.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Conv1d(x, weight *Array, bias *Array, stride, padding, dilation, groups int32) *Array {
|
|
out := New("CONV1D")
|
|
mlxCheck(C.mlx_conv1d(
|
|
&out.ctx,
|
|
x.ctx,
|
|
weight.ctx,
|
|
C.int(stride),
|
|
C.int(padding),
|
|
C.int(dilation),
|
|
C.int(groups),
|
|
DefaultStream().ctx,
|
|
))
|
|
if bias != nil {
|
|
out = Add(out, bias)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func Contiguous(a *Array, allowColMajor bool) *Array {
|
|
out := New("CONTIGUOUS")
|
|
mlxCheck(C.mlx_contiguous(&out.ctx, a.ctx, C.bool(allowColMajor), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
// Conv2d performs 2D convolution: x [N,H,W,C_in], weight [C_out,kH,kW,C_in].
|
|
// MLX uses NHWC layout.
|
|
func Conv2d(x, weight *Array, strideH, strideW, padH, padW, dilationH, dilationW, groups int32) *Array {
|
|
out := New("CONV2D")
|
|
mlxCheck(C.mlx_conv2d(
|
|
&out.ctx,
|
|
x.ctx,
|
|
weight.ctx,
|
|
C.int(strideH), C.int(strideW),
|
|
C.int(padH), C.int(padW),
|
|
C.int(dilationH), C.int(dilationW),
|
|
C.int(groups),
|
|
DefaultStream().ctx,
|
|
))
|
|
return out
|
|
}
|
|
|
|
// Pad pads array a along the given axes with specified low/high pad sizes.
|
|
// mode should be "constant", "edge", or "reflect".
|
|
func Pad(a *Array, axes []int, lowPad, highPad []int, padValue *Array, mode string) *Array {
|
|
cAxes := make([]C.int, len(axes))
|
|
cLow := make([]C.int, len(lowPad))
|
|
cHigh := make([]C.int, len(highPad))
|
|
for i := range axes {
|
|
cAxes[i] = C.int(axes[i])
|
|
cLow[i] = C.int(lowPad[i])
|
|
cHigh[i] = C.int(highPad[i])
|
|
}
|
|
cMode := C.CString(mode)
|
|
defer C.free(unsafe.Pointer(cMode))
|
|
out := New("PAD")
|
|
mlxCheck(C.mlx_pad(
|
|
&out.ctx,
|
|
a.ctx,
|
|
unsafe.SliceData(cAxes), C.size_t(len(cAxes)),
|
|
unsafe.SliceData(cLow), C.size_t(len(cLow)),
|
|
unsafe.SliceData(cHigh), C.size_t(len(cHigh)),
|
|
padValue.ctx,
|
|
cMode,
|
|
DefaultStream().ctx,
|
|
))
|
|
return out
|
|
}
|
|
|
|
// PadConstant pads with zeros along the given axes.
|
|
func PadConstant(a *Array, axes []int, lowPad, highPad []int) *Array {
|
|
zero := NewScalarArray(float32(0))
|
|
return Pad(a, axes, lowPad, highPad, zero, "constant")
|
|
}
|
|
|
|
func DepthwiseConv1d(x, weight *Array, bias *Array) *Array {
|
|
groups := int32(x.Dim(x.NumDims() - 1))
|
|
return Conv1d(x, weight, bias, 1, 0, 1, groups)
|
|
}
|
|
|
|
// Maximum returns element-wise maximum of two arrays.
|
|
func Maximum(a, b *Array) *Array {
|
|
out := New("MAXIMUM")
|
|
mlxCheck(C.mlx_maximum(&out.ctx, a.ctx, b.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
// Minimum returns element-wise minimum of two arrays.
|
|
func Minimum(a, b *Array) *Array {
|
|
out := New("MINIMUM")
|
|
mlxCheck(C.mlx_minimum(&out.ctx, a.ctx, b.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
// Softplus computes log(1 + exp(x)) using logaddexp for numerical stability.
|
|
func Softplus(a *Array) *Array {
|
|
return Logaddexp(a, Zeros(a.DType(), a.Dims()...))
|
|
}
|
|
|
|
// ReLU computes max(0, x).
|
|
func ReLU(a *Array) *Array {
|
|
return Maximum(a, NewScalarArray(float32(0)))
|
|
}
|
|
|
|
// GLU applies Gated Linear Unit: splits x along last dim into two halves,
|
|
// returns first * sigmoid(second).
|
|
func GLU(a *Array) *Array {
|
|
lastDim := a.NumDims() - 1
|
|
halfSize := a.Dim(lastDim) / 2
|
|
first := SliceStartStop(a,
|
|
make([]int32, lastDim+1), // all zeros for start
|
|
appendDims(a, lastDim, int32(halfSize)),
|
|
)
|
|
second := SliceStartStop(a,
|
|
appendDimsStart(a, lastDim, int32(halfSize)),
|
|
appendDims(a, lastDim, int32(a.Dim(lastDim))),
|
|
)
|
|
return first.Multiply(second.Sigmoid())
|
|
}
|
|
|
|
// helper: builds stop array for SliceStartStop where the target axis = val
|
|
func appendDims(a *Array, targetAxis int, val int32) []int32 {
|
|
n := a.NumDims()
|
|
out := make([]int32, n)
|
|
for i := range n {
|
|
if i == targetAxis {
|
|
out[i] = val
|
|
} else {
|
|
out[i] = int32(a.Dim(i))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// helper: builds start array for SliceStartStop where the target axis = val
|
|
func appendDimsStart(a *Array, targetAxis int, val int32) []int32 {
|
|
n := a.NumDims()
|
|
out := make([]int32, n)
|
|
for i := range n {
|
|
if i == targetAxis {
|
|
out[i] = val
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Clamp clamps array values to [min, max].
|
|
func Clamp(a *Array, minVal, maxVal float32) *Array {
|
|
return Minimum(Maximum(a, NewScalarArray(minVal)), NewScalarArray(maxVal))
|
|
}
|
|
|
|
// Convenience wrappers (function-style for the model code)
|
|
|
|
func Stack(arrays []*Array, axis int) *Array {
|
|
vectorData := make([]C.mlx_array, len(arrays))
|
|
for i := range arrays {
|
|
vectorData[i] = arrays[i].ctx
|
|
}
|
|
vector := mlxCheck(C.mlx_vector_array_new_data(unsafe.SliceData(vectorData), C.size_t(len(vectorData))))
|
|
defer freeVectorArray(vector)
|
|
|
|
out := New("STACK")
|
|
mlxCheck(C.mlx_stack_axis(&out.ctx, vector, C.int(axis), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Neg(a *Array) *Array {
|
|
return a.Negative()
|
|
}
|
|
|
|
func Sum(a *Array, axis int, keepDims bool) *Array {
|
|
return a.SumAxis(axis, keepDims)
|
|
}
|
|
|
|
func Argsort(a *Array, axis int) *Array {
|
|
return a.ArgsortAxis(axis)
|
|
}
|
|
|
|
func Take(a *Array, indices *Array, axis int) *Array {
|
|
return a.TakeAxis(indices, axis)
|
|
}
|
|
|
|
func RSqrt(a *Array) *Array {
|
|
out := New("RSQRT")
|
|
mlxCheck(C.mlx_rsqrt(&out.ctx, a.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Mean(a *Array, axis int, keepDims bool) *Array {
|
|
out := New("MEAN_AXIS")
|
|
mlxCheck(C.mlx_mean_axis(&out.ctx, a.ctx, C.int(axis), C.bool(keepDims), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Argpartition(a *Array, kth int, axis int) *Array {
|
|
return a.ArgpartitionAxis(kth, axis)
|
|
}
|
|
|
|
func TakeAlongAxis(a, indices *Array, axis int) *Array {
|
|
return a.TakeAlongAxis(indices, axis)
|
|
}
|
|
|
|
// Function-style wrappers for model code.
|
|
|
|
func Add(a, b *Array) *Array {
|
|
return a.Add(b)
|
|
}
|
|
|
|
func Sub(a, b *Array) *Array {
|
|
return a.Subtract(b)
|
|
}
|
|
|
|
func Mul(a, b *Array) *Array {
|
|
return a.Multiply(b)
|
|
}
|
|
|
|
func Div(a, b *Array) *Array {
|
|
return a.Divide(b)
|
|
}
|
|
|
|
func Matmul(a, b *Array) *Array {
|
|
return a.Matmul(b)
|
|
}
|
|
|
|
func Reshape(a *Array, shape ...int32) *Array {
|
|
axes := make([]int, len(shape))
|
|
for i, s := range shape {
|
|
axes[i] = int(s)
|
|
}
|
|
return a.Reshape(axes...)
|
|
}
|
|
|
|
func Transpose(a *Array, axes ...int) *Array {
|
|
return a.Transpose(axes...)
|
|
}
|
|
|
|
func ExpandDims(a *Array, axis int) *Array {
|
|
return a.ExpandDims(axis)
|
|
}
|
|
|
|
func Squeeze(a *Array, axis int) *Array {
|
|
return a.Squeeze(axis)
|
|
}
|
|
|
|
func Flatten(a *Array) *Array {
|
|
return a.Flatten(0, -1)
|
|
}
|
|
|
|
func Concatenate(arrays []*Array, axis int) *Array {
|
|
if len(arrays) == 0 {
|
|
return nil
|
|
}
|
|
if len(arrays) == 1 {
|
|
return arrays[0].Clone()
|
|
}
|
|
return arrays[0].Concatenate(axis, arrays[1:]...)
|
|
}
|
|
|
|
func SliceStartStop(a *Array, start, stop []int32) *Array {
|
|
n := len(start)
|
|
cStart := make([]C.int, n)
|
|
cStop := make([]C.int, n)
|
|
cStrides := make([]C.int, n)
|
|
for i := range n {
|
|
cStart[i] = C.int(start[i])
|
|
cStop[i] = C.int(stop[i])
|
|
cStrides[i] = 1
|
|
}
|
|
out := New("SLICE")
|
|
mlxCheck(C.mlx_slice(&out.ctx, a.ctx, unsafe.SliceData(cStart), C.size_t(n), unsafe.SliceData(cStop), C.size_t(n), unsafe.SliceData(cStrides), C.size_t(n), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func GatherMM(a, b *Array, lhsIndices, rhsIndices *Array, sortedIndices bool) *Array {
|
|
if lhsIndices == nil {
|
|
lhsIndices = New("")
|
|
}
|
|
if rhsIndices == nil {
|
|
rhsIndices = New("")
|
|
}
|
|
return a.GatherMM(b, lhsIndices, rhsIndices, sortedIndices)
|
|
}
|
|
|
|
// RoPEWithBase applies rotary position embeddings to x. offsets is an
|
|
// int32 array of shape [B] giving each batch row's starting position;
|
|
// the kernel applies positions offsets[b] + 0..T-1 per row.
|
|
func RoPEWithBase(x *Array, dims int, traditional bool, base, scale float32, offsets *Array) *Array {
|
|
return RoPEWithFreqs(x, dims, traditional, base, scale, offsets, nil)
|
|
}
|
|
|
|
// RoPEWithFreqs applies RoPE with optional custom frequencies.
|
|
// When freqs is non-nil, it is used instead of computing from base.
|
|
// Note: MLX takes reciprocal(freqs) internally to get inv_freq, so pass
|
|
// the actual frequencies (base^(2i/dim)), not the inverse frequencies.
|
|
func RoPEWithFreqs(x *Array, dims int, traditional bool, base, scale float32, offsets *Array, freqs *Array) *Array {
|
|
var freqsCtx C.mlx_array
|
|
var optBase C.mlx_optional_float
|
|
if freqs != nil {
|
|
freqsCtx = freqs.ctx
|
|
optBase = C.mlx_optional_float{has_value: C.bool(false)}
|
|
} else {
|
|
empty := New("")
|
|
freqsCtx = empty.ctx
|
|
optBase = C.mlx_optional_float{
|
|
value: C.float(base),
|
|
has_value: C.bool(func() bool { return base != 0 }()),
|
|
}
|
|
}
|
|
out := New("FAST_ROPE")
|
|
mlxCheck(C.mlx_fast_rope_dynamic(
|
|
&out.ctx,
|
|
x.ctx,
|
|
C.int(dims),
|
|
C.bool(traditional),
|
|
optBase,
|
|
C.float(scale),
|
|
offsets.ctx,
|
|
freqsCtx,
|
|
DefaultStream().ctx,
|
|
))
|
|
return out
|
|
}
|
|
|
|
func Sigmoid(a *Array) *Array {
|
|
return a.Sigmoid()
|
|
}
|
|
|
|
func Erf(a *Array) *Array {
|
|
out := New("ERF")
|
|
mlxCheck(C.mlx_erf(&out.ctx, a.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Exp(a *Array) *Array {
|
|
out := New("EXP")
|
|
mlxCheck(C.mlx_exp(&out.ctx, a.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Log(a *Array) *Array {
|
|
out := New("LOG")
|
|
mlxCheck(C.mlx_log(&out.ctx, a.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Sin(a *Array) *Array {
|
|
out := New("SIN")
|
|
mlxCheck(C.mlx_sin(&out.ctx, a.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Cos(a *Array) *Array {
|
|
out := New("COS")
|
|
mlxCheck(C.mlx_cos(&out.ctx, a.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func erf(a *Array) *Array {
|
|
out := New("ERF")
|
|
mlxCheck(C.mlx_erf(&out.ctx, a.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Clip(a, aMin, aMax *Array) *Array {
|
|
out := New("CLIP")
|
|
mlxCheck(C.mlx_clip(&out.ctx, a.ctx, aMin.ctx, aMax.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func Logaddexp(a, b *Array) *Array {
|
|
out := New("LOGADDEXP")
|
|
mlxCheck(C.mlx_logaddexp(&out.ctx, a.ctx, b.ctx, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func SoftmaxAxis(a *Array, axis int, precise bool) *Array {
|
|
out := New("SOFTMAX_AXIS")
|
|
mlxCheck(C.mlx_softmax_axis(&out.ctx, a.ctx, C.int(axis), C.bool(precise), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func LayerNormFn(x, weight, bias *Array, eps float32) *Array {
|
|
out := New("FAST_LAYERNORM")
|
|
var w, b C.mlx_array
|
|
if weight != nil {
|
|
w = weight.ctx
|
|
}
|
|
if bias != nil {
|
|
b = bias.ctx
|
|
}
|
|
mlxCheck(C.mlx_fast_layer_norm(&out.ctx, x.ctx, w, b, C.float(eps), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func RMSNormFn(x, weight *Array, eps float32) *Array {
|
|
out := New("FAST_RMSNORM")
|
|
var w C.mlx_array
|
|
if weight != nil {
|
|
w = weight.ctx
|
|
}
|
|
mlxCheck(C.mlx_fast_rms_norm(&out.ctx, x.ctx, w, C.float(eps), DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func AddMM(c, a, b *Array, alpha, beta float32) *Array {
|
|
return c.Addmm(a, b, alpha, beta)
|
|
}
|
|
|
|
// Scalar helpers
|
|
|
|
// scalarWithDtype creates a scalar array matching the dtype of a.
|
|
// Matching dtype is important for graph fusion and avoiding implicit casts.
|
|
func scalarWithDtype(s float32, a *Array) C.mlx_array {
|
|
dtype := a.DType()
|
|
f32 := mlxCheck(C.mlx_array_new_float(C.float(s)))
|
|
if dtype == DTypeFloat32 {
|
|
return f32
|
|
}
|
|
defer freeArray(f32)
|
|
casted := mlxCheck(C.mlx_array_new())
|
|
mlxCheck(C.mlx_astype(&casted, f32, C.mlx_dtype(dtype), DefaultStream().ctx))
|
|
return casted
|
|
}
|
|
|
|
func AddScalar(a *Array, s float32) *Array {
|
|
scalar := scalarWithDtype(s, a)
|
|
defer freeArray(scalar)
|
|
out := New("ADD_SCALAR")
|
|
mlxCheck(C.mlx_add(&out.ctx, a.ctx, scalar, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func MulScalar(a *Array, s float32) *Array {
|
|
scalar := scalarWithDtype(s, a)
|
|
defer freeArray(scalar)
|
|
out := New("MUL_SCALAR")
|
|
mlxCheck(C.mlx_multiply(&out.ctx, a.ctx, scalar, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func DivScalar(a *Array, s float32) *Array {
|
|
scalar := scalarWithDtype(s, a)
|
|
defer freeArray(scalar)
|
|
out := New("DIV_SCALAR")
|
|
mlxCheck(C.mlx_divide(&out.ctx, a.ctx, scalar, DefaultStream().ctx))
|
|
return out
|
|
}
|
|
|
|
func FloorDivideScalar(a *Array, s int32) *Array {
|
|
scalar := FromValue(int(s))
|
|
return a.FloorDivide(scalar)
|
|
}
|
|
|
|
// Array constructors
|
|
|
|
func NewArrayInt32(data []int32, shape []int32) *Array {
|
|
cShape := make([]C.int, len(shape))
|
|
for i, s := range shape {
|
|
cShape[i] = C.int(s)
|
|
}
|
|
out := New("NEW_ARRAY_INT32")
|
|
out.ctx = mlxCheck(C.mlx_array_new_data(unsafe.Pointer(&data[0]), unsafe.SliceData(cShape), C.int(len(shape)), C.mlx_dtype(DTypeInt32)))
|
|
return out
|
|
}
|
|
|
|
func NewScalarArray(value float32) *Array {
|
|
out := New("SCALAR")
|
|
out.ctx = mlxCheck(C.mlx_array_new_float32(C.float(value)))
|
|
return out
|
|
}
|
|
|
|
func ZerosF32(shape []int32) *Array {
|
|
return Zeros(DTypeFloat32, func() []int {
|
|
ints := make([]int, len(shape))
|
|
for i, s := range shape {
|
|
ints[i] = int(s)
|
|
}
|
|
return ints
|
|
}()...)
|
|
}
|
|
|
|
// Utility
|
|
|
|
func Collect(v any) []*Array {
|
|
var arrays []*Array
|
|
seen := make(map[uintptr]bool)
|
|
collect(reflect.ValueOf(v), &arrays, seen)
|
|
return arrays
|
|
}
|
|
|
|
func collect(v reflect.Value, arrays *[]*Array, seen map[uintptr]bool) {
|
|
if !v.IsValid() {
|
|
return
|
|
}
|
|
|
|
if v.Kind() == reflect.Pointer {
|
|
if v.IsNil() {
|
|
return
|
|
}
|
|
ptr := v.Pointer()
|
|
if seen[ptr] {
|
|
return
|
|
}
|
|
seen[ptr] = true
|
|
|
|
if arr, ok := v.Interface().(*Array); ok {
|
|
if arr != nil {
|
|
*arrays = append(*arrays, arr)
|
|
}
|
|
return
|
|
}
|
|
collect(v.Elem(), arrays, seen)
|
|
return
|
|
}
|
|
|
|
switch v.Kind() {
|
|
case reflect.Struct:
|
|
// Check if this struct IS an Array (not a pointer to one)
|
|
if arr, ok := v.Addr().Interface().(*Array); ok {
|
|
if arr != nil {
|
|
*arrays = append(*arrays, arr)
|
|
}
|
|
return
|
|
}
|
|
for i := range v.NumField() {
|
|
field := v.Field(i)
|
|
if field.CanInterface() {
|
|
collect(field, arrays, seen)
|
|
}
|
|
}
|
|
case reflect.Slice:
|
|
for i := range v.Len() {
|
|
collect(v.Index(i), arrays, seen)
|
|
}
|
|
case reflect.Map:
|
|
for _, key := range v.MapKeys() {
|
|
collect(v.MapIndex(key), arrays, seen)
|
|
}
|
|
case reflect.Interface:
|
|
if !v.IsNil() {
|
|
collect(v.Elem(), arrays, seen)
|
|
}
|
|
}
|
|
}
|
|
|
|
func EnableCompile() {
|
|
mlxCheck(C.mlx_enable_compile())
|
|
}
|
|
|
|
func DisableCompile() {
|
|
mlxCheck(C.mlx_disable_compile())
|
|
}
|