Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b027f8a
Refactor GGUF loading: new converter chain & GGUFQuantizer
ArthurZucker May 13, 2026
1b33217
Drop gguf_rename factory; use plain WeightRenaming/WeightConverter
ArthurZucker May 13, 2026
d6d6359
Replace gguf-py NumPy dequant with pure-PyTorch port (3-8x faster)
ArthurZucker May 13, 2026
06589a6
Dequant on the target device; drop gguf-py fallback
ArthurZucker May 13, 2026
cf27173
Inline GGUF spawn into GGUFQuantizer; drop spawn_gguf_materialize
ArthurZucker May 13, 2026
f7378ff
Simplify GGUF renaming with regex alternations; minimise diff vs main
ArthurZucker May 13, 2026
33c7f99
docs: add 'Adding GGUF support for a new architecture' contributor guide
ArthurZucker May 13, 2026
2203bc3
Fix type check: load_config.hf_quantizer can be None in is_hqq_or_qua…
ArthurZucker May 13, 2026
0b23297
Fix MPS dequant + Qwen biases; further consolidate T5 FFN
ArthurZucker May 13, 2026
67de05d
Guard heavy imports behind is_torch_available() — fixes tokenizer-onl…
ArthurZucker May 13, 2026
0cb151c
Update src/transformers/modeling_gguf_pytorch_utils.py
ArthurZucker May 13, 2026
b883274
Update src/transformers/modeling_gguf_pytorch_utils.py
ArthurZucker May 13, 2026
ec2f319
Restore gpt_oss rope_scaling reconstruction + minimax_m2 scoring_func…
ArthurZucker May 13, 2026
ed9c07e
Free per-source tensors in fuse ops + drain MPS pool between stages
ArthurZucker May 14, 2026
0cd7bce
Fix Gemma2/3 GGUF rename rules + drop GGUF runtime-aux tensors
ArthurZucker May 14, 2026
a3085b9
Fix GGUF rename path: dequantize before assigning to model
ArthurZucker May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions docs/source/en/gguf.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ rendered properly in your Markdown viewer.

The GGUF format also supports many quantized data types (refer to [quantization type table](https://hf.co/docs/hub/en/gguf#quantization-types) for a complete list of supported quantization types) which saves a significant amount of memory, making inference with large models like Whisper and Llama feasible on local and edge devices.

Transformers supports loading models stored in the GGUF format for further training or finetuning. The GGUF checkpoint is **dequantized to fp32** where the full model weights are available and compatible with PyTorch.
Transformers supports loading models stored in the GGUF format for further training or finetuning. The GGUF checkpoint is **dequantized lazily on the target device** by the conversion-mapping pipeline (see [Adding GGUF support for a new architecture](#adding-gguf-support-for-a-new-architecture) below for how that's plugged in).

> [!TIP]
> Models that support GGUF include Llama, Mistral, Qwen2, Qwen2Moe, Phi3, Bloom, Falcon, StableLM, GPT2, Starcoder2, and [more](https://github.com/huggingface/transformers/blob/main/src/transformers/integrations/ggml.py)
> Architectures wired up for GGUF loading include Llama, Mistral, Phi3, Cohere, Qwen2, Qwen3, Deci, StableLM, Starcoder2, Nemotron, Gemma2, Gemma3 (text + multimodal), Bloom, GPT2, Mamba, LFM2, Falcon, Qwen2-MoE, Qwen3-MoE, MiniMax-M2, GPT-OSS, T5, UMT5. The authoritative list is the `_GGUF_ARCH_CONVERTERS` mapping in [`modeling_gguf_pytorch_utils.py`](https://github.com/huggingface/transformers/blob/main/src/transformers/modeling_gguf_pytorch_utils.py).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, imo we can keep a few important ones and just reference the complete list. Not sure if it's smart to always list all models since it's hard to maintain in the docs


Add the `gguf_file` parameter to [`~PreTrainedModel.from_pretrained`] to specify the GGUF file to load.

Expand All @@ -52,3 +52,80 @@ model.save_pretrained("directory")

!python ${path_to_llama_cpp}/convert-hf-to-gguf.py ${directory}
```

## Adding GGUF support for a new architecture

GGUF loading plugs into the same weight-conversion framework that the rest of `transformers` uses for safetensors / Fp8 / etc. To support a new architecture you only need to:

1. **Pick the HF `model_type`.** The loader looks up the rename table by the HF `model_type` (e.g. `"llama"`, `"qwen2_moe"`, `"gemma3_text"`). This is the same string `config.model_type` produces.
2. **Write a list of `WeightRenaming` / `WeightConverter` rules** in [`src/transformers/modeling_gguf_pytorch_utils.py`](https://github.com/huggingface/transformers/blob/main/src/transformers/modeling_gguf_pytorch_utils.py) and register it in `_GGUF_ARCH_CONVERTERS`.
3. **Add the `model_type` to `EXPECTED_MODEL_TYPES`** in [`tests/quantization/ggml/test_gguf_arch_coverage.py`](https://github.com/huggingface/transformers/blob/main/tests/quantization/ggml/test_gguf_arch_coverage.py) so the fast regression test enforces it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there similar architectures were the conversions would overlap? Similar to conversion mapping where we point to archs that have the same converters


### Rule types

The conventions are the same as in `convert_mistral4_weight_to_hf.py` and `Fp8Quantizer`:

```python
from transformers.core_model_loading import WeightConverter, WeightRenaming

# Pure key rename (cheap — no op chain). Substring `re.sub` style: escape dots,
# use alternation when the same target template covers several source names.
WeightRenaming(r"^blk\.", "model.layers.")
WeightRenaming(r"\.ffn_(gate|up|down)\.weight", r".mlp.\1_proj.weight")

# With an actual tensor transform (transpose, permute, concat, etc.).
WeightConverter(
source_patterns=r"\.attn_q\.weight",
target_patterns=".self_attn.q_proj.weight",
operations=[ReversePermuteAttnQ()],
)
```

`WeightRenaming`s are applied **sequentially** (each one operates on the previous rule's output), so structural prefix renames (`^blk\.` → `model.layers.`, etc.) should come first and per-tensor renames after. `WeightConverter`s are evaluated after all renames; the first one whose source pattern matches is selected.

`GGUFDequantize` is automatically prepended to every `WeightConverter`'s op chain by `GGUFQuantizer.update_weight_conversions` — same pattern as `Fp8Dequantize` for FP8. You don't need to add it manually.

### Existing transform ops

These live in [`src/transformers/gguf_conversion_ops.py`](https://github.com/huggingface/transformers/blob/main/src/transformers/gguf_conversion_ops.py) and are reusable:

| Op | Used for |
|----|----------|
| `ReversePermuteAttnQ`, `ReversePermuteAttnK` | Llama-family rope-permuted Q/K weights |
| `SubtractOne` | Gemma / Nemotron norm weights (stored as `weight + 1` in GGUF) |
| `Unsqueeze(dim)` | Mamba conv1d, LFM2 shortconv — add a singleton dim |
| `LogNegate` | Mamba SSM-A: `log(-x)` on load |
| `BloomReshapeQKVWeight`, `BloomReshapeQKVBias` | Bloom interleaved QKV |
| `Concatenate(dim)` (from `core_model_loading`) | MoE gate+up merge into `gate_up_proj` |
| `Transpose` (from `core_model_loading`) | GPT-2 transposed `c_attn` / `c_proj` / `c_fc` |

If your architecture needs a new tensor transform, add it to `gguf_conversion_ops.py` as a `ConversionOps` subclass with `convert(input_dict, source_patterns, target_patterns, **kwargs)` and a `reverse_op` property.

### Worked example: adding a new Llama-like architecture

For a model whose HF `model_type` is `"my_llama"` and which uses the standard rope-permuted Q/K, plain Llama norms, and `mlp.gate_proj` / `up_proj` / `down_proj`, the rule list is already provided as `_LLAMA_CONVERTERS`. You only need:

```python
# src/transformers/modeling_gguf_pytorch_utils.py
_GGUF_ARCH_CONVERTERS = {
...
"my_llama": _LLAMA_CONVERTERS,
}
```

For a variant with `q_norm` / `k_norm` and attn biases (StableLM style) you compose:

```python
_MY_LLAMA_CONVERTERS = _LLAMA_CONVERTERS + [
WeightRenaming(r"\.attn_(q|k)_norm\.weight", r".self_attn.\1_norm.weight"),
WeightRenaming(r"\.attn_(q|k|v)\.bias", r".self_attn.\1_proj.bias"),
]
```

For a wholly new layout, start from one of the existing entries (`_BLOOM_CONVERTERS`, `_GPT2_CONVERTERS`, `_T5_CONVERTERS`, …) and adapt the renames + add any required `WeightConverter` ops.

### Dequantization performance

The actual byte-level dequant happens inside the `GGUFDequantize` op via [`integrations/gguf_dequant.py`](https://github.com/huggingface/transformers/blob/main/src/transformers/integrations/gguf_dequant.py) — a pure-PyTorch port of [city96/ComfyUI-GGUF](https://github.com/city96/ComfyUI-GGUF) (same kernels diffusers uses). It supports Q4_0/Q4_1, Q5_0/Q5_1, Q8_0, Q2_K…Q6_K, IQ4_NL, IQ4_XS, BF16, F16, F32 and runs on CPU or GPU/MPS unchanged — the loader moves the raw uint8 bytes to the target device first, then the kernel produces the float tensor on-device.

If you add a new quant type, add the corresponding `_dq_<TYPE>` kernel to `gguf_dequant.py` and register it in `_build_dispatch()`.
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"pydantic>=2",
"pytest>=7.2.0,<9.0.0",
"pytest-asyncio>=1.2.0",
"pytest-benchmark",
"pytest-random-order",
"pytest-rerunfailures<16.0",
"pytest-timeout",
Expand Down Expand Up @@ -218,6 +219,7 @@ def deps_list(*pkgs):
deps_list(
"pytest",
"pytest-asyncio",
"pytest-benchmark",
Comment on lines 221 to +222

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment, probably not needed

"pytest-random-order",
"pytest-rich",
"pytest-xdist",
Expand Down
40 changes: 37 additions & 3 deletions src/transformers/core_model_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ def convert(
all_tensors.extend(tensors)
else:
all_tensors.append(tensors)
return {target_pattern: torch.cat(all_tensors, dim=self.dim)}
result = torch.cat(all_tensors, dim=self.dim)
# Release source refs eagerly so accelerator caching allocators (MPS/CUDA) can pool/reclaim
# buffers immediately instead of waiting for the caller to drop the input_dict.
all_tensors.clear()
input_dict.clear()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm interesting, does this not imply that our post cleanups were never enough? Imo, it might be suitable to have something similar to all ops at some point

return {target_pattern: result}

def get_target_pattern(self, target_patterns: list[str]) -> str:
# Here we always return the target pattern
Expand Down Expand Up @@ -198,9 +203,22 @@ def convert(
**kwargs,
) -> dict[str, torch.Tensor]:
merged: dict[str, torch.Tensor] = {}
for source_pattern, tensors in input_dict.items():
target_pattern = self.get_target_pattern(input_dict, source_pattern, target_patterns)
# Snapshot keys so we can pop entries from `input_dict` as we consume them — this releases
# per-expert source tensors before the next stack runs, halving peak memory on MoE fuses
# (critical on MPS where MTLBuffers stay live until refcounts drop).
original_len = len(input_dict)
for source_pattern in list(input_dict.keys()):
tensors = input_dict.pop(source_pattern)
if original_len == 1:
if len(target_patterns) == 1:
target_pattern = target_patterns[0]
else:
raise ValueError("Undefined Operation encountered!")
else:
target_pattern = source_pattern
Comment on lines +212 to +218

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's either call self.get_target_pattern here, or if impossible slightly modify it to be able to call it - this way we keep the conventions of all others Ops

merged[target_pattern] = torch.stack(tensors, dim=self.dim)
tensors.clear()
del tensors

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here the del is needed extra? why tho?

return merged

def get_target_pattern(self, input_dict: dict, source_pattern: str, target_patterns: list[str]) -> str:
Expand Down Expand Up @@ -461,6 +479,8 @@ def convert(

for k, v in split_and_fused.items():
split_and_fused[k] = torch.cat(v, dim=self.concat_dim)
# Eager release of per-source tensor lists once the fused outputs are built.
input_dict.clear()

return split_and_fused

Expand Down Expand Up @@ -1365,6 +1385,14 @@ def convert_and_load_state_dict_in_model(
else:
mapping = param_name_to_load.setdefault(renamed_key, WeightRenaming(original_key, renamed_key))
source_pattern = original_key
# Pre-quantized loaders (e.g. GGUF) need a dequant op on the rename path:
# ``WeightRenaming.convert`` returns the raw quantized tensor directly under
# the renamed key, and ``set_param_for_module`` skips the shape check when a
# quantizer is active. Without an explicit dequant op the layer would land in
# the model as raw bytes cast to fp32 with the wrong (byte-) shape.
renaming_dequant = getattr(hf_quantizer, "renaming_dequantize_op", None)
if renaming_dequant is not None and mapping.quantization_operation is None:
mapping.quantization_operation = renaming_dequant

# 3. Handle dtype casting
needs_quantization = (
Expand Down Expand Up @@ -1435,6 +1463,10 @@ def convert_and_load_state_dict_in_model(
else:
loading_info.unexpected_keys.add(renamed_key)

# When loading onto MPS, periodically drain the MPS allocator pool back to the system so
# buffers from completed converter stages don't accumulate and push us into swap. CUDA's pool
# is already pressure-aware; MPS's is not.
mps_target = torch.backends.mps.is_available() and any(str(d).startswith("mps") for d in device_map.values())
try:
for first_param_name, mapping in tqdm(param_name_to_load.items(), desc="Loading weights"):
try:
Expand Down Expand Up @@ -1465,6 +1497,8 @@ def convert_and_load_state_dict_in_model(

# Cleanup all the tensors that were gathered before next iteration
del realized_value
if mps_target:
torch.mps.empty_cache()
Comment on lines +1500 to +1501

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed if the issue was the dicts inside the ConversionOps not being freed up immediately. In general this is super inefficient to call it, as we then have to reallocate memory all the time to the torch caching allocator


except SkipParameters:
continue
Expand Down
1 change: 1 addition & 0 deletions src/transformers/dependency_versions_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"pydantic": "pydantic>=2",
"pytest": "pytest>=7.2.0,<9.0.0",
"pytest-asyncio": "pytest-asyncio>=1.2.0",
"pytest-benchmark": "pytest-benchmark",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected to add the dependency here?

"pytest-random-order": "pytest-random-order",
"pytest-rerunfailures": "pytest-rerunfailures<16.0",
"pytest-timeout": "pytest-timeout",
Expand Down
Loading
Loading