Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

## Latest News 🗞️🚀

* 08/31/2026 7.4.0-dev `main`: ✨ Added Qwen3.8-Flash-Next (`qwen4_exp`) quantization.
* 08/26/2026 7.4.0-dev `main`: ✨ Added NVIDIA `LocateAnything-3B` quantization support.
* 08/25/2026 7.4.0-dev `main`: ✨ Added Tencent `HunyuanOCR` quantization support.
* 08/25/2026 7.4.0-dev `main`: ✨ Added `lm_head` and embedding quantization lifecycle.
Expand Down Expand Up @@ -255,7 +256,7 @@ Selected public references where teams or companies explicitly mention GPT-QMode

| Model | | | | | | | | | |
|-------------------------------|---|---------------------------------|--|----------------------------|--|---------------------------------|--|------------------------|---|
| Apertus | ✅ | EXAONE 3/4 | ✅ | Dots1 | ✅ | Mistral3 / Ministral3 | ✅ | Qwen 2/3/3.5 (Next/MoE) | ✅ |
| Apertus | ✅ | EXAONE 3/4 | ✅ | Dots1 | ✅ | Mistral3 / Ministral3 | ✅ | Qwen 2/3/3.5/3.8 (Next/MoE) | ✅ |
| Baichuan | ✅ | Falcon (H1 / Mamba) | ✅ | InternLM 1/2/2.5 | ✅ | Mixtral | ✅ | Qwen 2/2.5/3 VL | ✅ |
| Bloom | ✅ | FastVLM | ✅ | Kimi K2 | ✅ | MobileLLM | ✅ | Qwen 2.5/3 Omni | ✅ |
| ChatGLM | ✅ | Gemma 1-4 / 3n | ✅ | Klear | ✅ | MOSS | ✅ | RefinedWeb | ✅ |
Expand Down
51 changes: 34 additions & 17 deletions gptqmodel/looper/stage_subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,24 +498,41 @@ def build_subset_plan(
for module_name in moe_groups[group_key]:
forward_device_map[module_name] = target_device

if forward_device_map:
# Once either dense or expert placement is explicit, anchor every
# untouched module back to its baseline placement so stale quant
# devices never leak into a later subset forward.
baseline_devices = _resolve_forward_baseline_devices(
subset=subset,
full=full,
)
for module_name, baseline_device in baseline_devices.items():
forward_device_map.setdefault(module_name, baseline_device)

for module_name, named_module in subset.items():
preferred_device = forward_device_map.get(module_name)
if preferred_device is not None:
named_module.state["preferred_quant_device"] = preferred_device
# A model may keep selected leaf modules on CPU even while replaying their layer on GPU.
placement_override = getattr(looper.gptq_model, "forward_device_for_module", None)
placement_override_active = getattr(looper.gptq_model, "has_forward_device_overrides", None)
placement_override_active = (
callable(placement_override)
and callable(placement_override_active)
and placement_override_active()
)

restore_forward_device_overrides = False
subset_forward_serial = True
if forward_device_map or placement_override_active:
# Start from each leaf's current device so an excluded tensor is never
# moved implicitly with its parent layer.
baseline_devices = _resolve_forward_baseline_devices(
subset=subset,
full=full,
)
for module_name, baseline_device in baseline_devices.items():
forward_device_map.setdefault(module_name, baseline_device)

if placement_override_active:
for module_name, planned_device in list(forward_device_map.items()):
module_ref = subset.get(module_name)
if module_ref is None and full is not None:
module_ref = full.get(module_name)
actual_module = module_ref.module if isinstance(module_ref, NamedModule) else module_ref
if actual_module is not None:
forward_device_map[module_name] = placement_override(actual_module, planned_device)

for module_name, named_module in subset.items():
preferred_device = forward_device_map.get(module_name)
if preferred_device is not None:
named_module.state["preferred_quant_device"] = preferred_device

restore_forward_device_overrides = False
subset_forward_serial = True

auto_forward_data_parallel = getattr(
looper.gptq_model.quantize_config,
Expand Down
2 changes: 2 additions & 0 deletions gptqmodel/models/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
from .definitions.qwen3_next import Qwen3NextGPTQ # noqa: E402
from .definitions.qwen3_omni_moe import Qwen3OmniMoeGPTQ
from .definitions.qwen3_vl import Qwen3_VLQModel
from .definitions.qwen4_exp import Qwen4ExpQModel # noqa: E402
from .definitions.rw import RwgQModel # noqa: E402
from .definitions.solar_open import SolarOpenQModel # noqa: E402
from .definitions.solar_open2 import SolarOpen2QModel # noqa: E402
Expand Down Expand Up @@ -309,6 +310,7 @@
"qwen2_5_omni": Qwen2_5_OmniGPTQ,
"qwen3_omni_moe": Qwen3OmniMoeGPTQ,
"qwen3_vl": Qwen3_VLQModel,
"qwen4_exp": Qwen4ExpQModel,
"dbrx": DbrxQModel,
"dbrx_converted": DbrxConvertedQModel,
"deepseek_v2": DeepSeekV2QModel,
Expand Down
41 changes: 41 additions & 0 deletions gptqmodel/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ class BaseQModel(nn.Module):
# so `defuser_module_paths` is used to explicitly locate and defuse them.
defuser_module_paths = None

# Multimodal wrappers can reuse the checkpoint rules of their text model.
hf_conversion_model_type_alias: Optional[str] = None

def __init__(
self,
model: PreTrainedModel,
Expand Down Expand Up @@ -488,6 +491,15 @@ def resolve_hf_conversion_map_reversed(cls, target_model: Optional[nn.Module] =
if configured_map is not None:
return copy.deepcopy(configured_map)

model_type_alias = getattr(cls, "hf_conversion_model_type_alias", None)
if model_type_alias:
inferred_map = LazyTurtle.infer_hf_conversion_map_reversed(
target_model=target_model,
model_type=model_type_alias,
)
if inferred_map is not None:
return copy.deepcopy(inferred_map)

inferred_map = LazyTurtle.infer_hf_conversion_map_reversed(target_model=target_model)
return copy.deepcopy(inferred_map) if inferred_map is not None else None

Expand Down Expand Up @@ -1973,6 +1985,35 @@ def pre_quantize(self, module: nn.Module) -> nn.Module:
else:
return module

def forward_device_for_module(self, module: nn.Module, planned_device: torch.device) -> torch.device:
"""Apply model-declared placement exclusions to subset replay planning."""

turtle_model = self.turtle_model
if not isinstance(turtle_model, LazyTurtle):
return planned_device

# LazyTurtle matches exclusions by dotted parameter path, not module type.
module_paths = getattr(self, "_forward_module_paths_by_id", None)
if module_paths is None or id(module) not in module_paths:
module_paths = {id(candidate): name for name, candidate in self.model.named_modules() if name}
self._forward_module_paths_by_id = module_paths
module_path = module_paths.get(id(module))
if module_path is None:
return planned_device
# Check only tensors owned by this leaf; descendants receive their own plan entry.
for rel_name, _ in module.named_parameters(recurse=False):
if turtle_model.is_no_placement_tensor(module_path, rel_name):
return torch.device(CPU)
return planned_device

def has_forward_device_overrides(self) -> bool:
"""Return whether replay must preserve model-declared tensor placement."""

turtle_model = self.turtle_model
return isinstance(turtle_model, LazyTurtle) and bool(
getattr(turtle_model, "_no_placement_params", ())
)

def post_quantize(self, module: nn.Module) -> nn.Module:
#return self.offload_to_disk(module=module)
return move_to(module, device=CPU)
Expand Down
1 change: 1 addition & 0 deletions gptqmodel/models/definitions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
from .qwen3 import Qwen3QModel
from .qwen3_moe import Qwen3MoeQModel
from .qwen3_vl import Qwen3_VLQModel
from .qwen4_exp import Qwen4ExpQModel
from .rw import RwgQModel
from .solar_open import SolarOpenQModel
from .solar_open2 import SolarOpen2QModel
Expand Down
75 changes: 75 additions & 0 deletions gptqmodel/models/definitions/qwen4_exp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
# SPDX-FileCopyrightText: 2026 qubitium@modelcloud.ai
# SPDX-License-Identifier: Apache-2.0
# Contact: qubitium@modelcloud.ai, x.com/qubitium

from transformers import AutoModelForImageTextToText

from ..base import BaseQModel
from ..moe_lifecycle import GateUpDownMoELifecycleHooks


class Qwen4ExpQModel(BaseQModel):
"""Qwen3.8-Flash-Next / Qwen4 experimental multimodal MoE."""

loader = AutoModelForImageTextToText
require_load_processor = True
layer_modules_strict = False

dynamic_expert_index = "num_experts"

# The final mixer replaces the usual decoder RMSNorm.
pre_lm_head_norm_module = "model.language_model.hyper_connection_mixer"
rotary_embedding = "model.language_model.rotary_emb"

# Transformers intentionally ignores the auxiliary MTP decoder on load.
out_of_model_tensors = {"prefixes": ["mtp"]}

# The outer config reuses the text model's PLE checkpoint mapping.
hf_conversion_model_type_alias = "qwen4_exp_text"
moe_lifecycle_hooks = GateUpDownMoELifecycleHooks()

# GQA makes o_proj shape-incompatible with the Q/K/V AWQ scale group.
awq_scale_optimize_shape_dependent_modules = ["self_attn.o_proj"]

# Only unmarked entries are quantized; PLE and hyper weights are omitted.
module_tree = [
"model",
"language_model",
"layers",
"#",
{
"self_attn": (
"indexer.index_qk_proj:!",
"indexer.q_layernorm:!",
"indexer.k_layernorm:!",
"q_proj:0",
"q_norm:!",
"k_proj:0",
"k_norm:!",
"v_proj:0",
"o_proj:1",
),
"linear_attn": (
"conv1d:!",
"in_proj_qkv:0",
"in_proj_z:1",
"in_proj_b:!:1",
"in_proj_a:!:1",
"norm:!",
"out_proj:2",
),
"mlp:moe": {
# Keep shared experts separate so placeholder expansion does not duplicate them.
"shared_expert": ("gate_proj:0", "up_proj:0", "down_proj:1"),
"gate": ("gate:!",),
"experts:0": {
"#": ("gate_proj:0", "up_proj:0", "down_proj:1"),
},
"shared_expert_gate": ("shared_expert_gate:!",),
},
},
]


__all__ = ["Qwen4ExpQModel"]
17 changes: 15 additions & 2 deletions gptqmodel/models/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
from ..utils.marlin import _marlin_capability_supported, _validate_marlin_device_support
from ..utils.swordfish import _validate_swordfish_device_support
from ..utils.model import (
apply_no_placement_to_device_map,
auto_dtype,
convert_gptq_v1_to_v2_format,
find_config_seq_len,
Expand All @@ -80,6 +81,7 @@
is_embeddings_module_quantized,
load_checkpoint_in_model_then_tie_weights,
make_quant,
no_placement_module_names,
simple_dispatch_model,
)
from ._const import DEVICE, HAS_NPU, normalize_device
Expand Down Expand Up @@ -1638,6 +1640,17 @@ def assign(mod, device_id):
else:
device_map = dict(explicit_device_map)
log.info(f"Loader: honoring explicit device_map request: {device_map}")
original_device_map = dict(device_map)
# Checkpoint loading needs a non-overlapping map: parent and child entries
# would otherwise make Accelerate read the same PLE tensor on both devices.
device_map = apply_no_placement_to_device_map(model, device_map)
if device_map != original_device_map:
cpu_modules = sorted(no_placement_module_names(model))
log.info(f"Loader: keeping no-placement modules on CPU: {cpu_modules}")
# Runtime dispatch keeps the parent entry so layer inputs still move to
# the right GPU, while the explicit CPU leaf blocks recursive PLE moves.
dispatch_device_map = dict(original_device_map)
dispatch_device_map.update(dict.fromkeys(no_placement_module_names(model), "cpu"))
log.info(f"Loader: device_map = {device_map}")

load_checkpoint_in_model = native_gguf_qspec is None
Expand Down Expand Up @@ -1762,15 +1775,15 @@ def assign(mod, device_id):
)

if native_gguf_qspec is not None:
model = simple_dispatch_model(model, device_map)
model = simple_dispatch_model(model, dispatch_device_map)
_load_quantized_gguf_checkpoint_into_model(
model=model,
gguf_checkpoint_path=gguf_checkpoint_path,
tensor_key_mapping=gguf_tensor_key_mapping,
)
else:
# TODO: Why are we using this custom function and not dispatch_model?
model = simple_dispatch_model(model, device_map)
model = simple_dispatch_model(model, dispatch_device_map)

if format_code == FORMAT.EXL3:
qlinear_kernel = ExllamaV3TorchLinear if backend == BACKEND.EXL3_TORCH else ExllamaV3Linear
Expand Down
Loading