Skip to content

support qwen3 awq - #1951

Merged
jordimas merged 1 commit into
OpenNMT:masterfrom
BBC-Esq:support_qwen3_awq
Dec 21, 2025
Merged

jordimas merged 1 commit into
OpenNMT:masterfrom
BBC-Esq:support_qwen3_awq

Conversation

@BBC-Esq

@BBC-Esq BBC-Esq commented Dec 16, 2025 •

Copy link
Copy Markdown
Contributor

This will support running a Qwen3 AWQ model that has been converted into the ctranslate2 format.

Just FYI, currently the autoawq Github repository is archived as of May 11, 2025. The "vllm" project has expanded upon it in their llm-compressor library, however, it doesn't output an AWQ model in a format that can be converted to the ctranslate2 format. Thus, I was forced to patch the original autoawq repository to perform the conversions of qwen3.

TLDR - only the models here https://huggingface.co/collections/ctranslate2-4you/qwen3 will work unless/until someone continues with the original autoawq package.

Alternate uploads here:
https://huggingface.co/collections/CTranslate2HQ/ctranslate2-qwen3

The best way to test this before approving the PR would be to simply copy and paste my modified Qwen3Loader into transformers.py in an already pip installed version of ctranslate2, but here it is again for convenience:

MODIFIED QWEN3LOADER HERE
@register_loader("Qwen3Config")
class Qwen3Loader(ModelLoader):
    @property
    def architecture_name(self):
        return "Qwen3ForCausalLM"

    def get_model_spec(self, model):
        num_layers = model.config.num_hidden_layers
        num_heads = model.config.num_attention_heads
        num_heads_kv = getattr(model.config, "num_key_value_heads", num_heads)
        head_dim = getattr(
            model.config, "head_dim", model.config.hidden_size // num_heads
        )

        if num_heads_kv == num_heads:
            num_heads_kv = None

        rope_scaling = getattr(model.config, "rope_scaling", None)
        if rope_scaling:
            rope_type = rope_scaling.get("type") or rope_scaling["rope_type"]
            rotary_scaling_type = _SUPPORTED_ROPE_SCALING.get(rope_type)
            rotary_scaling_factor = rope_scaling["factor"]
            if rotary_scaling_type is None:
                raise NotImplementedError(
                    "RoPE scaling type '%s' is not yet implemented. "
                    "The following RoPE scaling types are currently supported: %s"
                    % (rope_scaling["type"], ", ".join(_SUPPORTED_ROPE_SCALING.keys()))
                )
        else:
            rotary_scaling_type = None
            rotary_scaling_factor = 1

        # Check for AWQ quantization config
        quantization_config = getattr(model.config, "quantization_config", None)
        if quantization_config:
            quant_type = None
            if quantization_config.quant_method == "awq":
                quant_type = _SUPPORTED_QUANTIZATION.get(quantization_config.version)
            if quant_type is None:
                raise NotImplementedError(
                    "Quantization type '%s' is not yet implemented. "
                    "The following Quantization types are currently supported: %s"
                    % (
                        quantization_config.quant_method,
                        ", ".join(_SUPPORTED_QUANTIZATION.keys()),
                    )
                )
            quant_group_size = quantization_config.group_size
            quant_bits = quantization_config.bits
        else:
            quant_type = common_spec.Quantization.CT2
            quant_group_size = None
            quant_bits = None

        spec = transformer_spec.TransformerDecoderModelSpec.from_config(
            num_layers,
            num_heads,
            activation=common_spec.Activation.SWISH,
            pre_norm=True,
            ffn_glu=True,
            rms_norm=True,
            rotary_dim=model.config.head_dim,
            rotary_interleave=False,
            rotary_scaling_type=rotary_scaling_type,
            rotary_scaling_factor=rotary_scaling_factor,
            rotary_base=getattr(model.config, "rope_theta", 10000),
            num_heads_kv=num_heads_kv,
            head_dim=head_dim,
            qk_norm=True,
            quant_type=quant_type,
            quant_group_size=quant_group_size,
            quant_bits=quant_bits,
        )

        self.set_decoder(spec.decoder, model.model, quant_type)
        self.set_linear(spec.decoder.projection, model.lm_head)
        return spec

    def get_vocabulary(self, model, tokenizer):
        tokens = super().get_vocabulary(model, tokenizer)
        extra_ids = model.config.vocab_size - len(tokens)
        for i in range(extra_ids):
            tokens.append("<extra_id_%d>" % i)
        return tokens

    def set_vocabulary(self, spec, tokens):
        spec.register_vocabulary(tokens)

    def set_config(self, config, model, tokenizer):
        config.bos_token = (
            tokenizer.bos_token
            if tokenizer.bos_token is not None
            else tokenizer.pad_token
        )
        config.eos_token = tokenizer.eos_token
        config.unk_token = (
            tokenizer.unk_token if tokenizer.unk_token is not None else ""
        )
        config.layer_norm_epsilon = model.config.rms_norm_eps

    def set_layer_norm(self, spec, layer_norm):
        spec.gamma = layer_norm.weight

    def set_decoder(self, spec, module, quant_type=common_spec.Quantization.CT2):
        spec.scale_embeddings = False
        self.set_embeddings(spec.embeddings, module.embed_tokens)
        self.set_layer_norm(spec.layer_norm, module.norm)

        for layer_idx, (layer_spec, layer) in enumerate(zip(spec.layer, module.layers)):
            self.set_layer_norm(
                layer_spec.self_attention.layer_norm, layer.input_layernorm
            )
            self.set_layer_norm(
                layer_spec.ffn.layer_norm, layer.post_attention_layernorm
            )

            self.set_layer_norm(
                layer_spec.self_attention.q_norm, layer.self_attn.q_norm
            )
            self.set_layer_norm(
                layer_spec.self_attention.k_norm, layer.self_attn.k_norm
            )

            split_layers = [common_spec.LinearSpec() for _ in range(3)]
            self.set_linear(
                split_layers[0], layer.self_attn.q_proj, quant_type=quant_type
            )
            self.set_linear(
                split_layers[1], layer.self_attn.k_proj, quant_type=quant_type
            )
            self.set_linear(
                split_layers[2], layer.self_attn.v_proj, quant_type=quant_type
            )

            if quant_type == common_spec.Quantization.CT2:
                utils.fuse_linear(layer_spec.self_attention.linear[0], split_layers)
            else:
                cc_dim = 1 if quant_type == common_spec.Quantization.AWQ_GEMM else 0
                utils.fuse_linear_prequant(
                    layer_spec.self_attention.linear[0], split_layers, cc_dim
                )

            self.set_linear(
                layer_spec.self_attention.linear[1],
                layer.self_attn.o_proj,
                quant_type=quant_type,
            )

            self.set_linear(
                layer_spec.ffn.linear_0, layer.mlp.gate_proj, quant_type=quant_type
            )
            self.set_linear(
                layer_spec.ffn.linear_0_noact, layer.mlp.up_proj, quant_type=quant_type
            )
            self.set_linear(
                layer_spec.ffn.linear_1, layer.mlp.down_proj, quant_type=quant_type
            )

            delattr(layer, "self_attn")
            delattr(layer, "mlp")
            gc.collect()

@BBC-Esq BBC-Esq mentioned this pull request Dec 16, 2025
@jordimas

Copy link
Copy Markdown
Collaborator

Have you test it with https://huggingface.co/Qwen/Qwen3-32B-AWQ?

The CI/CD builds wheels for PRs. For this PR: https://github.com/OpenNMT/CTranslate2/actions/runs/20275888121

I will expect this official model to work.

@BBC-Esq

BBC-Esq commented Dec 17, 2025

Copy link
Copy Markdown
Contributor Author

An AWQ model has to be converted to a ctranslate2 model I thought. But yes, I converted that model to ctranslate2 format and it works. Only takes up about 18.6 GB of VRAM as well...unless there's a really long context in which case it slowly climbs.

Might be a good time to bring back flash attention or I might just need to learn how to compile the library with it...All the models here work:

https://huggingface.co/collections/ctranslate2-4you/ctranslate2-qwen3

@winstxnhdw

winstxnhdw commented Dec 17, 2025 •

Copy link
Copy Markdown

I am surprised AWQ is able to convert to CT2 with no issues. Did you notice any artefacts in the output in your testing? Do the throughput improvements also carry over?

@BBC-Esq

BBC-Esq commented Dec 18, 2025

Copy link
Copy Markdown
Contributor Author

@winstxnhdw I had to modify the source code for autoawq because the repository was archived in May, but after that it worked perfectly - no artifacts.

@jordimas

Copy link
Copy Markdown
Collaborator

@BBC-Esq It will be great if you can publish your autoawq with the changes the I can test this PR end to end. Thanks

@BBC-Esq

BBC-Esq commented Dec 21, 2025

Copy link
Copy Markdown
Contributor Author

@jordimas Do you mean publish the pypi? I mean...here is my fork of autoawq if you wanted to pip install from the fork? I'm not very familiar with publishing to pypi...

https://github.com/BBC-Esq/AutoAWQ

@jordimas

Copy link
Copy Markdown
Collaborator

Hello @BBC-Esq I can install from the repo, no need to have the package publish. If you can share the exact instructions that you are following to convert the model to ct2 then I can produce them and test the PR. Thanks

@BBC-Esq

BBC-Esq commented Dec 21, 2025

Copy link
Copy Markdown
Contributor Author

I'm not using a specific command per se...Rather, I'm using this script because I hate constructing commands. 😄 Enjoy!

CONVERSION SCRIPT WITH GUI
import sys
import os
from pathlib import Path

def set_cuda_paths():
    venv_base = Path(sys.executable).parent.parent
    nvidia_base_path = venv_base / 'Lib' / 'site-packages' / 'nvidia'
    cuda_path_runtime = nvidia_base_path / 'cuda_runtime' / 'bin'
    cuda_path_runtime_lib = nvidia_base_path / 'cuda_runtime' / 'bin' / 'lib' / 'x64'
    cuda_path_runtime_include = nvidia_base_path / 'cuda_runtime' / 'include'
    cublas_path = nvidia_base_path / 'cublas' / 'bin'
    cudnn_path = nvidia_base_path / 'cudnn' / 'bin'
    nvrtc_path = nvidia_base_path / 'cuda_nvrtc' / 'bin'
    nvcc_path = nvidia_base_path / 'cuda_nvcc' / 'bin'

    paths_to_add = [
        str(cuda_path_runtime),
        str(cuda_path_runtime_lib),
        str(cuda_path_runtime_include),
        str(cublas_path),
        str(cudnn_path),
        str(nvrtc_path),
        str(nvcc_path),
    ]

    current_value = os.environ.get('PATH', '')
    new_value = os.pathsep.join(paths_to_add + [current_value] if current_value else paths_to_add)
    os.environ['PATH'] = new_value

    triton_cuda_path = nvidia_base_path / 'cuda_runtime'
    os.environ['CUDA_PATH'] = str(triton_cuda_path)

set_cuda_paths()

import traceback
import subprocess
from PySide6.QtWidgets import (
    QApplication, QWidget, QPushButton, QLabel, QFileDialog,
    QMessageBox, QVBoxLayout, QHBoxLayout, QCheckBox,
    QTextEdit, QStyleFactory, QComboBox,
)
from PySide6.QtCore import QThread, Signal

class ConversionThread(QThread):
    conversion_started  = Signal(str, str)
    conversion_finished = Signal(str, str)
    conversion_error    = Signal(str)

    def __init__(self, model_dir, selected_file, command, quantization, needs_renaming):
        super().__init__()
        self.model_dir = model_dir
        self.selected_file = selected_file
        self.command = command
        self.quantization = quantization
        self.needs_renaming = needs_renaming

    def run(self):
        self.conversion_started.emit(self.quantization, self.command)

        if not self.needs_renaming:
            self._run_conversion()
        else:
            self._run_with_renaming()

    def _run_conversion(self):
        try:
            proc = subprocess.run(
                self.command, shell=True,
                stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                text=True, env=os.environ.copy()
            )
            if proc.returncode == 0:
                self.conversion_finished.emit(self.quantization, proc.stdout)
            else:
                self.conversion_error.emit(
                    f"Conversion failed (code {proc.returncode}):\n{proc.stderr}"
                )
        except Exception:
            self.conversion_error.emit(
                "Exception during conversion:\n" + traceback.format_exc()
            )

    def _run_with_renaming(self):

        if self.selected_file.endswith('.bin'):
            expected_name = "pytorch_model.bin"
            temp_name = "temp_pytorch_model.bin"
        else:
            expected_name = "model.safetensors"
            temp_name = "temp_model.safetensors"
            
        original_path = os.path.join(self.model_dir, expected_name)
        temp_path = os.path.join(self.model_dir, temp_name)
        selected_path = os.path.join(self.model_dir, self.selected_file)

        try:
            if os.path.exists(original_path):
                os.rename(original_path, temp_path)

            os.rename(selected_path, original_path)

            self._run_conversion()

        except Exception:
            self.conversion_error.emit(
                "Exception during renaming/conversion:\n" + traceback.format_exc()
            )
        finally:
            try:
                if os.path.exists(original_path):
                    os.rename(original_path, selected_path)
                if os.path.exists(temp_path):
                    os.rename(temp_path, original_path)
            except Exception:
                pass

class App(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("chintellalaw.com - for non-commercial use")
        self.resize(800, 500)

        self.model_path = ""
        self.output_dir = ""
        self.current_thread = None
        self.conversion_queue = []
        self.needs_file_renaming = False

        layout = QVBoxLayout(self)

        row = QHBoxLayout()
        self.browse_btn = QPushButton("Browse Model Directory")
        self.browse_btn.clicked.connect(self.browse)
        row.addWidget(self.browse_btn)

        self.file_combo = QComboBox()
        self.file_combo.setEnabled(False)
        row.addWidget(self.file_combo)

        layout.addLayout(row)

        self.path_label = QLabel("")
        layout.addWidget(self.path_label)

        row = QHBoxLayout()
        self.output_btn = QPushButton("Select Output Directory")
        self.output_btn.clicked.connect(self.browse_output)
        row.addWidget(self.output_btn)

        self.output_label = QLabel("")
        row.addWidget(self.output_label)
        layout.addLayout(row)

        self.quant_opts = [
            "float32", "float16", "bfloat16",
            "int8_float32", "int8_float16", "int8_bfloat16", "int8"
        ]
        self.quant_boxes = {opt: QCheckBox(opt) for opt in self.quant_opts}
        row = QHBoxLayout()
        for cb in self.quant_boxes.values():
            row.addWidget(cb)
        self.awq_cb = QCheckBox("AWQ")
        row.addWidget(self.awq_cb)
        layout.addLayout(row)

        self.run_btn = QPushButton("Run Conversion")
        self.run_btn.clicked.connect(self.run_conversion)
        layout.addWidget(self.run_btn)

        self.log = QTextEdit()
        self.log.setReadOnly(True)
        layout.addWidget(self.log)

    def analyze_model_directory(self, path):
        try:
            files = os.listdir(path)
        except OSError:
            return "error", []

        sharded_pattern = [f for f in files if 
                          ('-of-' in f and f.endswith(('.bin', '.safetensors')))]

        single_model_files = [f for f in files if 
                             f.endswith(('.bin', '.safetensors')) and 
                             '-of-' not in f]

        if sharded_pattern:
            return "sharded", sharded_pattern
        elif len(single_model_files) > 1:
            return "multiple_variants", single_model_files
        elif len(single_model_files) == 1:
            return "single", single_model_files
        else:
            return "none", []

    def browse(self):
        path = QFileDialog.getExistingDirectory(self, "Select Model Directory")
        if not path:
            return

        self.model_path = path
        self.path_label.setText(f"Model: {path}")

        parent_path = str(Path(path).parent)
        self.output_dir = parent_path
        self.output_label.setText(f"Output: {parent_path}")

        model_type, files = self.analyze_model_directory(path)

        if model_type == "sharded":
            self.file_combo.clear()
            self.file_combo.addItem(f"Sharded model ({len(files)} files)")
            self.file_combo.setEnabled(False)
            self.needs_file_renaming = False
            self.append_log(f"✅ Detected sharded model with {len(files)} files")

        elif model_type == "multiple_variants":
            self.file_combo.clear()
            self.file_combo.addItems(files)
            if "pytorch_model.bin" in files:
                self.file_combo.setCurrentText("pytorch_model.bin")
            elif "model.safetensors" in files:
                self.file_combo.setCurrentText("model.safetensors")
            self.file_combo.setEnabled(True)
            self.needs_file_renaming = True
            self.append_log(f"🔍 Multiple model variants found - please select one")

        elif model_type == "single":
            self.file_combo.clear()
            self.file_combo.addItem(files[0])
            self.file_combo.setEnabled(False)
            self.needs_file_renaming = False
            self.append_log(f"✅ Single model file detected: {files[0]}")

        else:
            self.file_combo.clear()
            self.file_combo.setEnabled(False)
            self.needs_file_renaming = False
            self.append_log("❌ No model files found in directory")

    def browse_output(self):
        path = QFileDialog.getExistingDirectory(self, "Select Output Directory")
        if path:
            self.output_dir = path
            self.output_label.setText(f"Output: {path}")

    def append_log(self, msg: str):
        self.log.append(msg)

    def run_conversion(self):
        self.log.clear()
        self.conversion_queue.clear()

        if not self.model_path:
            QMessageBox.critical(self, "Error", "Please select a model directory.")
            return
        if not self.output_dir:
            QMessageBox.critical(self, "Error", "Please select an output directory.")
            return
        if not self.file_combo.count():
            QMessageBox.critical(self, "Error", "No model files detected.")
            return

        if self.awq_cb.isChecked():
            self.conversion_queue.append("awq")
        for opt, cb in self.quant_boxes.items():
            if cb.isChecked():
                self.conversion_queue.append(opt)
        
        if not self.conversion_queue:
            QMessageBox.information(self, "Info", "No quantization options selected.")
            return

        self._process_next()

    def _process_next(self):
        if self.current_thread and self.current_thread.isRunning():
            return

        if not self.conversion_queue:
            self.append_log("🎉 All conversions completed!")
            return

        option = self.conversion_queue.pop(0)
        selected_file = self.file_combo.currentText() if self.file_combo.isEnabled() else None
        model_name = os.path.basename(self.model_path)
        out_dir = os.path.join(self.output_dir, f"{model_name}-ct2-{option}")

        try:
            extras = [
                fn for fn in os.listdir(self.model_path)
                if not fn.endswith((".bin", ".safetensors", "onnx", "Pooling"))
                and fn not in [
                    "config.json", ".git", "coreml", "configs", "runs", ".idea",
                    ".cache",
                    "__pycache__",
                    ".gitignore",
                    ".gitattributes",
                    "flax_model.msgpack",
                    "tf_model.h5",
                ]
                and not fn.startswith('.')
                and os.path.isfile(os.path.join(self.model_path, fn))
            ]
            copy_part = "--copy_files " + " ".join(f'"{e}"' for e in extras) + " " if extras else ""
        except:
            copy_part = ""

        if option == "awq":
            cmd = (
                f'ct2-transformers-converter --model "{self.model_path}" '
                f'--output_dir "{out_dir}" '
                f'--low_cpu_mem_usage --trust_remote_code {copy_part.strip()}'
            )
        else:
            cmd = (
                f'ct2-transformers-converter --model "{self.model_path}" '
                f'--output_dir "{out_dir}" '
                f'--quantization {option} '
                f'--low_cpu_mem_usage --trust_remote_code {copy_part.strip()}'
            )

        thread = ConversionThread(
            self.model_path, selected_file, cmd, option, self.needs_file_renaming
        )
        self.current_thread = thread

        # Only log on these signals - don't chain to _process_next here
        thread.conversion_started.connect(
            lambda q, c: self.append_log(f"🚀 Starting {q} conversion...")
        )
        thread.conversion_finished.connect(
            lambda q, out: self.append_log(f"✅ {q} conversion completed")
        )
        thread.conversion_error.connect(
            lambda err: self.append_log(f"❌ Error: {err}")
        )
        
        # Chain to next conversion only after thread truly finishes
        thread.finished.connect(self._on_thread_finished)
        thread.start()

    def _on_thread_finished(self):
        if self.current_thread:
            self.current_thread.deleteLater()
            self.current_thread = None
        self._process_next()

    def _cleanup_thread(self):
        # Can remove this method now, or keep for other uses
        pass

    def closeEvent(self, event):
        if self.current_thread and self.current_thread.isRunning():
            self.current_thread.wait()
        event.accept()

if __name__ == "__main__":
    app = QApplication([])
    app.setStyle(QStyleFactory.create("Fusion"))
    window = App()
    window.show()
    app.exec()

You can remove the set_cuda_path functionality if you're relying on a systemwide installation of CUDA or what not.

@BBC-Esq

BBC-Esq commented Dec 21, 2025

Copy link
Copy Markdown
Contributor Author

Please let me know if the script doesn't save to the new ctranslate2 model's directory certain required files...I was always wondering about if I did it right...

@jordimas

Copy link
Copy Markdown
Collaborator

I need the exact steps to reproduce for example https://huggingface.co/CTranslate2HQ/Qwen3-0.6B-ct2-AWQ, this includes

  • Source model used
  • Script if used to convert to AWQ
  • Finally using the UI

Thanks

@BBC-Esq

BBC-Esq commented Dec 21, 2025 •

Copy link
Copy Markdown
Contributor Author

Geesh! You want all my hard work don't ya! 😉 Well here ya go sir:

ANOTHER BADASS SCRIPT TO CONVERT TO AWQ
import multiprocessing
if __name__ == '__main__':
    multiprocessing.set_start_method('spawn', force=True)

import sys
import os
from pathlib import Path

def set_cuda_paths():
    venv_base = Path(sys.executable).parent.parent
    nvidia_base_path = venv_base / 'Lib' / 'site-packages' / 'nvidia'
    cuda_path_runtime = nvidia_base_path / 'cuda_runtime' / 'bin'
    cuda_path_runtime_lib = nvidia_base_path / 'cuda_runtime' / 'bin' / 'lib' / 'x64'
    cuda_path_runtime_include = nvidia_base_path / 'cuda_runtime' / 'include'
    cublas_path = nvidia_base_path / 'cublas' / 'bin'
    cudnn_path = nvidia_base_path / 'cudnn' / 'bin'
    nvrtc_path = nvidia_base_path / 'cuda_nvrtc' / 'bin'
    nvcc_path = nvidia_base_path / 'cuda_nvcc' / 'bin'

    paths_to_add = [
        str(cuda_path_runtime),
        str(cuda_path_runtime_lib),
        str(cuda_path_runtime_include),
        str(cublas_path),
        str(cudnn_path),
        str(nvrtc_path),
        str(nvcc_path),
    ]

    current_value = os.environ.get('PATH', '')
    new_value = os.pathsep.join(paths_to_add + [current_value] if current_value else paths_to_add)
    os.environ['PATH'] = new_value

    triton_cuda_path = nvidia_base_path / 'cuda_runtime'
    os.environ['CUDA_PATH'] = str(triton_cuda_path)

set_cuda_paths()

import logging
import traceback

from PySide6.QtWidgets import (QApplication, QWidget, QPushButton, QLabel, QFileDialog, 
                             QMessageBox, QVBoxLayout, QHBoxLayout, QCheckBox, QTextEdit, 
                             QComboBox, QStyleFactory, QGroupBox, QProgressBar, QSpinBox, QSizePolicy)
from PySide6.QtCore import QThread, Signal, QObject, QTimer
from datasets import load_dataset
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

class QuantizationProcess(multiprocessing.Process):
    def __init__(self, model_dir, output_dir, quant_config, calib_config, pipe):
        super().__init__()
        self.model_dir = model_dir
        self.output_dir = output_dir
        self.quant_config = quant_config
        self.calib_config = calib_config
        self.pipe = pipe

    def load_cosmopedia_simple(self):
        data = load_dataset('HuggingFaceTB/cosmopedia-100k', split="train")
        data = data.filter(lambda x: x["text_token_length"] >= 1024)
        return [text for text in data["text"]]

    def load_cosmopedia_tokenized(self):
        data = load_dataset('HuggingFaceTB/cosmopedia-100k', split="train")
        data = data.filter(lambda x: x["text_token_length"] >= 1024)
        texts = [text for text in data["text"]]

        tokenized_inputs = self.tokenizer(texts, truncation=True, max_length=self.calib_config['seq_len'])
        return tokenized_inputs['input_ids']

    def run(self):
        try:
            self.pipe.send("Starting model loading...")

            model = AutoAWQForCausalLM.from_pretrained(
                self.model_dir,
                **{"low_cpu_mem_usage": True, "use_cache": False}
            )
            tokenizer = AutoTokenizer.from_pretrained(self.model_dir, trust_remote_code=True)
            self.tokenizer = tokenizer

            self.pipe.send("Loading calibration data...")

            try:
                calib_data = self.load_cosmopedia_simple()
                self.pipe.send("Starting quantization...")
                model.quantize(
                    tokenizer,
                    quant_config=self.quant_config,
                    calib_data=calib_data,
                    n_parallel_calib_samples=self.calib_config['n_samples'],
                    max_calib_samples=self.calib_config['max_samples'],
                    max_calib_seq_len=self.calib_config['seq_len']
                )
            except TypeError as e:
                if "missing 1 required positional argument: 'attention_mask'" in str(e):
                    self.pipe.send("Model requires attention masks, switching to tokenized format...")
                    calib_data = self.load_cosmopedia_tokenized()
                    self.pipe.send("Restarting quantization...")
                    try:
                        model.quantize(
                            tokenizer,
                            quant_config=self.quant_config,
                            calib_data=calib_data,
                            n_parallel_calib_samples=self.calib_config['n_samples'],
                            max_calib_samples=self.calib_config['max_samples'],
                            max_calib_seq_len=self.calib_config['seq_len']
                        )
                    except Exception as inner_e:
                        tb = traceback.format_exc()
                        self.pipe.send(f"Detailed error during tokenized quantization:\n{str(inner_e)}\n\nTraceback:\n{tb}")
                        self.pipe.send(f"Model class: {model.__class__.__name__}")
                        self.pipe.send(f"Model type: {model.config.model_type if hasattr(model, 'config') else 'unknown'}")
                        self.pipe.send(f"AutoAWQ version: {awq.__version__ if 'awq' in globals() else 'unknown'}")
                        self.pipe.send(f"Transformers version: {transformers.__version__ if 'transformers' in globals() else 'unknown'}")
                        self.pipe.send(f"Calibration data type: {type(calib_data)}")
                        if isinstance(calib_data, list):
                            self.pipe.send(f"First calib item type: {type(calib_data[0]) if calib_data else 'empty list'}")
                        raise
                else:
                    raise e

            self.pipe.send("Saving quantized model...")
            model.save_quantized(self.output_dir)
            tokenizer.save_pretrained(self.output_dir)

            self.pipe.send(f"Quantization complete. Model saved to {self.output_dir}")
        except Exception as e:
            tb = traceback.format_exc()
            self.pipe.send(f"Error during quantization: {str(e)}\n\nTraceback:\n{tb}")
            logging.error(f"Error during quantization: {str(e)}")
            logging.error(f"Traceback:\n{tb}")
        finally:
            self.pipe.close()

class ProcessCommunicator(QObject):
    message_received = Signal(str)

    def __init__(self, pipe):
        super().__init__()
        self.pipe = pipe

    def check_pipe(self):
        if self.pipe.poll():
            message = self.pipe.recv()
            self.message_received.emit(message)

class App(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("AWQ Quantization GUI")
        self.model_path = ""
        self.output_dir = ""
        self.resize(800, 600)
        self.quantization_process = None
        self.process_communicator = None
        
        layout = QVBoxLayout(self)

        browse_group = QGroupBox("Model Selection")
        browse_layout = QVBoxLayout()

        model_layout = QHBoxLayout()
        self.browse_btn = QPushButton("Browse Model Directory")
        self.browse_btn.setToolTip("Select the directory containing the model files to be quantized")
        self.browse_btn.clicked.connect(self.browse_model)
        model_layout.addWidget(self.browse_btn)
        self.path_label = QLabel("")
        model_layout.addWidget(self.path_label)
        browse_layout.addLayout(model_layout)

        output_layout = QHBoxLayout()
        self.output_browse_btn = QPushButton("Select Output Directory")
        self.output_browse_btn.setToolTip("Choose the directory where the quantized model will be saved")
        self.output_browse_btn.clicked.connect(self.browse_output)
        output_layout.addWidget(self.output_browse_btn)
        self.output_path_label = QLabel("")
        output_layout.addWidget(self.output_path_label)
        browse_layout.addLayout(output_layout)

        browse_group.setLayout(browse_layout)
        layout.addWidget(browse_group)

        quant_group = QGroupBox("Quantization Settings")
        quant_layout = QHBoxLayout()

        version_layout = QVBoxLayout()
        version_label = QLabel("Version:")
        self.version_combo = QComboBox()
        self.version_combo.addItems(["gemm", "gemv", "gemv_fast", "marlin"])
        self.version_combo.setToolTip(
            "Select quantization algorithm version:\n\n"
            "gemm: General Matrix Multiplication algorithm\n"
            "- Optimized for general matrix operations\n"
            "- Suitable for batch sizes below 8\n"
            "- Performs well with large contexts\n"
            "- Faster than FP16 for small to medium batch sizes\n"
            "- Requires zero_point=True\n\n"
            "gemv: General Matrix-Vector algorithm\n"
            "- 20% faster than GEMM for batch size 1\n"
            "- Not recommended for large contexts\n"
            "- Best suited for small batch sizes and low-latency tasks\n"
            "- Requires zero_point=True\n\n"
            "gemv_fast: Optimized GEMV implementation\n"
            "- Specifically designed for single-token and small-batch inference\n"
            "- 20% faster than standard GEMV for batch size 1\n"
            "- Implements advanced packing and decoding techniques to reduce overhead\n"
            "- Best for low-latency real-time tasks\n"
            "- Uses efficient interleaving and dequantization to minimize memory bottlenecks\n"
            "- Capable of efficient weight packing to enhance performance in memory-bound environments\n"
            "- Requires zero_point=True\n\n"
            "marlin: Advanced quantization algorithm\n"
            "- Specialized for large-scale inference tasks, supporting batch sizes from 16 to 32 tokens\n"
            "- Optimized for high-throughput environments with complex partitioning of inputs\n"
            "- Delivers near-ideal 4x speedups compared to FP16, even for larger batch sizes\n"
            "- Implements a unique memory layout and partitioning strategy to fully utilize GPU resources (L2 cache, tensor cores)\n"
            "- Designed to handle high-demand tasks such as speculative decoding or multi-inference scenarios\n"
            "- Features multiple optimizations like conflict-free memory operations, ideal access patterns, and reduced memory offset calculations\n"
            "- Best suited for high-throughput and large-model scenarios\n"
            "- Requires zero_point=False"
        )
        self.version_combo.currentTextChanged.connect(self.on_version_changed)
        version_layout.addWidget(version_label)
        version_layout.addWidget(self.version_combo)
        quant_layout.addLayout(version_layout)

        zero_point_layout = QVBoxLayout()
        self.zero_point_chk = QCheckBox("Enable Zero Point")
        self.zero_point_chk.setChecked(True)
        self.zero_point_chk.setToolTip(
            "Zero point setting for quantization:\n\n"
            "- Must be True for gemm/gemv/gemv_fast\n"
            "- Must be False for marlin\n"
            "- This setting will update automatically based on version selection"
        )
        self.zero_point_chk.setEnabled(False)
        zero_point_layout.addWidget(QLabel("Zero Point:"))
        zero_point_layout.addWidget(self.zero_point_chk)
        quant_layout.addLayout(zero_point_layout)

        group_size_layout = QVBoxLayout()
        group_size_label = QLabel("Group Size:")
        self.group_size_spin = QSpinBox()
        self.group_size_spin.setRange(8, 256)
        self.group_size_spin.setValue(128)
        self.group_size_spin.setToolTip(
            "Quantization group size parameter:\n\n"
            "Controls the granularity of weight quantization by defining the number of weights that share a scale factor.\n"
            "- Default value is 128, meaning each group of 128 weights shares the same scale.\n"
            "- Smaller group sizes (e.g., 32) increase precision but can use more memory.\n"
            "- Larger group sizes (e.g., 128) optimize memory efficiency at the cost of some precision.\n"
            "- Applies to all quantization algorithms like GEMM, GEMV, GEMVFast, and Marlin."
        )
        group_size_layout.addWidget(group_size_label)
        group_size_layout.addWidget(self.group_size_spin)
        quant_layout.addLayout(group_size_layout)

        bit_width_layout = QVBoxLayout()
        bit_width_label = QLabel("Bit Width:")
        self.bit_width_spin = QSpinBox()
        self.bit_width_spin.setRange(2, 8)
        self.bit_width_spin.setValue(4)
        self.bit_width_spin.setEnabled(False)
        self.bit_width_spin.setToolTip(
            "Number of bits to use for weight quantization:\n\n"
            "Controls the bit width for quantizing weights. This affects the balance between memory efficiency and precision:\n"
            "- Default is 4 bits, which significantly reduces memory usage while maintaining acceptable precision.\n"
            "- Lower bit widths are more memory-efficient but less precise.\n"
            "- This parameter is used across all quantization algorithms, including GEMM, GEMV, GEMVFast, and Marlin.\n"
            "- Currently, AutoAWQ only supports 4-bit quantizations."
        )
        bit_width_layout.addWidget(bit_width_label)
        bit_width_layout.addWidget(self.bit_width_spin)
        quant_layout.addLayout(bit_width_layout)

        quant_group.setLayout(quant_layout)
        layout.addWidget(quant_group)

        calib_group = QGroupBox("Calibration Settings")
        calib_layout = QHBoxLayout()

        parallel_samples_layout = QVBoxLayout()
        parallel_samples_label = QLabel("Parallel Calibration Samples:")
        self.parallel_samples_spin = QSpinBox()
        self.parallel_samples_spin.setRange(1, 32)
        self.parallel_samples_spin.setValue(1)
        self.parallel_samples_spin.setToolTip(
            "Number of calibration samples to process simultaneously during quantization:\n\n"
            "- Controls how many samples from the dataset are processed in parallel\n"
            "- Higher values can speed up calibration but require more GPU memory\n"
            "- If set too high combined with a large max_calib_samples, may cause out-of-memory (OOM) errors\n"
            "- Set to 1 for minimum memory usage but slower processing\n"
            "- If set to None (not available in GUI), runs all samples simultaneously\n"
            "- Adjust based on your GPU memory capacity and model size"
        )
        parallel_samples_layout.addWidget(parallel_samples_label)
        parallel_samples_layout.addWidget(self.parallel_samples_spin)
        calib_layout.addLayout(parallel_samples_layout)

        max_samples_layout = QVBoxLayout()
        max_samples_label = QLabel("Max Calibration Samples:")
        self.max_samples_spin = QSpinBox()
        self.max_samples_spin.setRange(1, 512)
        self.max_samples_spin.setValue(128)
        self.max_samples_spin.setToolTip(
            "Maximum total number of samples to use for calibration:\n\n"
            "Defines the total number of samples from the dataset that will be used to calibrate the model.\n"
            "- The AWQ algorithm is sample efficient, so 128-256 samples is typically sufficient\n"
            "- A higher number of samples may provide better calibration but will take longer\n"
            "- Combined with parallel samples setting, affects total memory usage during quantization\n"
            "- Adjust based on desired calibration quality and available compute resources"
        )
        max_samples_layout.addWidget(max_samples_label)
        max_samples_layout.addWidget(self.max_samples_spin)
        calib_layout.addLayout(max_samples_layout)

        seq_len_layout = QVBoxLayout()
        seq_len_label = QLabel("Max Sequence Length:")
        self.seq_len_spin = QSpinBox()
        self.seq_len_spin.setRange(4096, 32768)
        self.seq_len_spin.setValue(8192)
        self.seq_len_spin.setToolTip(
            "Maximum sequence length for calibration samples:\n\n"
            "Defines the maximum sequence length for samples during calibration.\n"
            "- The max_calib_seq_len parameter ensures that any sample exceeding this length will be truncated to fit within the specified limit.\n"
            "- Setting an appropriate value is important to ensure memory efficiency and maintain accuracy during calibration.\n"
            "- Larger sequence lengths will require more memory, but may provide better calibration for models handling long contexts.\n"
            "- Choose this value based on the expected input sequence length for your model and available memory resources."
        )
        seq_len_layout.addWidget(seq_len_label)
        seq_len_layout.addWidget(self.seq_len_spin)
        calib_layout.addLayout(seq_len_layout)

        calib_group.setLayout(calib_layout)
        layout.addWidget(calib_group)

        button_layout = QHBoxLayout()

        self.reset_btn = QPushButton("Reset to Defaults")
        self.reset_btn.setToolTip("Reset all settings to their default values")
        self.reset_btn.clicked.connect(self.reset_to_defaults)
        button_layout.addWidget(self.reset_btn)

        self.run_btn = QPushButton("Run Quantization")
        self.run_btn.setToolTip("Start the quantization process with current settings")
        self.run_btn.clicked.connect(self.run_quantization)
        button_layout.addWidget(self.run_btn)

        layout.addLayout(button_layout)

        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 0)
        self.progress_bar.hide()
        layout.addWidget(self.progress_bar)

        self.output_text = QTextEdit()
        self.output_text.setReadOnly(True)
        self.output_text.setToolTip("Status messages and error output will appear here")
        self.output_text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        layout.addWidget(self.output_text)

    def on_version_changed(self, version):
        if version == "marlin":
            self.zero_point_chk.setChecked(False)
        else:
            self.zero_point_chk.setChecked(True)

    def browse_model(self):
        path = QFileDialog.getExistingDirectory(self, "Select Model Directory")
        if path:
            self.model_path = path
            self.path_label.setText(f"Model Directory: {path}")
            parent_path = str(Path(path).parent)
            self.output_dir = parent_path
            self.output_path_label.setText(f"Output Directory: {parent_path}")

    def browse_output(self):
        path = QFileDialog.getExistingDirectory(self, "Select Output Directory")
        if path:
            self.output_dir = path
            self.output_path_label.setText(f"Output Directory: {path}")

    def append_to_text_widget(self, content):
        self.output_text.append(content)

    def run_quantization(self):
        if not self.model_path:
            QMessageBox.critical(self, "Error", "Please select a model directory.")
            return
        if not self.output_dir:
            QMessageBox.critical(self, "Error", "Please select an output directory.")
            return

        quant_config = {
            "zero_point": self.zero_point_chk.isChecked(),
            "q_group_size": self.group_size_spin.value(),
            "w_bit": self.bit_width_spin.value(),
            "version": self.version_combo.currentText()
        }

        calib_config = {
            "n_samples": self.parallel_samples_spin.value(),
            "max_samples": self.max_samples_spin.value(),
            "seq_len": self.seq_len_spin.value()
        }

        parent_conn, child_conn = multiprocessing.Pipe()
        self.quantization_process = QuantizationProcess(
            self.model_path, 
            self.output_dir, 
            quant_config,
            calib_config,
            child_conn
        )
        self.quantization_process.start()

        self.process_communicator = ProcessCommunicator(parent_conn)
        self.process_communicator.message_received.connect(self.on_process_message)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.process_communicator.check_pipe)
        self.timer.start(100)

        self.progress_bar.show()
        self.run_btn.setEnabled(False)

    def on_process_message(self, message):
        self.append_to_text_widget(message)
        if "complete" in message.lower() or "error" in message.lower():
            self.progress_bar.hide()
            self.timer.stop()
            self.run_btn.setEnabled(True)
            if self.quantization_process:
                self.quantization_process.join()
                self.quantization_process = None

    def reset_to_defaults(self):
        self.version_combo.setCurrentText("gemm")
        self.zero_point_chk.setChecked(True)
        self.group_size_spin.setValue(128)
        self.bit_width_spin.setValue(4)
        self.parallel_samples_spin.setValue(1)
        self.max_samples_spin.setValue(128)
        self.seq_len_spin.setValue(8192)
        self.append_to_text_widget("Reset all parameters to defaults.")

if __name__ == "__main__":
    multiprocessing.freeze_support()
    app = QApplication([])
    app.setStyle(QStyleFactory.create("Fusion"))
    widget = App()
    widget.show()
    sys.exit(app.exec())

It uses the HuggingFaceTB/cosmopedia-100k dataset, which is a good general one. Also, I set the context to 8192.

Steps

  1. Download the qwen3 models from Qwen's HF repo.
  2. Install my fork of AWQ and all necessary dependencies. This is a little tricky so make sure the dependencies don't conflict. Disregard any messages that the newer version of transformers isn't compatible with my fork of AWQ because it is. You might have to force install.
  3. Use my updated qwen3 converter class within ctranslate2.
  4. Run the above GFUI to convert the model to AWQ.
  5. Run the very first script further above to convert to ctranslate2 format.
  6. Make sure and use my modified qwen3 loader class that has AWQ support.

I also tested it starting with AWQ models provided by Qwen3 themselves...no difference, both work. The only reason I created a custom autoawq library was because Qwen3 didn't provide AWQ version of the 1.7 nor .6B versions.

@BBC-Esq

BBC-Esq commented Dec 21, 2025

Copy link
Copy Markdown
Contributor Author

If you only get this error when starting the program don't worry, it's normal but will still work:

image

@jordimas

Copy link
Copy Markdown
Collaborator

Thanks so much, great work

@jordimas
jordimas merged commit fd5aef6 into OpenNMT:master Dec 21, 2025
16 checks passed
@BBC-Esq
BBC-Esq deleted the support_qwen3_awq branch December 21, 2025 21:03
Lanurence666 pushed a commit to Lanurence666/CTranslate2_backport_py38 that referenced this pull request May 23, 2026
veilnetwork pushed a commit to veilnetwork/CTranslate2 that referenced this pull request Aug 27, 2026
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.

3 participants