Summary
The OpenXLA packed in-graph dequant path (MLXCEL_XLA_QUANT=packed, landed in #568) currently requires the MLX affine scales/biases to be f16. mlx-lm stores them as bf16 for some checkpoints (Qwen3, Gemma3-27B, Qwen3-MoE). Issue #569 already broadened the dequant-at-LOAD path (dequantize to f32 at load) to accept bf16 scales, but the packed path (which uploads the raw scales and dequantizes in the StableHLO graph) still rejects bf16 and only accepts f16. This issue extends the packed path to accept bf16 scales/biases, so a bf16-scale MLX checkpoint can use the packed representation.
Background
This was the residual in-tree deliverable of #574 and is deferred, low-priority breadth work.
The #573 spike (merged, PR #578) concluded that the dequant to matmul fusion on the CUDA target is upstream IREE work: there is no in-tree flag or representation that fuses it, and int8 has no tensor-core path in IREE 3.11.0rc. As a result the packed path is off by default and is currently dominated by f16-resident weights (#572) on CUDA.
This bf16-scale breadth is therefore low priority. It prepares the packed representation for more checkpoints so it is ready for when upstream IREE lands a fused CUDA quantized-matmul, and it was deferred out of #574.
Scope
Concrete changes, all in src/lib/mlxcel-xla:
- Emitter (
emitter/model.rs, take_weight packed branch): declare the .scales / .biases args as bf16 (instead of the current hard-coded f16) when the checkpoint stores them as bf16. This requires threading the scale element type from the checkpoint to the emit path. The emitter currently emits purely from config.json, which does not carry the scale dtype, so thread it analogously to how precision is threaded via resolve_precision.
Builder::dequant_affine (emitter/builder.rs): handle a bf16 scales/biases operand by converting bf16 to f32 in-graph before q*scale + bias, matching the existing f16 handling.
- Loader (
iree.rs, load_weights QuantRaw path): accept Dtype::BF16 scales/biases (today it errors unless Dtype::F16) and upload the raw bf16 bytes with a new WDT_BF16 dtype code.
- C shim (
csrc/xla_iree.c): add a case mapping the bf16 dtype code to IREE_HAL_ELEMENT_TYPE_BFLOAT_16 (esize 2), alongside the existing f32/f16/u32 cases.
- Keep the emitter and loader in lock-step on the scale dtype (like the existing packed f16 path), so the uploaded buffer dtype always matches the emitted arg.
Integration / Acceptance
Dependencies / Links
Summary
The OpenXLA packed in-graph dequant path (
MLXCEL_XLA_QUANT=packed, landed in #568) currently requires the MLX affinescales/biasesto be f16. mlx-lm stores them as bf16 for some checkpoints (Qwen3, Gemma3-27B, Qwen3-MoE). Issue #569 already broadened the dequant-at-LOAD path (dequantize to f32 at load) to accept bf16 scales, but the packed path (which uploads the raw scales and dequantizes in the StableHLO graph) still rejects bf16 and only accepts f16. This issue extends the packed path to accept bf16 scales/biases, so a bf16-scale MLX checkpoint can use the packed representation.Background
This was the residual in-tree deliverable of #574 and is deferred, low-priority breadth work.
The #573 spike (merged, PR #578) concluded that the dequant to matmul fusion on the CUDA target is upstream IREE work: there is no in-tree flag or representation that fuses it, and int8 has no tensor-core path in IREE 3.11.0rc. As a result the packed path is off by default and is currently dominated by f16-resident weights (#572) on CUDA.
This bf16-scale breadth is therefore low priority. It prepares the packed representation for more checkpoints so it is ready for when upstream IREE lands a fused CUDA quantized-matmul, and it was deferred out of #574.
Scope
Concrete changes, all in
src/lib/mlxcel-xla:emitter/model.rs,take_weightpacked branch): declare the.scales/.biasesargs asbf16(instead of the current hard-codedf16) when the checkpoint stores them as bf16. This requires threading the scale element type from the checkpoint to the emit path. The emitter currently emits purely fromconfig.json, which does not carry the scale dtype, so thread it analogously to how precision is threaded viaresolve_precision.Builder::dequant_affine(emitter/builder.rs): handle a bf16 scales/biases operand by converting bf16 to f32 in-graph beforeq*scale + bias, matching the existing f16 handling.iree.rs,load_weightsQuantRaw path): acceptDtype::BF16scales/biases (today it errors unlessDtype::F16) and upload the raw bf16 bytes with a newWDT_BF16dtype code.csrc/xla_iree.c): add acasemapping the bf16 dtype code toIREE_HAL_ELEMENT_TYPE_BFLOAT_16(esize 2), alongside the existing f32/f16/u32 cases.Integration / Acceptance
MLXCEL_XLA_QUANT=packedgate; a bf16-scale MLX checkpoint that satisfiesConfig::supports_packed_quantloads and runs.Dependencies / Links