Crosswalk

hf_config_to_gguf

HF checkpoints keep hyperparameters in config.json (+ tokenizer.json / tokenizer_config.json / generation_config.json). GGUF keeps hyperparameters as typed KV inside the file. "{arch}" = general.architecture (e.g. llama, qwen2, gemma3). The converter tries the HF keys IN ORDER and takes the first one present. GGUF types are those gguf-py writes (u32 for counts, f32 for floats).

gguftypehfnotes
general.architecturestringarchitectures[0] -> converter registry namee.g. LlamaForCausalLM -> llama
general.namestring_name_or_path / model directory name
general.file_typeu32llama_ftype of the dominant tensor type
general.quantization_versionu32always written by convert_hf_to_gguf.py, GGML_QUANT_VERSION = 2
{arch}.block_countu32n_layers num_hidden_layers n_layer num_layers
{arch}.context_lengthu32max_position_embeddings n_ctx n_positions max_length max_sequence_length model_max_length
{arch}.embedding_lengthu32hidden_size n_embd dim
{arch}.feed_forward_lengthu32prefix_dense_intermediate_size dense_intermediate_size intermediate_size n_inner hidden_dim
{arch}.attention.head_countu32num_attention_heads n_head n_heads
{arch}.attention.head_count_kvu32num_key_value_heads n_kv_headsabsent -> equals head_count (MHA)
{arch}.attention.key_length value_lengthu32head_dimwritten when head_dim is given explicitly
{arch}.rope.freq_basef32global_rope_theta rope_global_theta rope_theta_global rope_theta rotary_emb_base
{arch}.rope.dimension_countu32head_dim or hidden_size // num_attention_heads (x partial_rotary_factor)
{arch}.rope.scaling.type .factor .original_context_length .yarn_*string f32 u32rope_scaling{rope_type|type, factor, original_max_position_embeddings, ...}
{arch}.attention.layer_norm_rms_epsilonf32rms_norm_eps norm_eps
{arch}.attention.layer_norm_epsilonf32layer_norm_eps layer_norm_epsilon norm_epsilon
{arch}.expert_countu32num_local_experts num_experts n_routed_experts
{arch}.expert_used_countu32num_experts_per_tok num_experts_per_token top_k_experts
{arch}.vocab_sizeu32vocab_sizewritten by some converters (e.g. llama)
{arch}.attention.sliding_windowu32sliding_window
tokenizer.ggml.modelstringset by the converter class's set_vocab(): tokenizer.model (SentencePiece) -> llama; BPE tokenizer.json -> gpt2; WordPiece -> bert; T5 Unigram -> t5 (also rwkv, none, and others for specific models)
tokenizer.ggml.prestringconverter detects the pre-tokenizer by hashing the tokenizer's output (get_vocab_base_pre)
tokenizer.ggml.tokens scores token_type mergesarray[string] array[f32] array[i32] array[string]tokenizer.json vocab, merges; added_tokens -> token_type CONTROL/USER_DEFINED
tokenizer.ggml.bos_token_id eos_token_id unknown_token_id padding_token_id seperator_token_idu32tokenizer_config.json / special_tokens_map.json / config.json *_token_id'seperator' (sic) spelling is the real key
tokenizer.ggml.add_bos_token add_eos_tokenbooltokenizer_config.add_bos_token add_eos_token
tokenizer.chat_templatestringtokenizer_config.chat_template chat_template.jinja

hf_tensor_names_to_gguf

gguf-py tensor_mapping.py (TensorNameMap) maps per-architecture HF names to GGUF names; '.weight'/'.bias' suffix kept. LLaMA-family examples:

hfggufnotes
model.embed_tokens.weighttoken_embd.weight
model.norm.weightoutput_norm.weight
lm_head.weightoutput.weightabsent when tie_word_embeddings: GGUF then has no output.weight and the runtime reuses token_embd
model.layers.{N}.input_layernorm.weightblk.{N}.attn_norm.weight
model.layers.{N}.self_attn.q_proj.weightblk.{N}.attn_q.weightrows PERMUTED for llama-family
model.layers.{N}.self_attn.k_proj.weightblk.{N}.attn_k.weightpermuted with n_head_kv
model.layers.{N}.self_attn.v_proj.weightblk.{N}.attn_v.weight
model.layers.{N}.self_attn.o_proj.weightblk.{N}.attn_output.weight
model.layers.{N}.post_attention_layernorm.weightblk.{N}.ffn_norm.weight
model.layers.{N}.mlp.gate_proj.weightblk.{N}.ffn_gate.weight
model.layers.{N}.mlp.up_proj.weightblk.{N}.ffn_up.weight
model.layers.{N}.mlp.down_proj.weightblk.{N}.ffn_down.weight
model.layers.{N}.self_attn.rotary_emb.inv_freq(dropped)recomputed at runtime

6. LLaMA Q/K row permutation (HF <-> GGUF)

HF LLaMA-family checkpoints store q_proj/k_proj rows in the "rotate-half" RoPE layout. llama.cpp expects interleaved pairs. convert_hf_to_gguf.py (LlamaModel.permute) applies:

def permute(w, n_head, n_head_kv):          # w: [rows, cols] numpy, canonical order
    if n_head_kv is not None and n_head != n_head_kv:
        n_head = n_head_kv                   # used for k_proj
    return (w.reshape(n_head, 2, w.shape[0] // n_head // 2, *w.shape[1:])
             .swapaxes(1, 2).reshape(w.shape))
def unpermute(w, n_head, n_head_kv):         # inverse (GGUF -> HF)
    if n_head_kv is not None and n_head != n_head_kv: n_head = n_head_kv
    return (w.reshape(n_head, w.shape[0] // n_head // 2, 2, *w.shape[1:])
             .swapaxes(1, 2).reshape(w.shape))

q uses (n_head, n_head), k uses (n_head, n_head_kv). The permutation only reorders whole rows (axis 0). ggml blocks never span rows, so it can also be applied to already-quantized tensors by moving each row's block bytes as a unit. Other architectures have their own transforms (e.g. fused QKV split, norm +1 offsets in Gemma, exp of A_log in Mamba). Check the modify_tensors method of the specific converter class.

Implement it per architecture and test against llama.cpp output. Do not try to derive it generically.

gguf_to_hf_config

The reverse (GGUF -> HF, e.g. transformers' gguf loading in integrations/ggml.py) maps the same keys back. Lossy points: GGUF does not store torch_dtype, architectures class names, generation_config, or per-architecture extras that the converter dropped. Tokenizers must be rebuilt from tokenizer.ggml.* (merges/scores), which is not always byte-identical to the original tokenizer.json.

safetensors_metadata_to_gguf

safetensors __metadata__ is string->string (typically only {"format": "pt"}). Copy it, if needed, to GGUF keys under a private prefix (e.g. "safetensors.metadata.<key>" as STRING). Reverse: GGUF typed KV -> safetensors __metadata__ by JSON-encoding each value with a type tag, e.g. "gguf.<key>": "{\"type\":\"UINT32\",\"value\":4096}". Large arrays (tokenizer vocab) usually do not belong in safetensors metadata; write them to tokenizer.json instead (header limit 100 MB).

pytorch_to_safetensors

state_dict keys map 1:1. Tied/shared tensors must be de-duplicated (safetensors refuses aliasing); HF save_pretrained drops the tied copy and records tie_word_embeddings in config.json.

mlx_lm_quantization

mlx-lm stores quantization beside the weights, not inside the safetensors header. save_config copies quantization onto quantization_config, sorts keys, and tags non-finite floats as {"__float__": "Infinity"|"-Infinity"|"NaN"}. Tensor names are the module path from tree_flatten plus a suffix. group_size, bits, and mode are Python attributes and are not tensors.

keytypenotes
quantization.group_sizeintaffine default 64; mxfp4 and mxfp8 32; nvfp4 16
quantization.bitsintaffine default 4; mxfp8 8
quantization.modestringaffine | mxfp4 | nvfp4 | mxfp8. Load treats a missing mode as affine.
quantization.<module path>false | {group_size, bits, mode, global_scale?}false leaves the layer dense. global_scale true records an nvfp4 per-expert scale tensor.
quantization_configobjectduplicate of quantization on save. On load, used only when quantization is absent; quant_method then selects a foreign importer (awq, gptq, mxfp4, mxfp8, bitnet, compressed-tensors, modelopt, fp8).
quantize_activationsbooloptional. Rebuilds QuantizedLinear as QQLinear.
suffixdtypeshapenotes
.weightuint32[..., K * bits / 32]packed codes. K is the logical input features.
.scalesw.dtype (affine) or uint8 (mxfp4, nvfp4, mxfp8)[..., K / group_size]
.biasessame as scalessame as scalesaffine only. fp modes omit it.
.biasoriginal floating dtypeoptional dense bias, not quantized
.global_scalefloat32optional nvfp4 tensor scale, one scalar or one per expert
keyvaluenotes
formatmlxwritten on every shard by save_model

mlx_gguf

What mx.load / mx.save_gguf and mlx_lm.gguf.convert_to_gguf do, on top of hf_config_to_gguf. Dimension order is reversed (ggml ne[0] innermost <-> MLX last axis).

gguf_typemlx
F32, F16, I8, I16, I32same dtype, shape reversed
Q4_0affine group_size 32 bits 4, f16 scales, biases = -8 * scale, nibble-repacked uint32
Q4_1affine group_size 32 bits 4, f16 scale and the block min as biases
Q8_0affine group_size 32 bits 8, f16 scales, biases = -128 * scale, codes = int8 XOR 0x80
BF16, Q2_K, Q4_K, Q6_Kone float16 tensor (gguflib dequant)
otherload throws
mlxgguf
F32, F16, I8, I16, I32same type, shape reversed to ne[]
bf16, uint*, i64, f64, complex64, packed uint32save_gguf throws

Refuses config.quantization. Hardcodes general.architecture = llama. Renames a llama-style subset (translate_weight_names in mlx_lm/gguf.py). Permutes q_proj and k_proj with the section-6 permute. Casts bfloat16 to float16 via float32, and any tensor whose name contains 'norm' to float32. general.file_type and general.quantization_version are both the integer 1, not GGML_QNT_VERSION.

Sources

Nick Reynolds · @goldnode · rev 2.5