Skip to content

MLX Apple Silicon

Qubitium edited this page Nov 4, 2026 · 9 revisions

MLX (Apple Silicon)

Documentation home · Quickstart · Lifecycle

On this page

Overview

The contracts below describe source revision 08efb7c7; installed MLX-LM behavior and supported architectures can also vary by dependency version.

GPT-QModel's MLX support has three separate entry points:

Workflow Entry point Result
Quantize a dense MLX-LM model without loading a Torch model GPTQModel.quantize_mlx(...) Saved MLX-LM checkpoint; supports method="gptq" or "awq"
Load an existing GPT-QModel checkpoint for MLX inference GPTQModel.load(..., backend="mlx") Converts supported packed layers into an MLX model and uses MLX-LM generation
Use individual native quantization primitives Functions in gptqmodel/quantization/mlx_*.py Weight-level pseudo-quantized tensors, checkpoint payloads, optimization results, or processor bridges, depending on the function

The third row is not an extended method list for the first row. In particular, quantize_mlx(method="exl3"), method="rtn", method="fp8", method="qqq", and method="paroquant" are not accepted by the high-level native MLX-LM factory in this source tree. The existence of a native weight primitive does not imply a complete model calibration/export interface for that method.

Workflow at a glance

MLX artifacts versus runtimes. Native model quantization and packed-checkpoint conversion are separate routes.

Reading the diagram: Alternative routes. Failed transfer may requantize ordinary GPTQ/AWQ or reject special formats. A custom converted runtime is not automatically a stock MLX-LM reloadable export.

Text version of the workflow
  • Native MLX-LM quantization: Load in MLX-LM → Calibration tokens → GPTQ or supported AWQ → Save MLX-LM artifact.
  • Packed checkpoint transfer: Saved quantized model → Validate source state → Transcode packed state → Construct MLX runtime.

Open full-size SVG

Requirements and installation

The mlx optional dependency group in pyproject.toml declares:

Dependency Requirement
MLX mlx>=0.32.4
MLX-LM, Python < 3.11 mlx_lm>=0.31.3
MLX-LM, Python >= 3.11 mlx_lm>=0.32.2
pip install 'gptqmodel[mlx]' --no-build-isolation

The native compute kernels use mx.fast.metal_kernel and target Apple Silicon/Metal. The EXL3 and ParoQuant processor availability probes explicitly require sys.platform == "darwin", platform.machine() == "arm64", and an importable mlx.core. Installing a package with an MLX name on an unrelated host does not satisfy those gates. The utility conversion module separately sets MLX_AVAILABLE=False if its MLX/MLX-LM runtime imports fail.

“Native MLX” does not mean every operation runs on the GPU: MLX's Cholesky and triangular inverse operations used here execute on the MLX CPU stream. They do not transfer the model into a Torch quantizer, but they do contribute to calibration/quantization time and memory use.

Concepts: method, checkpoint, runtime, and tensor layout

Concept Meaning in this backend
Native MLX-LM affine storage Packed uint32 codes, scales, and additive biases. Reconstruction is code * scale + bias; this is not a GPTQ packed-zero tensor.
GPTQ/AWQ checkpoint conversion Reads method-specific packed words, zero points, scale dtype, activation-order metadata, and layout. A same-bit conversion need not mean dequantize/requantize.
Pseudo-quantization Returns floating values reconstructed from discrete codes. Those values are useful to calibration and error accounting but are not by themselves a packed inference checkpoint.
EXL3 weight orientation Quantization pipeline uses [input_features, output_features], unlike most weight primitives' [output_features, input_features].
Native/custom runtime Some converted layers require project MLX modules that MLX-LM's ordinary QuantizedLinear loader cannot reconstruct. Runtime construction and portable checkpoint serialization are different promises.
FP8 and GGUF Quantized byte payloads have their own scale and block conventions; they are not integer-affine aliases.

See Packing Formats and Compatibility before interchanging files between GPT-QModel, Transformers, MLX-LM, and GGUF consumers.

Quantize a model directly in MLX

The public wrapper in gptqmodel/models/auto.py is:

@staticmethod
def quantize_mlx(model_id_or_path: str, output_path: str, method: str = "gptq", **kwargs):
    ...

It delegates to gptqmodel.quantization.mlx_native.quantize_mlx:

def quantize_mlx(
    model_id_or_path: str,
    output_path: str,
    method: str = "gptq",
    bits: int = 4,
    group_size: int = 64,
    num_samples: int = 128,
    sequence_length: int = 512,
    trust_remote_code: bool = False,
    calibration_data=None,
    seed: int = 123,
    **kwargs,
):
    ...
from gptqmodel import GPTQModel

output = GPTQModel.quantize_mlx(
    "organization/dense-model",
    "./model-mlx-gptq",
    method="gptq",
    bits=4,
    group_size=64,
    num_samples=128,
    sequence_length=512,
    batch_size=8,
    damp_percent=0.01,
)
Option Default and behavior
method "gptq"; lowercased and restricted to gptq/awq
bits, group_size 4, 64; native GPTQ supports bits 2, 4, 8 and groups 32, 64, 128
num_samples, sequence_length 128, 512; passed to MLX-LM load_data when custom data is absent
calibration_data None; otherwise supply a nonempty rank-2 token array, not text strings or a Python prompt list
trust_remote_code False; when true, the installed MLX-LM load must explicitly expose this parameter or the wrapper raises ValueError
seed 123; seeds mx.random
**kwargs Passed to gptq_quantize_model_mlx or MLX-LM awq_quantize; they are not a QuantizeConfig object or arbitrary loading options
Return The output_path after mlx_lm.utils.save completes

The loader uses lazy=True, return_config=True. For a Hub ID, saving resolves the cached config.json with hf_hub_download(..., local_files_only=True) to locate the source artifact directory. The source must therefore have been materialized in the local Hub cache by loading; this final lookup is not an independent network download.

Native GPTQ model algorithm

def gptq_quantize_model_mlx(
    model,
    data,
    bits: int = 4,
    group_size: int = 64,
    fallback_bits: int = 6,
    fallback_group_size: int = 64,
    batch_size: int = 8,
    damp_percent: float = 0.01,
):
    ...

def inverse_hessian_mlx(hessian, damp_percent: float = 0.01):
    ...

def gptq_quantize_weight_mlx(
    weight, inverse_hessian, bits: int = 4, group_size: int = 64
):
    ...

The model path wraps exact nn.Linear and MLX-LM SwitchLinear leaf types with Hessian catchers. Only widths admitting a supported group size are caught. _effective_group_size tries 128, 64, 32, taking the largest size no greater than requested that divides the width. Every caught layer must receive calibration activations; otherwise quantization raises rather than silently using an empty Hessian.

Hessian accumulation flattens tokens and accumulates FP32 covariance. _hessian_partial_mlx and _accurate_matmul_mlx split values into BF16-high and FP32-residual components to correct reduced-precision GPU matmul input arithmetic. Damping uses the maximum of damp_percent * mean(diagonal), max(diagonal) * 1e-6, and 1e-12; damp_percent must be strictly between zero and one. The CPU-stream Cholesky/inverse/upper-Cholesky sequence returns the factor needed by the group update.

The weight primitive accepts rank-2 or rank-3 weights, flattens output prefixes, and performs a fused Metal dispatch per group. It returns (packed_weight, scales, biases) in native MLX affine layout. Error feedback updates the remaining columns. Quantized layer scale dtype is restored to the original layer dtype using set_dtype.

Remaining leaf modules with to_quantized use the fallback bits/group size, defaulting to 6-bit/64. Widths with no eligible group are marked False in the returned quantization config; per-layer group reductions are explicitly recorded. Thus a “4-bit” native GPTQ run may deliberately include 6-bit fallback modules and unquantized modules. This path is not equivalent to selecting every Torch GPTQ option such as activation ordering or static groups.

Native AWQ model algorithm

GPTQModel.quantize_mlx(
    "organization/dense-model",
    "./model-mlx-awq",
    method="awq",
    bits=4,
    group_size=64,
)

AWQ delegates to MLX-LM: it looks up AWQ_MODEL_CONFIGS[config["model_type"]], rejects unsupported model types, distributes calibration with dist_split(..., mx.distributed.init()), calls awq_quantize, and runs update_config. The supported model table and AWQ keyword defaults belong to the installed MLX-LM version, not GPT-QModel's Torch AWQ processor. See AWQ.

Native weight-level quantization APIs

These APIs operate on MLX arrays and are opt-in building blocks. Unless noted, a weight uses [out_features, in_features]. Caller-owned calibration, activation ordering, smoothing, packer selection, and model serialization are not implicitly supplied.

RTN, GPTAQ, FOEM, and QQQ

def quantize_rtn_weight_mlx(weight, *, bits=4, group_size=128, sym=True):
    ...

def gptaq_correction_mlx(delta_covariance, inverse_hessian, *, alpha=0.25):
    ...

def gptaq_quantize_weight_mlx(
    weight, inverse_hessian, correction, *, bits=4, group_size=128, sym=True,
):
    ...

def foem_quantize_weight_mlx(
    weight,
    inverse_hessian,
    *,
    bits: int = 4,
    group_size: int = 128,
    beta: float = 0.2,
    sym: bool = True,
    return_residuals: bool = False,
):
    ...

def qqq_quantize_weight_mlx(weight, inverse_hessian, *, group_size=128):
    ...

def qqq_quantize_weight_mlx_with_loss(
    weight, inverse_hessian, *, group_size=128
):
    ...
Module/API Supported contract Outputs and important limits
mlx_rtn.py / quantize_rtn_weight_mlx Nonempty rank-2; bits 2..8; positive group size or -1 for one group per row; partial final group supported (dequantized_weight, scales, zeros, group_indices); scales/zeros FP32, indices int32. Input FP16/BF16/FP32 dtype is preserved in pseudo-weight output. No smoothing.
mlx_gptaq.py / correction Square FP32 delta covariance and matching upper inverse-Hessian factor; finite alpha, default 0.25 Strict-upper activation correction via accurate FP32 matmuls.
mlx_gptaq.py / quantization FP16/BF16/FP32 rank-2 weight; FP32 matching Hessian/correction; bits 2..8, group 16,32,64,128 dividing width; bool sym FP32 (pseudo_weight, scales, zeros); dynamic groups, MSE search and activation order disabled, blocksize=group_size.
mlx_foem.py Same bit/group/weight/factor contracts; finite inputs, positive factor diagonal; beta in [0,1] Alpha-zero FOEM only. FP32 (pseudo_weight, scales, zeros); optional fourth per-weight residual tensor for loss accounting. No MSE search or activation-order permutation.
mlx_qqq.py Four-bit QQQ; group_size=-1 or 128; group 128 must divide width; FP32 matching factor (pseudo_weight, scales, zeros, extra_scales); with_loss adds aggregate sum(error**2)/2. Inputs already preordered; not yet a QQQ inference layout.

QQQ per-channel (-1) uses fixed row scales and signed codes [-7,7], returning no extra scale. Group-128 uses dynamic unsigned codes [0,15] with zero point 8 and returns original-weight int8 row scales (max_abs/127, with zero-row handling). Activation ordering and MSE search are caller responsibilities. See QQQ.

FP8 byte encoding

def quantize_fp8_weight_mlx(
    weight,
    *,
    format="float8_e4m3fn",
    weight_scale_method="row",
    weight_block_size=None,
):
    ...

mlx_fp8.py accepts finite nonempty rank-2 FP16/BF16/FP32 weights and returns (uint8_codes, inverse_scales). Supported format strings are float8_e4m3fn, float8_e5m2, float8_e4m3fnuz, and float8_e5m2fnuz. Scale modes are tensor, row, and block, normalized by the common config helpers. A block layout must divide both weight dimensions.

The stored scale is the multiplicative encoding/inverse-dequantization scale: values are encoded after multiplication by it. Decoding divides codebook values by it. This is not interchangeable with an affine integer group scale. The Metal encoder explicitly handles signed zero according to the FP8 format, saturation, ties-to-even rounding, float32 subnormal inputs, and scale overflow. See FP8 and NVFP4.

Bitsandbytes payloads

def quantize_4bit_weight_mlx(weight, *, quant_type="nf4", block_size=64, compress_statistics=False):
    ...

def quantize_int8_weight_mlx(weight):
    ...

mlx_bitsandbytes.py accepts finite rank-2 FP16/BF16/FP32 weights. Four-bit types are nf4 and fp4; accepted block sizes are 32,64,128,256,512,1024,2048,4096. Two flattened codes are stored per byte, first code in the high nibble; an odd final value pads with the code for zero. This order differs from several integer-affine and GGUF nibble layouts.

Uncompressed output is (packed_uint8, FP32_block_absmax). With compress_statistics=True, the second value is a dict containing absmax (8-bit nested codes), nested_absmax, offset, and nested_code. Nested scale blocks use 256 values. The implementation includes a special all-subnormal FP32 packing/nested-scale path to avoid Metal flush-to-zero changing checkpoint bytes.

Int8 returns (int8_row_codes, FP32_row_absmax) using row normalization to 127 and ties-to-even. These are quantized weight payloads; the function does not itself install a full bitsandbytes module or model export. See BitsAndBytes.

GGUF block-byte packing

def gguf_quantize_weight_mlx(weight, qtype: str):
    ...

mlx_gguf.py uppercases the qtype and returns a uint8 matrix [rows, input_width / block_size * bytes_per_block]. Inputs must be rank-2 FP16/BF16/FP32 with nonzero rows and width divisible by the selected block size. This packs weight bytes, not a complete .gguf file with model metadata/tokenizer/container headers.

Accepted qtype(s) Values per block Bytes per block
Q1_0, Q1_0_G128 128 18
Q2_0 64 18
Q4_0 32 18
Q4_K, Q4_K_S, Q4_K_M 256 144
Q5_K, Q5_K_S, Q5_K_M 256 176
Q6_K 256 210
TQ1_0 256 54
TQ2_0 256 66
MXFP4 32 17
NVFP4 64 36
Q8_0 32 34

At this weight-level entry point, _S/_M K aliases select the same block kernel; they do not independently implement a model-wide mixed-precision policy. Q1_0 stores mean-absolute FP16 scale and sign bits. Q2_0 uses ternary values inside a two-bit code layout. TQ1_0 packs base-3 symbols, while TQ2_0 stores two-bit ternary symbols. Ternary kernels choose a parallel launch at 65,536 blocks. FP4 scale encoding differs between MXFP4 and NVFP4. Do not substitute one format's bytes for another merely because nominal bits are similar. See GGUF.

Native ParoQuant path

The path is split into rotation, pseudo-quantization, checkpoint packing, and module-scope optimization. These functions are not aliases for the native MLX-LM AWQ algorithm.

def paroquant_rotate_mlx(
    weight,
    pairs,
    theta,
    *,
    group_size: int = 128,
    channel_scales=None,
    inverse: bool = False,
):
    ...

def paroquant_quantize_weight_mlx(
    weight,
    scales,
    *,
    bits: int = 4,
    group_size: int = 128,
    sym: bool = True,
    zero_point_float=None,
):
    ...

def paroquant_pack_weight_mlx(weight, scales, *, group_size: int):
    ...
Module Contract
mlx_paroquant_rotation.py Weight rank >=2, FP16/BF16/FP32; grouped Givens rotations; group 16,32,64,128 dividing input width. Integer pairs is [rotations,input_features], each group a complete permutation of local channel indices; theta is [rotations,input_features/2]. Optional positive finite channel scales multiply before forward rotation and divide after inverse rotation. Output always FP32.
mlx_paroquant_quant.py Rank-2 weight; bits 2..8; positive even group dividing width, or -1 for full row. FP32 scales/optional asymmetric zero parameters shaped [out,groups] or [out*groups,1]. Symmetric mode forbids zero parameters; asymmetric requires them. Scales clamp to [1e-5,1e5]; ties-to-even; output preserves input weight dtype.
mlx_paroquant.py Packs symmetric four-bit exported transformed weights into ParoQuant's AWQ-compatible qweight/qzeros/scales. Output width divisible by 32. Groups 16,32,64,128 or full input width, even and dividing width. Weight/scales share FP16/BF16/FP32 dtype. FP32 scales are stored as FP16, matching the Torch AWQ pack contract. Finite positive stored scales and codes in [0,15] are required.
mlx_paroquant_optimize.py MLX-autodiff module reconstruction with rotation stage, then weight/quantizer fine-tuning, smooth-L1 loss, STE round/clamp, validation-best snapshots and cosine learning-rate decay to 1/20 of base LR. Supports adamw, adam, sgd; snapshot dtypes fp16,bf16,fp32.

The optimizer has no defaults for its optimization controls. bias is a required positional argument but may be None:

def optimize_paroquant_linear_mlx(
    weight,
    bias,
    inputs,
    pairs,
    theta_mask,
    *,
    bits: int,
    group_size: int,
    symmetric: bool,
    train_rows: int,
    val_rows: int,
    batch_size: int,
    rotation_epochs: int,
    finetune_epochs: int,
    rotation_lr: float,
    weight_lr: float,
    quantizer_lr: float,
    optimizer_name: str,
    optimizer_weight_decay: float,
    optimizer_betas,
    optimizer_eps: float,
    optimizer_amsgrad: bool,
    sgd_momentum: float,
    sgd_dampening: float,
    sgd_nesterov: bool,
    best_state_dtype: str,
    scale_clamp_min: float,
    scale_clamp_max: float,
):
    ...

The optimizer initializes zero theta and unit channel scales, subsamples evenly when calibration exceeds the requested train+validation rows, and masks selected angles to zero after updates. It returns eight values: pseudo-weight in the original domain, transformed pack weight, quantizer scales, quantizer zeros, theta, runtime reciprocal channel scales, train loss, validation loss. optimize_paroquant_linear_mlx_to_torch synchronizes MPS when necessary, bridges via DLPack, and returns the Torch processor's ParoQuantOptimizationResult with used_identity=False.

The packer serializes AWQ lane order [0,2,4,6,1,3,5,7] and zero point 8. Rotations and runtime channel scales remain essential metadata; the qweight alone is insufficient. See ParoQuant.

Native EXL3 path

EXL3 has a complete calibrated weight-level pipeline, with a Torch processor bridge and individually exposed stages. Its quantization orientation is [input_features, output_features]; stage matrices generally require FP32.

def exl3_quantize_weight_mlx(
    weight,
    hessian,
    input_signs,
    output_signs,
    *,
    sample_count: int,
    bits: int,
    codebook: str = "3inst",
    force_output_scales=None,
    sigma_reg: float = 0.025,
    workspace_bytes: int = 256 << 20,
):
    ...

Both weight dimensions must be divisible by 128; Hessian is [input,input]; input/output signs are FP32 [input,1] and [1,output]. Bits are integers 1..8; pipeline codebooks are exactly 3inst, mcg, mul1. The default pipeline codebook differs from several low-level helpers' mcg default.

The pipeline returns (reconstructed_weight, proxy_error, packed_trellis, input_scales_fp16, output_scales_fp16, fallback, global_scale, apply_output_scales). Reconstruction is FP32; proxy error must be finite. Fallback proxy error is zero because calibrated error is not evaluated there, not because reconstruction is lossless.

Stage-by-stage implementation

Source module Key API and defaults Algorithm/contract
mlx_exl3_pipeline.py exl3_quantize_weight_mlx, exl3_quantize_weight_mlx_to_torch, exl3_mlx_quantization_available Orchestrates all stages. Torch bridge generates deterministic signs with CPU Torch generators seeded seed and seed+1, DLPack-transfers inputs/results, and emits suh, svh, trellis, plus codebook markers for mcg/mul1.
mlx_exl3_hessian.py exl3_finalize_hessian_mlx(..., sample_count, sigma_reg=0.025, block_size=16) Normalizes captured Hessian by sample count, adds sigma_reg * mean(diagonal), applies signs and two order-128 Hadamards, and returns fallback flag, transformed Hessian, LDLQ factor, regularized diagonal. Width divisible by 128. Fallback when count is zero or mean diagonal < 1e-20; nonfinite inputs/results raise.
mlx_exl3_block_ldl.py exl3_block_ldl_mlx(hessian, *, block_size=16) CPU-stream Cholesky and diagonal-block triangular inverses, followed by fused Metal block-column normalization. Writes exact identity diagonal blocks. Finalization removes the scalar diagonal before LDLQ. Non-positive-definite input raises ValueError.
mlx_exl3_rms.py exl3_block_rms_mlx(matrix, *, axis) FP32 RMS with 32-value reductions; axis 0 gives [1,columns], axis 1 gives [rows,1].
mlx_exl3_regularize.py exl3_output_regularize_mlx, exl3_input_regularize_mlx, exl3_regularize_transforms_mlx(..., force_output_scales=None, hessian_diagonal=None, fallback=False) RMS normalization, sign/channel scales, fused left/right Hadamards. For calibrated input, sorts sqrt(hessian_diagonal) and divides the sum of the largest input_width//50 values by the full sum; absent an override, enables output scales when this ratio is below 0.15. Fallback uses the force setting. If mean output RMS is <=1e-30 and a force setting is supplied, output scales are enabled even when that setting was False.
mlx_exl3_hadamard.py exl3_hadamard_128_mlx(matrix, *, axis) Normalized orthonormal/self-inverse order-128 transform along row/column blocks; finite FP32 rank-2 input; transformed dimension divisible by 128.
mlx_exl3_gss.py exl3_sample_global_scale_tiles_mlx(..., width=3), exl3_global_scale_search_mlx(..., bits, codebook="mcg", width=3, workspace_bytes=256<<20) Wrapped-diagonal tile sampling; golden-section search on [0.1,1.9] until interval <= 0.01, returning selected scale and mean of final two FP32 errors.
mlx_exl3_tiles.py exl3_to_tensor_core_tiles_mlx, exl3_from_tensor_core_tiles_mlx Exact 16x16 tensor-core lane permutation; matrix to [input/16,output/16,256] and back; both matrix dimensions divisible by 16.
mlx_exl3.py exl3_quantize_tiles_mlx(..., bits, codebook="mcg", workspace_bytes=256<<20) Two-pass tail-biting Viterbi search on 256-value tiles; input FP32 is cast to FP16 for specified search arithmetic. Returns FP32 decoded values and int16 state bit patterns.
mlx_exl3.py exl3_decode_states_mlx(..., codebook="mcg"), exl3_pack_trellis_mlx(..., bits) Decode int16-as-uint16 codebook states; pack only low bits bits into checkpoint trellis, shape [input/16,output/16,16*bits] int16. Adjacent physical uint16 words are swapped to match EXL3's bitstream ABI.
mlx_exl3_ldlq.py exl3_ldlq_quantize_mlx(..., bits, codebook="mcg", buffer_rows=128, workspace_bytes=256<<20) Descending buffered 16-row LDLQ error feedback with explicit FP32 product accumulation. Both dimensions must be divisible by buffer_rows, a positive multiple of 16; output width must also be divisible by 128. Pending compensation shrinks to unquantized prefix.
mlx_exl3_fallback.py exl3_fallback_quantize_mlx(..., bits, codebook="mcg", workspace_bytes=256<<20) Quantizes regularized weight without Hessian compensation. Input width divisible by 16 and output width by 128. Returns reconstructed matrix and unpacked tile states.

Viterbi workspace is bounded independently of tile count: edges = 1 << (16 - bits), with backtrace bytes per active slot edges * (256*2 + (0 if bits>=4 else 2*2)). Requested workspace must hold at least one slot. Active groups are capped by tile count, 256, and the budget. The budget bounds this search scratch, not total model weights, Hessians, output tensors, or all pipeline temporaries.

Regularization precedes global scaling; the selected scale multiplies regularized weights and divides input scales. Quantization uses either LDLQ or fallback, reconstructs with inverse Hadamards/channel scales, and finalizes the packed trellis. The low-level stage outputs are useful for independent oracle checks; treating unpacked int16 search states as a production inference cache would violate the packed-storage contract. See EXL3.

Automatic MLX acceleration inside Torch workflows

Several ordinary Torch quantization/packing paths can select MLX internally. This does not select the MLX inference backend, extend the high-level quantize_mlx(method=...) list, or mean the entire quantization loop runs in MLX. Their availability probes require Darwin ARM64 and importable mlx.core; unlike checkpoint-holder validation, these probes do not explicitly call mx.metal.is_available().

Calling path Minimum weight elements Additional selection conditions in this revision
quantization/rtn.py 256 * 1024 Rank-2 CPU/MPS weight, no smoothing, integer bits 2..8, positive group size or -1.
quantization/foem.py 256 * 1024 CPU/MPS weight and same-device inverse factor; FOEM config with alpha=0, beta in [0,1]; no activation order, static groups, or MSE search; integer bits 2..8; group 16,32,64,128 dividing width and equal to blocksize.
quantization/qqq.py 4_194_304 CPU/MPS weight and same-device non-None inverse factor; debug disabled; blocksize=128; group -1 or width-dividing 128; no static groups or MSE search; symmetric quantizer with matching group metadata and maximum code 7 for whole-row or 15 for grouped operation.
nn_modules/qlinear/gguf.py packing 2 * 1024 * 1024 Counts the contiguous FP32 CPU matrix after optional smoothing and input padding; otherwise uses the existing non-MLX GGUF packer.
nn_modules/qlinear/fp8.py packing 60 * 1024 * 1024 Supported MLX FP8 format; counts the contiguous FP32 CPU matrix after optional smoothing.
nn_modules/qlinear/bitsandbytes.py four-bit packing 4 * 1024 * 1024 Four-bit branch; matrix has been converted to its configured compute dtype.
nn_modules/qlinear/bitsandbytes.py INT8 packing 8 * 1024 * 1024 INT8 branch; source matrix has been converted to FP16.
looper/exllamav3_processor.py No element threshold in this branch CPU/MPS target, captured Hessian, and positive captured sample count. Uses the native EXL3 bridge instead of the CUDA/HIP route.
looper/paroquant_processor.py optimization No element threshold in this branch Nonempty captured inputs, CPU/MPS weight, module optimization scope, opt_stage_impl="fast", and gradient checkpointing disabled.

These are dispatch conditions, not a substitute for each primitive's shape/dtype validation. The weight-level GPTAQ helper is exposed separately; its existence does not establish automatic dispatch from the Torch GPTAQ loop.

Loading existing checkpoints for MLX inference

from gptqmodel import GPTQModel

model = GPTQModel.load("organization/quantized-model", backend="mlx")
text = model.generate(prompt="Explain groupwise quantization.", max_tokens=128)

This is the GPT-QModel checkpoint conversion route, not the direct native MLX-LM quantization route. To consume the latter's saved artifact, use MLX-LM:

from mlx_lm import load, generate

model, tokenizer = load("./model-mlx-gptq")
text = generate(model, tokenizer, prompt="Explain groupwise quantization.")

Checkpoint holders versus executing kernels

gptqmodel/nn_modules/qlinear/mlx.py contains Torch checkpoint holders and conversion validators, not the final MLX nn.Module implementations. Their forward raises an error requiring conversion before inference. The common validator requires Darwin ARM64, importable MLX/MLX-LM, and mx.metal.is_available().

The holders declare MPS as their Torch-side device, FP16 checkpoint dtype, no training, no adapters, sharding support, and no automatic feature padding. These declarations do not mean the final MLX layer rejects BF16 activations: checkpoint validation and activation execution are separate stages.

Holder class in qlinear/mlx.py Source format / method Bits Declared group sizes Feature alignment (input, output)
MlxQuantLinear GPTQ / GPTQ_V2 / GPTQ_P 2,3,4,5,6,7,8 -1,16,32,64,128,256,512,1024 (32,32)
GPTQMarlinMlxQuantLinear GPTQ Marlin 4,8 -1,32,64,128 (32,32)
GPTQBitBLASMlxQuantLinear GPTQ BitBLAS 2,4,8 -1,32,64,128 (32,32)
AwqMlxQuantLinear AWQ GEMM 2,3,4,5,6,7,8 -1,16,32,64,128 (8,8)
AWQMarlinMlxQuantLinear AWQ Marlin 4,8 -1,32,64,128 (8,8)
AWQBitBLASMlxQuantLinear AWQ BitBLAS 2,3,4,5,6,7,8 -1,32,64,128 (16,16)
AwqGemvMlxQuantLinear AWQ GEMV 4 -1,64,128 (32,64)
AwqGemvFastMlxQuantLinear, LLMAwqMlxQuantLinear AWQ GEMV_FAST / LLM_AWQ 4 -1,16,32,64,128 (64,8)
ParoMlxQuantLinear ParoQuant 4 -1,16,32,64,128 (32,8)
QQQMlxQuantLinear QQQ 4 source bits -1,128 (64,64)
GGUFMlxQuantLinear Supported GGUF types below 1,2,4,5,6,8 Format/block dependent (32,8) plus full source blocks
FP8MlxQuantLinear FP8 8 Scale-method dependent (1,1); native MXFP8 adds 32-input alignment
BitsAndBytesMlxQuantLinear BNB NF4/FP4/INT8 4,8 Format dependent (1,1)

The table is a registry declaration, not the entire compatibility test. The repackers have additional constraints, notably native affine _pack_rows requires input width divisible by 32. Group-16 requires 32-input alignment even where a class declares a weaker generic alignment. Requested whole-row -1 must map to a supported effective group or an input width >=128 divisible by 128. Shapes, source class, zero layout, g_idx, scale dtype, and runtime metadata are validated too.

GPTQ holders require desc_act=False and monotonically grouped g_idx == arange(in_features)//group_size; nontrivial activation-order permutations are not silently discarded. GPTQ/GPTQ_V2 require normalized raw-zero v2 state (REQUIRES_FORMAT_V2=True); GPTQ_P is explicitly accepted with raw zero codes despite legacy metadata. The Marlin holder repairs legacy zero-minus-one storage before MLX packing. GPTQ Marlin is symmetric-only. AWQ GEMM also declares desc_act=False; several AWQ-specific source holders permit both metadata values. ParoQuant and QQQ require symmetric source layouts.

Conversion lifecycle and fallback

def convert_gptq_to_mlx_weights(
    model_id_or_path: str,
    model: Union[PreTrainedModel, BaseQModel],
    gptq_config: dict,
    lm_head_name: str,
):
    ...

The conversion wrapper in utils/mlx.py runs conversion under mx.stream(mx.cpu) while restoring the prior stream afterward. _packed_mlx_weights validates each supported source layer, transcodes checkpoint weights, creates the MLX-LM architecture from config, quantizes matching leaf modules, installs custom runtime wrappers, and loads transferred arrays. If packed layer names do not match the architecture's to_quantized modules, it returns None rather than pretending that the mapping succeeded.

Supported conversion format values are GPTQ, GPTQ_V2, GPTQ_P, GEMM, GEMV, GEMV_FAST, LLM_AWQ, MARLIN, BITBLAS, PAROQUANT, QQQ, GGUF, FP8, BITSANDBYTES, and EXL3. This top-level list is broader than any one holder's bit/group contract. Nonempty dynamic configurations are also checked and currently accept dynamic bit values 2..8, not dynamic one-bit settings.

Every nonempty dynamic rule must explicitly contain bits: this conversion wrapper reads config["bits"] rather than inheriting the base value. A group-size-only rule therefore raises KeyError here even if it is valid for the ordinary QuantizeConfig path. Empty rule dictionaries skip this bit check. This is a conversion limitation, not a general dynamic-configuration requirement.

Conversion case Behavior
Transferable packed source Preserves supported checkpoint semantics through a method-specific transcode; logs transfer without requantization. This is not a statement that every method retains the original bit width; grouped QQQ and seven-bit affine sources change storage width as described below.
Untransferable ordinary GPTQ/AWQ Legacy fallback dequantizes source layers, casts to FP16, builds an MLX-LM model, and calls quantize_model; this is requantization, not a bit-exact transcode
Legacy fallback group -1 or 16 Changes the config's group size to 64 before requantization
Special formats without transferable state ParoQuant, QQQ, GGUF, FP8, BNB, EXL3, GEMV, GEMV_FAST, LLM_AWQ, Marlin, and BitBLAS raise instead of taking the generic requantization fallback
Custom-runtime marker _gptqmodel_group16_runtime or _gptqmodel_custom_mlx_runtime tells models/loader.py to retain the constructed runtime model directly
Ordinary native runtime Loader uses a temporary MLX-LM save/config/tokenizer directory and reloads it with mlx_lm.load

Do not assume every returned runtime can be serialized and reloaded by stock MLX-LM. The custom markers exist precisely because the ordinary loader only reconstructs native quantized layers. The generic fallback also consumes/empties parts of the source model during conversion; it is not an API for retaining two independently usable model copies.

Packed layouts and exact transcoding

gptqmodel/utils/mlx_packing.py uses NumPy to move discrete codes into the target ABI. The key public-style helper signatures are:

def repack_gptq(qweight, qzeros, scales, in_features, out_features, bits, planar=None):
    ...

def repack_awq(qweight, qzeros, scales, in_features, out_features, bits=4, planar=False):
    ...

def repack_gptq_bitblas(qweight, qzeros, scales, in_features, out_features, bits, sym):
    ...

def repack_awq_bitblas(qweight, qzeros, scales, in_features, out_features, bits=4):
    ...
Source representation Target mapping / caveat
gptq_v2 continuous/planar Unpack input-axis codes and output-axis raw zeros; transpose scales to [out,groups]; additive bias -zero*scale; target uint32 row streams. Planar default is enabled for bits 5,6,7; caller may explicitly select source layout.
Native affine rows Bits 2,4,8 fit integer codes per uint32; 3,5,6 cross uint32 boundaries in a continuous little-endian bitstream.
Seven-bit source No native affine 7-bit target; preserves code values in packed 8-bit target words. Storage grows; it is not a lossy change of levels.
Large/full-row affine groups Metadata repeats scales/biases into supported subgroups of at most 128 without changing codes.
Group 16 Keeps one packed weight stream, separate even/odd 16-value scale/bias arrays; fallback executes two masked group-32 quantized products.
AWQ GEMM Decodes output-axis continuous streams; four-bit AWQ reverses its lane permutation before row packing. Non-planar only.
GPTQ BitBLAS Portable byte-packed rows only, bits 2,4,8; signed symmetric codes flip the sign bit for unsigned-affine mapping. Transformed private BitBLAS state is not assumed portable.
AWQ BitBLAS Continuous little-endian byte streams, 2..8 bits; seven-bit target promoted to eight; source scales cast to FP16.
AWQ GEMV Output-major INT4 plus padded output-major scale/zero rows; extracts only actual groups.
AWQ GEMV_FAST / LLM_AWQ Undoes four-output interleave and the 32-input permutation; already-scaled zero offsets become additive biases.
ParoQuant AWQ codes plus learned rotations/channel scales; cannot discard runtime transforms.
QQQ whole-row Signed nibbles XOR sign bit, affine scale 16/bias -128; group-128 source uses packed 8-bit inference values after group-to-channel scaling.
FP8 Native MXFP8 for a compatible subset, otherwise raw FP8 bytes + a 256-entry decode codebook and FP32 inverse-scale metadata.
BNB four-bit Keeps compressed nibbles + NF4/FP4 codebook and decoded scale metadata; nested scale statistics may be decoded, not full dense weights.
BNB int8 If width divides 128/64/32, exact unsigned affine mapping with row scale absmax/127, bias -128*scale; otherwise packed signed-int8 Metal path.
EXL3 Checkpoint trellis remains packed; runtime reconstructs cyclic trailing 16-bit windows only inside its consuming kernel.

Ordinary GPTQ/AWQ holders supply FP16 scales to their conversion packer; affine biases are computed in FP32. Unquantized Torch model parameters and many biases are also transferred as FP16. “Exact packed transfer” means preserving the discrete source checkpoint semantics, not a promise to preserve an arbitrary original BF16 dense model dtype end-to-end.

Current grouped-QQQ storage limitation: when the effective source group differs from the input width, QQQMlxQuantLinear.pack_source reconstructs the group's scaled integer values and stores the complete layer as packed 8-bit affine weights. This preserves the intended QQQ integer inference values, but expands the four-bit source into a retained eight-bit representation. Under the explicit INT4-to-INT8 prohibition in AGENTS.md, this route must not be described as passing the persistent-storage acceptance gate. The whole-row decision is module.group_size == module.in_features, which selects a distinct four-bit mapping; a group-128 layer with input width 128 therefore also takes that branch. The expanded grouped path is documented here as an implementation limitation, not endorsed as a compliant optimization.

The FP8 native MXFP8 subset is likewise conditional: the holder requires float8_e4m3fn, row or tensor inverse scaling, input width divisible by 32, finite positive inverse scales with finite reciprocals, and no E4M3 NaN codes. Other source-compatible FP8 layers use the packed-byte/codebook runtime; they are not silently converted to native MXFP8. Source compatibility also requires inverse-scale semantics and the exact weight matrix shape.

GGUF-specific mapping

utils/mlx_gguf_packing.py separates affine and native FP4 modes:

def repack_gguf_affine(qweight: np.ndarray, qtype: str, in_features: int):
    ...

def repack_gguf_float4(qweight: np.ndarray, qtype: str, in_features: int):
    ...

Both return (weight, scales, biases, quantization_params). GGUF affine transfer rejects padded/malformed blocks. Accepted source qtype names are case-sensitive here: Q1_0, Q1_0_g128, Q2_0, Q4_0, Q8_0, Q4_K, Q5_K, Q6_K, TQ1_0, TQ2_0, MXFP4, and NVFP4. Unlike the quantization entry point, the transfer helper does not uppercase arbitrary strings or accept _S/_M aliases directly.

GGUF type Target bits/group/mode
Q1_0 / Q1_0_g128 2-bit, group 128, affine; codes 0/1 with scale 2*d, bias -d
Q2_0 2-bit, group 64, affine
Q4_0 4-bit, group 32, affine
Q8_0 8-bit, group 32, affine; signed byte plus 128
Q4_K / Q5_K 4-/5-bit, group 32, affine; per-subgroup scale and minimum
Q6_K 6-bit packed rows, 16-value source scales; custom even/odd runtime despite group-32 config compatibility
TQ1_0 / TQ2_0 2-bit, group 128, affine; repeats block scale for the two subgroups
MXFP4 4-bit, group 32, mode="mxfp4"; uint8 encoded scales, no additive bias
NVFP4 4-bit, group 16, mode="nvfp4"; uint8 encoded scales, no additive bias

Use repack_gguf_float4 for MXFP4/NVFP4; their appearance in the common supported-type set does not make them affine. The float4 helper reorders low/high half-block nibbles into native input order and retains each format's scale bytes.

Executing MLX qlinear kernels

The following are the actual MLX nn.Module classes, installed by utils/mlx.py rather than called as Torch checkpoint holders:

Module Runtime class(es) Execution behavior
mlx_gptq.py MlxGPTQLinear, MlxGPTQGroup16Linear Ordinary affine wrapper casts output to activation dtype. Group-16 uses packed Metal single-row decode; selected BF16 prefill uses tiled Metal; otherwise two group-32 products.
mlx_awq.py MlxAWQLinear, MlxAWQGroup16Linear Same output-dtype contract; AWQ group-16 accepts target storage bits 2,3,4,5,6,8.
mlx_group16.py MlxGroup16Linear Two masked mx.quantized_matmul calls using shared packed weights and independent even/odd scale metadata; adds bias and restores input dtype.
mlx_gguf.py MlxGGUFLinear, MlxGGUFQ6KLinear Ordinary quantized wrapper plus exact Q6_K group-16 fused packed decode/small-prefill kernel; two-product fallback for other prefill.
mlx_paro.py MlxParoLinear Precomputes partner/cos/sin indexing metadata, applies channel scales and all Givens stages in FP32, then packed affine matmul and input-dtype output. Identity state skips rotation.
mlx_qqq.py MlxQQQLinear Converts input to FP16, per-token max/127 activation quantization, affine product, input/channel scaling, FP16 rounding, bias, then original output dtype.
mlx_fp8.py MlxFP8Linear, MlxFP8PackedLinear Compatible native MXFP8 computes unscaled dot in FP32 then applies output scale; general packed Metal decodes raw FP8/codebook bytes with row/tensor/block scales.
mlx_bitsandbytes.py MlxBitsAndBytesLinear Packed NF4/FP4 Metal matmul; aligned int8 affine path or signed-int8 Metal path; decoded four-bit and non-affine int8 weight values intentionally round through FP16 before FP32 accumulation.
mlx_exl3.py MlxEXL3Linear Packed trellis decode, input/output Hadamard and channel scaling, single-row or 16-row tiled prefill; no dense weight fallback. Supports bits 1..8, codebooks 3inst,mcg,mul1, positive feature multiples of 128.

Decode and prefill dispatch are different numerical paths

Kernel Current source dispatch
GPTQ group-16 Decode when flattened rows == 1; output width >=5120 selects multi-column warp decode, otherwise the original packed decode. Packed BF16 prefill requires input >=5120, output <=1024, and rows 2..8, 12, or 16. Otherwise generic group-16 path.
AWQ group-16 Decode when rows == 1; output width >=5120 selects the same multi-column warp decode. Packed BF16 prefill for rows 2..16 and output <=2048.
GGUF Q6_K Decode when rows == 1. Packed BF16 prefill for rows 2..16 and output <=2048; otherwise two-product fallback.
ParoQuant Fuses all rotations when nonempty, group <=128, and BF16 input or more than one rotation. Nonempty FP16 otherwise uses per-stage Metal; other cases use array math. FP32 is retained between stages.
Packed FP8 Tiled prefill when rows !=1, input <=8192, output >=8192, input divisible by64 and output divisible by32. Other cases use row-tiled packed reduction.
EXL3 Single-row fuses output transform/scales/bias when output width > input width; other decode uses separate output transform. Multirow uses 16-row padded scratch and 16x16 bounded weight tiles.

The GPTQ/AWQ wide single-row path reuses each activation load across four output columns per warp, with eight warps per threadgroup and guarded tail columns. It consumes packed weights directly, accumulates in FP32, and returns the input activation dtype, including FP16 and BF16. Selection is automatic; no configuration flag or checkpoint conversion is needed, and the prefill gates above are unchanged.

These conditions describe this source version, not stable application knobs. A benchmark of one row/shape/dtype does not measure every path. Zero-element inputs are explicitly handled in the custom packed kernels; test those alongside ordinary decode and prefill.

FP16 versus BF16 caveats

  1. Output dtype preservation does not mean all intermediates use that dtype. ParoQuant rotations, EXL3 transforms, Hessian stages, and many reductions deliberately use FP32.
  2. FP16 has a smaller exponent range than BF16. Native MXFP8 computes its unscaled dot in FP32 because it can overflow FP16 before the checkpoint output scale brings the final result back into range.
  3. QQQ converts activations to FP16 and applies FP16 output rounding even for BF16 callers, then casts the result back. Its per-token max/127 activation scale stays FP32; zero rows use scale 1.0, avoiding scale underflow/zero division. BF16 output is not a fully BF16 internal QQQ algorithm.
  4. BNB's codebook/scale product rounds through FP16 in the packed Metal path. Its aligned int8 affine path is separate arithmetic and needs separate comparison.
  5. Group-16 may choose a special prefill kernel only for BF16; FP16 and BF16 cannot share one performance result by assumption.
  6. BF16 cannot represent every FP16 result (and vice versa). Compare the FP32 oracle and the oracle rounded to the requested output dtype separately; final-rounding drift is not the same as accumulation error.
  7. FP16 checkpoint-holder validation is distinct from support for BF16 activations. Do not change holder metadata to BF16 merely to request BF16 execution.

Generation adapter

mlx_generate(model, tokenizer, **kwargs) requires prompt=; it does not consume Transformers tensor input IDs as a substitute. It delegates to mlx_lm.generate and returns that function's result.

Accepted keyword Default / handling
prompt Required; missing or None raises ValueError
verbose False
max_tokens 256; use this, not an assumed max_new_tokens alias
sampler Optional explicit sampler
temp / temperature, top_p, min_p, min_tokens_to_keep Creates a sampler if any is supplied and no explicit sampler exists; non-None temp takes precedence over temperature. Construction defaults temp=0.0, top_p=0.0, min_p=0.0, min_tokens_to_keep=1.
logits_processors Optional existing list
repetition_penalty, repetition_context_size Non-None, non-1 penalty appends processors; context default 20
prefill_step_size 512
kv_group_size, quantized_kv_start 64, 0
max_kv_size, prompt_cache, kv_bits, prompt_progress_callback Forwarded only when present
formatter Removed, not forwarded

Only the explicitly collected sampling arguments reach MLX-LM. Unrecognized leftover keywords are not forwarded. A Transformers-style do_sample, max_new_tokens, attention-mask, or generation-config argument therefore must not be assumed to behave identically here. Apply any chat template deliberately with the tokenizer appropriate to the saved model. See Hugging Face Integration.

Benchmarks and numerical validation

The repository includes target-hardware tests and synchronized benchmark programs. No throughput/error numbers are asserted by this page: obtain them on the actual Apple Silicon host, installed MLX/MLX-LM versions, checkpoint, shape, and dtype. A documentation/source audit is not a substitute for executing those tests.

Validation area Representative source files
Native model/weight GPTQ tests/test_mlx_native_quantization.py, tests/test_mlx_qwen38_native_gptq_audit.py
Exact GPTQ/AWQ transfer tests/test_mlx_packing.py, tests/test_mlx_gptq_formats.py, tests/test_mlx_awq_formats.py, tests/test_mlx_awq_full_bits.py, tests/test_mlx_qwen38_packing_audit.py
RTN/GPTAQ/FOEM/QQQ/FP8/GGUF/BNB quantization Corresponding tests/test_mlx_*_quantization.py; QQQ processor also tests/test_mlx_qqq_processor.py
ParoQuant stages tests/test_mlx_paroquant_rotation.py, tests/test_mlx_paroquant_quantization.py, tests/test_mlx_paroquant_packing.py, tests/test_mlx_paroquant_optimize.py
EXL3 stages tests/test_mlx_exl3.py, tests/test_mlx_exl3_codebook.py, tests/test_mlx_exl3_viterbi.py, and tests/test_mlx_exl3_{block_ldl,gss,hadamard,hessian,ldlq,pipeline,regularize,rms,tiles,fallback}.py
Output dtype tests/test_mlx_gptq_output_dtype.py, tests/test_mlx_bnb_output_dtype.py, tests/test_mlx_gguf_output_dtype.py, tests/test_mlx_exl3_output_dtype.py plus FP8/ParoQuant/QQQ runtime tests
Real projection matrix tests/qwen38_27b_shapes.py, tests/test_mlx_qwen38_27b_shapes.py; full attention Q/K/V/O, linear-attention fused QKV/O, MLP gate/up/down
Performance harnesses tests/benchmark_mlx_qwen38_*.py; individual format, dtype, decode/prefill, packer, and pipeline programs

Example focused commands, to run on Apple Silicon with the project dependencies installed:

python -m pytest -q tests/test_mlx_native_quantization.py tests/test_mlx_packing.py
python -m pytest -q tests/test_mlx_gptq_output_dtype.py tests/test_mlx_gguf_output_dtype.py
python -m pytest -q tests/test_mlx_paroquant_rotation.py tests/test_mlx_exl3_pipeline.py
python tests/benchmark_mlx_qwen38_27b.py --rows 1 16 --repeats 5 --krot 1 --rotate
python tests/benchmark_mlx_qwen38_exl3_pipeline.py --samples 3 --dtype bf16

The Qwen3.8-27B ParoQuant harness constructs each projection's packed source state, checks Torch-MPS/MLX output agreement at rtol=atol=0.002, warms up once, synchronizes every result, and reports median repeated milliseconds, speedup and max absolute error as JSON lines. This harness is a comparison of operations at those projection shapes, not a full-model token/s benchmark. Its FP16 input generation does not replace BF16 validation required by the current rules.

Acceptance gates

AGENTS.md, .agents/skills/gptqmodel-torch-oracle-numerics/SKILL.md, and docs/mlx-native-quantization-plan.md establish the following requirements:

Gate Required evidence
Independent oracle Deterministic identical inputs/configuration; Torch reference performs its own arithmetic rather than calling the MLX kernel under test
Discrete outputs Exact complete packed bytes/codes/indices, except individually proven rounding ties; no blanket tolerance on code mismatches
Quantization floating outputs rtol <=1e-6 and atol <=1e-6 away from ties; normalized matrix error <=1e-6 for scale-dependent stages; nonfinite checks
Boundary cases Exact thresholds and adjacent representable values, zeros/signed zero where significant, saturation, clamp/scale/zero-point ties, supported dtype casts
Inference rtol and atol <=2e-3; exact selected token IDs when sampling/ties are fixed
Activation dtypes FP16 and BF16 independently; assert output dtype; report raw FP32-oracle and same-output-dtype visible drift separately
Dispatch Decode, tiled prefill, generic fallback, bias/no bias, applicable shapes and explicit unsupported-dtype rejection
Performance Same operation against main, resident inputs, identical shape/dtype/warmup/synchronization; per-projection timings and stated aggregation
Artifact usability Save and reload with the intended runtime; primitive-only byte production is not proof of complete model support
Persistent storage Quantized weights and permitted scales/codebooks/index metadata only; no retained dense/dequantized/expanded caches across calls, including fallbacks/compiled paths

For true near-ties, retain a higher-precision independent reference and classify the mismatch individually. Report total mismatches, proven tie mismatches, floating drift away from ties, reference precision, and output drift. Do not weaken all tolerances to excuse one boundary case.

Inspect state after first and repeated forwards, mutation/replacement, checkpoint loads, dtype/device moves and supported explicit-clear operations. Report unchanged checkpoint bytes, extra packed-layout bytes, and peak transient dequantization scratch separately. A full-layer expanded INT4-to-INT8 cache is not permitted just because it uses integers; moving it to CPU or a nonpersistent buffer does not fix its lifetime. Code inspection of one route does not establish compliance for all kernels.

The native-quantization plan is a design/acceptance document, not a current support matrix. In particular its historical statement that NVFP4 is dequantization-only is superseded by the current mlx_gguf.py NVFP4 packer and mlx_gguf_packing.py native FP4 transfer path described above.

Pitfalls and edge cases

  • Choose the correct entry point: the high-level quantize_mlx factory accepts only GPTQ/AWQ, while native method primitives and Torch-processor bridges are separate interfaces.
  • Do not confuse custom runtime availability with a stock MLX-LM reloadable artifact. Exercise the intended consumer before publishing.
  • Check actual per-layer config after native GPTQ: fallback six-bit layers and skipped modules may coexist with the requested four-bit layers.
  • Preserve zero conventions and source packing metadata; gptq (v1), gptq_v2, gptq_p, AWQ, and legacy Marlin are not interchangeable byte streams.
  • Nontrivial GPTQ activation order cannot be passed to the exact affine holder by ignoring g_idx.
  • A seven-bit source maps to eight-bit packed target storage; group-16 may use two quantized matmuls. Account for both storage and work in benchmarks.
  • GGUF _S/_M aliases in a block packer do not choose model-wide mixed precision. A raw block array is not a complete GGUF file.
  • ParoQuant pairs, angles, and channel scales and EXL3 trellis/scales/codebook must travel together.
  • BNB checkpoint conversion still requires its source library validation and, for nested scales, bitsandbytes scale decoding; the existence of an MLX weight packer alone does not remove those loader dependencies.
  • Conversion may cast scale/nonquantized tensors to FP16. BF16 activation support does not mean that conversion preserves every source BF16 parameter.
  • max_tokens and prompt are MLX generation arguments here. Do not assume unused Transformers kwargs are forwarded.
  • Do not publish generic speedups without stating shape, row count, dtype, dispatch path, sync method, median/aggregation, errors, and weight/scratch storage.

Source files

Area Paths
Entry point, dependencies, runtime construction gptqmodel/models/auto.py, gptqmodel/models/loader.py, pyproject.toml
Runtime conversion/generation gptqmodel/utils/mlx.py
GPTQ/AWQ packing gptqmodel/utils/mlx_packing.py
GGUF packing transfer gptqmodel/utils/mlx_gguf_packing.py
Native model quantization gptqmodel/quantization/mlx_native.py
Automatic Torch-workflow bridges gptqmodel/quantization/rtn.py, gptqmodel/quantization/foem.py, gptqmodel/quantization/qqq.py, gptqmodel/looper/exllamav3_processor.py, gptqmodel/looper/paroquant_processor.py, gptqmodel/nn_modules/qlinear/gguf.py, gptqmodel/nn_modules/qlinear/fp8.py, gptqmodel/nn_modules/qlinear/bitsandbytes.py
RTN/GPTAQ/FOEM gptqmodel/quantization/mlx_rtn.py, gptqmodel/quantization/mlx_gptaq.py, gptqmodel/quantization/mlx_foem.py
FP8/GGUF/QQQ/BNB gptqmodel/quantization/mlx_fp8.py, gptqmodel/quantization/mlx_gguf.py, gptqmodel/quantization/mlx_qqq.py, gptqmodel/quantization/mlx_bitsandbytes.py
ParoQuant gptqmodel/quantization/mlx_paroquant.py, gptqmodel/quantization/mlx_paroquant_optimize.py, gptqmodel/quantization/mlx_paroquant_quant.py, gptqmodel/quantization/mlx_paroquant_rotation.py
EXL3 codebooks, packing, fallback gptqmodel/quantization/mlx_exl3.py, gptqmodel/quantization/mlx_exl3_fallback.py
EXL3 complete pipeline gptqmodel/quantization/mlx_exl3_pipeline.py
EXL3 Hessian/factorization/LDLQ gptqmodel/quantization/mlx_exl3_hessian.py, gptqmodel/quantization/mlx_exl3_block_ldl.py, gptqmodel/quantization/mlx_exl3_ldlq.py
EXL3 transforms/search gptqmodel/quantization/mlx_exl3_gss.py, gptqmodel/quantization/mlx_exl3_hadamard.py, gptqmodel/quantization/mlx_exl3_regularize.py, gptqmodel/quantization/mlx_exl3_rms.py, gptqmodel/quantization/mlx_exl3_tiles.py
Checkpoint holders gptqmodel/nn_modules/qlinear/mlx.py
GPTQ/AWQ/group-16 inference gptqmodel/nn_modules/qlinear/mlx_gptq.py, gptqmodel/nn_modules/qlinear/mlx_awq.py, gptqmodel/nn_modules/qlinear/mlx_group16.py
MLX dispatch steering gptqmodel/quantization/mlx_splitk_dispatch.py — pads decode/prefill row counts toward better Metal kernels; mirrors the MLX 0.32.4 quantized.cpp dispatch and stops steering once the installed MLX is newer (unparsable versions count as unmirrored)
Method-specific inference gptqmodel/nn_modules/qlinear/mlx_bitsandbytes.py, gptqmodel/nn_modules/qlinear/mlx_exl3.py, gptqmodel/nn_modules/qlinear/mlx_fp8.py, gptqmodel/nn_modules/qlinear/mlx_gguf.py, gptqmodel/nn_modules/qlinear/mlx_paro.py, gptqmodel/nn_modules/qlinear/mlx_qqq.py
Validation policy AGENTS.md, .agents/skills/gptqmodel-torch-oracle-numerics/SKILL.md, docs/mlx-native-quantization-plan.md
Tests/benchmarks tests/test_mlx_*.py, tests/benchmark_mlx_qwen38_*.py, tests/qwen38_27b_shapes.py

See also

Clone this wiki locally