Files
Jesse Gross 2e036e7cdf mlx, mlxrunner: move the MLX engine out of x/
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.
2026-09-16 14:06:08 -07:00

109 lines
3.6 KiB
Go

package create
import (
"regexp"
"strconv"
"strings"
)
// defaultQuantPolicy is the quantize policy for any architecture without a
// registered override: the shared GetTensorQuantization decision with no
// architecture-specific adjustments.
type defaultQuantPolicy struct{}
func (defaultQuantPolicy) quantizationType(name string, shape []int32, quantize string) string {
return GetTensorQuantization(name, shape, quantize)
}
// layerIndexRe extracts the layer index from tensor names like
// "model.language_model.layers.5.self_attn.v_proj.weight" or
// "model.language_model.layers.5.moe.experts.42.down_proj.weight"
var layerIndexRe = regexp.MustCompile(`\.layers\.(\d+)\.`)
// layerIndex returns the transformer layer index encoded in name, or -1.
func layerIndex(name string) int {
m := layerIndexRe.FindStringSubmatch(name)
if m == nil {
return -1
}
idx, err := strconv.Atoi(m[1])
if err != nil {
return -1
}
return idx
}
// useMoreBits returns true for layers where quantization-sensitive tensors
// should use higher precision: the first and last 1/8 of layers (which handle
// input grounding and final output refinement), plus every 3rd layer in between
// to limit error accumulation through the residual stream.
func useMoreBits(layerIdx, numLayers int) bool {
return useMoreBitsWithMiddleEnd(layerIdx, numLayers, 7*numLayers/8)
}
// useMoreBitsWithMiddleEnd applies the standard early/late promotion and
// limits the every-third-layer cadence to layers before middleEnd.
func useMoreBitsWithMiddleEnd(layerIdx, numLayers, middleEnd int) bool {
if layerIdx < 0 || numLayers <= 0 {
return false
}
first := numLayers / 8
last := 7 * numLayers / 8
return layerIdx < first ||
layerIdx >= last ||
(layerIdx >= first && layerIdx < middleEnd && (layerIdx-first)%3 == 2)
}
// eightBit returns the 8-bit quantization type in base's family: int8 for the
// affine family, mxfp8 for the fp4 family.
func eightBit(base string) string {
if base == "int4" || base == "int8" {
return "int8"
}
return "mxfp8"
}
// promoteEmbedding returns the 8-bit type in base's family when the embedding
// shape fits it, or "" when it does not. Token embeddings often double as the
// lm_head projection, where an 8-bit type keeps quality close to bf16 while
// saving decode bandwidth; the caller decides the fallback when 8-bit does not
// fit (the base type, or source precision).
func promoteEmbedding(shape []int32, base string) string {
if e := eightBit(base); isAligned(shape, e) {
return e
}
return ""
}
// sensitiveType resolves a quantization-sensitive projection (v/k/down): the
// 8-bit type in base's family when promote is set and fits the shape,
// otherwise the base type when it fits, otherwise source precision.
func sensitiveType(promote bool, shape []int32, base string) string {
if promote {
if e := eightBit(base); isAligned(shape, e) {
return e
}
}
if isAligned(shape, base) {
return base
}
return ""
}
// isEmbedTokensWeight returns true for the main token embedding weight.
func isEmbedTokensWeight(name string) bool {
return strings.HasSuffix(name, "embed_tokens.weight") &&
!strings.Contains(name, "per_layer")
}
// isVision reports tensors under a model's vision components: towers,
// encoder-free embedders, and vision-to-text projections alike.
func isVision(name string) bool {
return strings.Contains(name, "vision") || strings.Contains(name, "visual")
}
// isAudioTower reports tensors under a model's audio tower or audio embedding.
func isAudioTower(name string) bool {
return strings.Contains(name, "audio_tower") || strings.Contains(name, "embed_audio")
}