Skip to content

Conversation

@jixiongdeng
Copy link
Contributor

@jixiongdeng jixiongdeng commented Nov 3, 2025

Problem

The current model builder doesn't support shared embeddings layers with 4bit qweights and 16bit float weights, which occupies more room in disk (unnecessary for originally tied embeddings models) and hurts compression rate for quantized models. builder.py doesn't provide flexible options to toggle the graph construction and quantization config, like unpacked/packed matmul, rtn, kquant, etc.

Solution

Calculated flat_dim in a more generic way on reshape node before GatherBlockQuantized (support 4bit and 8bit).
Added CUDA kernel support in ORT #26484.
Added more extra_options to enable different quant configs and pack options, and shared embeddings.

Running examples:
unpacked qkv_projs and shared 4 bit RTN on Llama3.2 1B Instruct:

python src/python/py/models/builder.py -m meta-llama/Llama-3.2-1B-Instruct -p int4 -e cuda -o export_model/llama32_1bi_rtn_4_4_unpacked_tied --extra_options int4_is_symmetric=false unpack_matmul=true int4_algo_config=rtn

shared 4 bit k_quant on Phi-4-Mini Instruct:

python src/python/py/models/builder.py -m microsoft/Phi-4-Mini-Instruct -p int4 -e cuda -o export_model/phi4mini_i_kquant_4_4_tied --extra_options int4_is_symmetric=false unpack_matmul=true int4_algo_config=k_quant

shared 16 bit float emb on Phi-4-Mini Instruct:

python src/python/py/models/builder.py -m microsoft/Phi-4-Mini-Instruct -p fp16 -e cuda -o export_model/phi4mini_i_fp16_tied --extra_options shared_embeddings=true

Changes

Modified Files

  • src/python/py/models/builder.py

Key Modifications

  1. Computed flat_dim in a generic manner before feeding in GatherBlockQuantized.
  2. Explicitly defined gather_axis and quantize_axis for clarity.
  3. Added unpack_matmul option to separate qvk_proj if needed.
  4. Added shared_embeddings option to tied embed_tokens/lm_head.
  5. Added rtn_last like k_quant_last as a new mixed precision option
  6. Added k_quant like rtn as a new 4 bit quantizer option

@jixiongdeng
Copy link
Contributor Author

@jixiongdeng please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree company="Microsoft"

@jixiongdeng jixiongdeng requested a review from jambayk November 3, 2025 23:27
)

# Allow extra_options to override use_packed_matmul
if "unpack_matmul" in extra_options:
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an optimization opportunity that should be auto-detected by the model builder. We should not need to give the responsibility to the user. You can see the review comments on this PR for more details.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an optimization opportunity that should be auto-detected by the model builder. We should not need to give the responsibility to the user. You can see the review comments on this PR for more details.

Thanks for the details. We attempt to deliver fine tuned weights on open sources models like llama3.2 and qwen3. These models are not originally with packed qkv_proj. We fine tuned these models with unpacked projs which might be better to deliver these weights with consistency of their original forms. The unpack option is False at default until we add it in extra_options. It would be great if we have this option for development.


elif quant_method in {"k_quant_mixed", "k_quant_last"}:
elif quant_method in {"k_quant", "k_quant_mixed", "k_quant_last"}:
from onnxruntime.quantization.matmul_nbits_quantizer import KQuantWeightOnlyQuantConfig
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move this import up. It was previously here because it was not part of a stable release.

from onnxruntime.quantization.matmul_nbits_quantizer import (
MatMulNBitsQuantizer,
QuantFormat,
RTNWeightOnlyQuantConfig,
)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's move this import up. It was previously here because it was not part of a stable release.

from onnxruntime.quantization.matmul_nbits_quantizer import (
MatMulNBitsQuantizer,
QuantFormat,
RTNWeightOnlyQuantConfig,
)

The current build checks are still using ort 1.22 which will block this PR if I move the import on the top. Would be better to change it after updating check tests.


if quant_method == "rtn":
int4_algo_config = RTNWeightOnlyQuantConfig()
if quant_method in {"rtn", "rtn_last"}:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be simplified to the following.

if quant_method in {"rtn", "rtn_last"}:
    if quant_method == "rtn_last":
        customized_weight_config["/lm_head/MatMul"] = {"bits": 8}
    int4_algo_config = RTNWeightOnlyQuantConfig(customized_weight_config=customized_weight_config)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this can be simplified to the following.

if quant_method in {"rtn", "rtn_last"}:
    if quant_method == "rtn_last":
        customized_weight_config["/lm_head/MatMul"] = {"bits": 8}
    int4_algo_config = RTNWeightOnlyQuantConfig(customized_weight_config=customized_weight_config)

Done.

int4_algo_config = RTNWeightOnlyQuantConfig(customized_weight_config=customized_weight_config)

elif quant_method in {"k_quant_mixed", "k_quant_last"}:
elif quant_method in {"k_quant", "k_quant_mixed", "k_quant_last"}:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be simplified to the following.

elif quant_method in {"k_quant", "k_quant_mixed", "k_quant_last"}:
    if quant_method != "k_quant":
        customized_weight_config["/lm_head/MatMul"] = {"bits": 8}

    if quant_method == "k_quant_mixed":
        # k_quant_mixed is from llama.cpp.
        # Reference: https://github.com/ggml-org/llama.cpp/blob/36667c8edcded08063ed51c7d57e9e086bbfc903/src/llama-quant.cpp#L136
        # We also consider some MatMuls are more senstive to quantization than other MatMuls.
        layers_to_exclude = [
            i
            for i in range(self.num_layers)
            if i < self.num_layers / 8 or i >= 7 * self.num_layers / 8 or (i - (round)(self.num_layers / 8)) % 3 == 2
        ]
        for i in layers_to_exclude:
            customized_weight_config["/model/layers." + str(i) + "/attn/qkv_proj/MatMul"] = {"bits": 8}
            customized_weight_config["/model/layers." + str(i) + "/attn/v_proj/MatMul"] = {"bits": 8}
            customized_weight_config["/model/layers." + str(i) + "/mlp/down_proj/MatMul"] = {"bits": 8}

    int4_algo_config = KQuantWeightOnlyQuantConfig(customized_weight_config=customized_weight_config)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this can be simplified to the following.

elif quant_method in {"k_quant", "k_quant_mixed", "k_quant_last"}:
    if quant_method != "k_quant":
        customized_weight_config["/lm_head/MatMul"] = {"bits": 8}

    if quant_method == "k_quant_mixed":
        # k_quant_mixed is from llama.cpp.
        # Reference: https://github.com/ggml-org/llama.cpp/blob/36667c8edcded08063ed51c7d57e9e086bbfc903/src/llama-quant.cpp#L136
        # We also consider some MatMuls are more senstive to quantization than other MatMuls.
        layers_to_exclude = [
            i
            for i in range(self.num_layers)
            if i < self.num_layers / 8 or i >= 7 * self.num_layers / 8 or (i - (round)(self.num_layers / 8)) % 3 == 2
        ]
        for i in layers_to_exclude:
            customized_weight_config["/model/layers." + str(i) + "/attn/qkv_proj/MatMul"] = {"bits": 8}
            customized_weight_config["/model/layers." + str(i) + "/attn/v_proj/MatMul"] = {"bits": 8}
            customized_weight_config["/model/layers." + str(i) + "/mlp/down_proj/MatMul"] = {"bits": 8}

    int4_algo_config = KQuantWeightOnlyQuantConfig(customized_weight_config=customized_weight_config)

Done.

self.int8_lm_head = extra_options.get("int4_algo_config", "default") in {"k_quant_mixed", "k_quant_last"}
if not self.int8_lm_head:
self.int8_lm_head = extra_options.get("int4_algo_config", "default") in {"k_quant_mixed", "k_quant_last", "rtn_last"}
if not self.int8_lm_head and extra_options.get("int4_algo_config", "default") not in {"rtn", "k_quant"}:
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we rewrite the above section and the if condition to just match on the conditions needed for tied embeddings to be true and otherwise set it to false?

Something like this:

self.int8_lm_head = extra_options.get("int4_algo_config", "default") in {"k_quant_mixed", "k_quant_last", "rtn_last"}
self.int4_tied_embeddings = extra_options.get("int4_tied_embeddings", config.tie_word_embeddings if hasattr(config, "tie_word_embeddings") and config.tie_word_embeddings is not None else False)

# matmul_nbits_quantizer.py has a different naming for default quantization, so lm_head.MatMul.weight_Q{}G{} does not match.
# tied_embeddings lm_head.MatMul.weight_Q{}G{} only works with rtn&k_quant on 4bit
self.int4_tied_embeddings = <boolean expression>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can we rewrite the above section and the if condition to just match on the conditions needed for tied embeddings to be true and otherwise set it to false?

Something like this:

self.int8_lm_head = extra_options.get("int4_algo_config", "default") in {"k_quant_mixed", "k_quant_last", "rtn_last"}
self.int4_tied_embeddings = extra_options.get("int4_tied_embeddings", config.tie_word_embeddings if hasattr(config, "tie_word_embeddings") and config.tie_word_embeddings is not None else False)

# matmul_nbits_quantizer.py has a different naming for default quantization, so lm_head.MatMul.weight_Q{}G{} does not match.
# tied_embeddings lm_head.MatMul.weight_Q{}G{} only works with rtn&k_quant on 4bit
self.int4_tied_embeddings = <boolean expression>

Done.

@kunal-vaishnavi
Copy link
Contributor

Can you update the options for int4_algo_config here and add their descriptions?

int4_algo_config = Method for int4 quantization. Default is 'default'.
Currently supported options are: 'default', 'rtn', 'k_quant_mixed', 'k_quant_last'.
k_quant_mixed = k_quant algorithm with mixed precision (int4 + int8).
k_quant_last = k_quant algorithm where only the last MatMul (/lm_head/MatMul) is quantized as int8. Other MatMuls are quantized as int4.

@jixiongdeng
Copy link
Contributor Author

@kunal-vaishnavi Thanks for the review! I edited codes and adapted to most of your comments.
The unpack_matmul option relates to my other project which involves weights replacements.
i.e. Llama3.2 series models originally have q_proj, k_proj, v_proj separately . It would be great, if builder.py can provide an easy option to keep consistent with torch models' projs.

@jixiongdeng
Copy link
Contributor Author

jixiongdeng commented Nov 12, 2025

Added an option to create shared emb_tokens/lm_head for float matmul node.
This would benefit small models with smaller size, especially for low-bit quantized models.
i.e. phi4mini fp16: (8.3G -> 7.2G) ~ 13.2% smaller
phitmini int4 + fp16 emb_tokens/lm_head: (4.1G -> 2.9G) ~ 29.3% smaller

@jixiongdeng jixiongdeng changed the title Shared emb_tokens/lm_head on nibbled 4bit qweights Shared emb_tokens/lm_head on fp16 & uint4 weights Nov 13, 2025
@jixiongdeng
Copy link
Contributor Author

Can you update the options for int4_algo_config here and add their descriptions?

int4_algo_config = Method for int4 quantization. Default is 'default'.
Currently supported options are: 'default', 'rtn', 'k_quant_mixed', 'k_quant_last'.
k_quant_mixed = k_quant algorithm with mixed precision (int4 + int8).
k_quant_last = k_quant algorithm where only the last MatMul (/lm_head/MatMul) is quantized as int8. Other MatMuls are quantized as int4.

Added. Done.

lm_head_excluded = "/lm_head/MatMul" in self.quant_attrs["int4"]["nodes_to_exclude"]

self.int4_tied_embeddings = extra_options.get("int4_tied_embeddings", config.tie_word_embeddings if hasattr(config, "tie_word_embeddings") and config.tie_word_embeddings is not None else False)
self.int8_lm_head = extra_options.get("int4_algo_config", "default") in {"k_quant_mixed", "k_quant_last", "rtn_last"}
Copy link
Contributor

@tianleiwu tianleiwu Nov 17, 2025

Choose a reason for hiding this comment

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

why int4_algo_config is used to set int8_lm_head here? Our model support different bits for different weights. I thought that it is better to have straight forward setting like weight name to n_bits, or a configuration for lm_head.

k_quant_last = k_quant algorithm where only the last MatMul (/lm_head/MatMul) is quantized as int8. Other MatMuls are quantized as int4.
int4_tied_embeddings = Enable weight sharing for quantization. Default is false.
Use this option when you want to share the weights in the embedding and unembedding.
int4_tied_embeddings = Enable weight sharing for quantized models (INT4/UINT4/INT8/UINT8). Default is false.
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this option?

If we shared embeddings (lm_head), that also means the quantized weights shall be shared.

As long as we know quantization method and number of bits, that will be enough.

@jixiongdeng
Copy link
Contributor Author

Closed this PR due to refactor of model builder.
Avoid massive unnecessary rebase.
New PR at: #1885

@jixiongdeng jixiongdeng deleted the jdeng/shared_4bit_emb branch November 20, 2025 02:42
kunal-vaishnavi pushed a commit that referenced this pull request Nov 26, 2025
## Problem
This is a refactored PR from [previous shared emb
PR](#1854).

The current model builder doesn't support shared embeddings layers with
4bit qweights and 16bit float weights, which occupies more room in disk
(unnecessary for originally tied embeddings models) and hurts
compression rate for quantized models. builder.py doesn't provide
flexible options to toggle the graph construction and quantization
config, like rtn, kquant, etc.

## Solution

Calculated flat_dim in a more generic way on reshape node before
`GatherBlockQuantized` (support 4bit and 8bit).
Added CUDA kernel support in ORT
[#26484](microsoft/onnxruntime#26484).
Added more extra_options to enable different quant configs and pack
options, and shared embeddings.

Running examples:

**shared 4 bit k_quant on Phi-4-Mini Instruct**:
```
python src/python/py/models/builder.py -m microsoft/Phi-4-Mini-Instruct -p int4 -e cuda -o export_model/phi4mini_i_kquant_4_4_tied --extra_options int4_is_symmetric=false int4_algo_config=k_quant
```

**shared 16 bit float emb on Phi-4-Mini Instruct**:
```
python src/python/py/models/builder.py -m microsoft/Phi-4-Mini-Instruct -p fp16 -e cuda -o export_model/phi4mini_i_fp16_tied --extra_options shared_embeddings=true
```

## Changes

### Modified Files
- `src/python/py/models/builder.py`
- `src/python/py/models/builders/base.py`
- `src/python/py/models/README.MD`

### Key Modifications
1. Computed `flat_dim` in a generic manner before feeding in
`GatherBlockQuantized`.
2. Explicitly defined gather_axis and quantize_axis for clarity.
3. Added `shared_embeddings` option to tied embed_tokens/lm_head.
4. Added `rtn_last` like `k_quant_last` as a new mixed precision option
5. Added `k_quant` like `rtn` as a new 4 bit quantizer option
6. Removed `int4_tied_embeddings` and merged to `shared_embeddings`.
7. Added documents.
kunal-vaishnavi pushed a commit that referenced this pull request Dec 5, 2025
## Problem
This is a refactored PR from [previous shared emb
PR](#1854).

The current model builder doesn't support shared embeddings layers with
4bit qweights and 16bit float weights, which occupies more room in disk
(unnecessary for originally tied embeddings models) and hurts
compression rate for quantized models. builder.py doesn't provide
flexible options to toggle the graph construction and quantization
config, like rtn, kquant, etc.

## Solution

Calculated flat_dim in a more generic way on reshape node before
`GatherBlockQuantized` (support 4bit and 8bit).
Added CUDA kernel support in ORT
[#26484](microsoft/onnxruntime#26484).
Added more extra_options to enable different quant configs and pack
options, and shared embeddings.

Running examples:

**shared 4 bit k_quant on Phi-4-Mini Instruct**:
```
python src/python/py/models/builder.py -m microsoft/Phi-4-Mini-Instruct -p int4 -e cuda -o export_model/phi4mini_i_kquant_4_4_tied --extra_options int4_is_symmetric=false int4_algo_config=k_quant
```

**shared 16 bit float emb on Phi-4-Mini Instruct**:
```
python src/python/py/models/builder.py -m microsoft/Phi-4-Mini-Instruct -p fp16 -e cuda -o export_model/phi4mini_i_fp16_tied --extra_options shared_embeddings=true
```

## Changes

### Modified Files
- `src/python/py/models/builder.py`
- `src/python/py/models/builders/base.py`
- `src/python/py/models/README.MD`

### Key Modifications
1. Computed `flat_dim` in a generic manner before feeding in
`GatherBlockQuantized`.
2. Explicitly defined gather_axis and quantize_axis for clarity.
3. Added `shared_embeddings` option to tied embed_tokens/lm_head.
4. Added `rtn_last` like `k_quant_last` as a new mixed precision option
5. Added `k_quant` like `rtn` as a new 4 bit quantizer option
6. Removed `int4_tied_embeddings` and merged to `shared_embeddings`.
7. Added documents.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants