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 🗞️🚀

* 09/01/2026 7.4.0-dev `main`: ✨ Added `glm5_next` / GLM-5.3-Flash quantization support.
* 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.
Expand Down Expand Up @@ -267,7 +268,7 @@ Selected public references where teams or companies explicitly mention GPT-QMode
| DeepSeek-V2/V3/V3.2/V4/R1 | ✅ | GPT-OSS | ✅ | LongCat Flash | ✅ | OLMo2/3 / LLaDA2 | ✅ | Yi | ✅ |
| DeepSeek-V2 Lite / VL / VL2 / OCR2 | ✅ | Granite / Granite MoE | ✅ | LongLLaMA | ✅ | Ovis 1.6/2/2.5/2.6 MoE/2.6 Next | ✅ | Seed-OSS | ✅ |
| Dream | ✅ | GRIN-MoE | ✅ | Instella | ✅ | Phi 1-4 | ✅ | Voxtral | ✅ |
| ERNIE 4.5 / MoE / VL MoE | ✅ | GLM 4/4V/4.5V/4.6V/5/5.1/OCR/ASR | ✅ | GLM4 MoE / Lite / 4.5V MoE | ✅ | MiniCPM 3/O/V/V 4_6 | ✅ | PanGu-α | ✅ |
| ERNIE 4.5 / MoE / VL MoE | ✅ | GLM 4/4V/4.5V/4.6V/5/5.1/5.3/OCR/ASR | ✅ | GLM4 MoE / Lite / 4.5V MoE | ✅ | MiniCPM 3/O/V/V 4_6 | ✅ | PanGu-α | ✅ |
| XVERSE | ✅ | Brumby | ✅ | Hymba | ✅ | Mistral | ✅ | Qwen 1/2/3/3.5 | ✅ |
| MiniMax M2/M3 | ✅ | AfMoE | ✅ | Bailing-MoE | ✅ | LFM2 / LFM2-VL / LFM2-MoE | ✅ | Marin | ✅ |
| InternVL Chat | ✅ | Laguna | ✅ | Mimo / Mimo V2 | ✅ | Zamba / Zamba2 | ✅ | Intern S1 / S2 Preview | ✅ |
Expand Down
2 changes: 2 additions & 0 deletions gptqmodel/models/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
from .definitions.glm4v import Glm4vGPTQ # noqa: E402
from .definitions.glm4v_moe import Glm4vMoeQModel, Glm4vMoeTextQModel # noqa: E402
from .definitions.glm_moe_dsa import GlmMoeDsaQModel # noqa: E402
from .definitions.glm5_next import Glm5NextQModel # noqa: E402
from .definitions.glm_ocr import GlmOCRGPTQ # noqa: E402
from .definitions.glmasr import GlmASRGPTQ # noqa: E402
from .definitions.gpt2 import GPT2QModel # noqa: E402
Expand Down Expand Up @@ -241,6 +242,7 @@
"glm4_moe": GLM4MoEGPTQ,
"glm4_moe_lite": Glm4MoeLiteQModel,
"glm_moe_dsa": GlmMoeDsaQModel,
"glm5_next": Glm5NextQModel,
"gpt_bigcode": GptBigCodeQModel,
"codegen": CodeGenQModel,
"cohere": LlamaQModel, # 100% llama clone
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 @@ -41,6 +41,7 @@
from .glmasr import GlmASRGPTQ
from .glm_ocr import GlmOCRGPTQ
from .glm_moe_dsa import GlmMoeDsaQModel
from .glm5_next import Glm5NextQModel
from .gpt2 import GPT2QModel
from .gpt_bigcode import GptBigCodeQModel
from .gpt_neo import GptNeoQModel
Expand Down
86 changes: 86 additions & 0 deletions gptqmodel/models/definitions/glm5_next.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
# SPDX-FileCopyrightText: 2026 [email protected]
# SPDX-License-Identifier: Apache-2.0
# Contact: [email protected], x.com/qubitium

from transformers import AutoModelForImageTextToText

from ...utils.model import move_to
from ..base import BaseQModel
from ..moe_lifecycle import GateUpDownMoELifecycleHooks


class Glm5NextQModel(BaseQModel):
"""GLM-5.3-Flash hybrid KDA/DSA multimodal MoE model."""

loader = AutoModelForImageTextToText
require_load_processor = True
require_trust_remote_code = False
layer_modules_strict = False

dynamic_expert_index = "n_routed_experts"
pre_lm_head_norm_module = "model.language_model.norm"

# The checkpoint carries an auxiliary MTP decoder after the 45 inference
# layers. Transformers intentionally ignores it while loading; preserve it
# verbatim when a quantized checkpoint is saved.
out_of_model_tensors = {"prefixes": ["model.language_model.layers.45"]}

moe_lifecycle_hooks = GateUpDownMoELifecycleHooks()

# Quantize Q/K/V/O in KDA, the post-LoRA Q/KV and output projections in DSA,
# and both dense-MLP and routed-expert projections.
# KDA state/gate projections, DSA LoRA/indexer projections, routers, shared
# experts, norms, and hyper-connections remain in the native dtype.
module_tree = [
"model",
"language_model",
"layers",
"#",
{
"input_layernorm": ("input_layernorm:!",),
"self_attn": (
# KDA (linear-attention) layers.
"q_proj:0:q",
"k_proj:0:k",
"v_proj:0:v",
# DSA layers. The low-rank input and sparse indexer stay dense.
"q_a_proj:!",
"kv_a_proj_with_mqa:!",
"indexer.wq_b:!",
"indexer.wk:!",
"indexer.weights_proj:!",
"q_b_proj:0:q",
"kv_b_proj:0:k:v",
"o_proj:1",
),
"post_attention_layernorm": ("post_attention_layernorm:!",),
"mlp:moe": {
# Dense fallback used by the first three decoder layers.
"": ("gate_proj:0:gate", "up_proj:0:up", "down_proj:1:down"),
"gate": ("gate:!", "e_score_correction_bias:!"),
"experts:routed:expert_activation=experts._apply_gate": {
"#": ("gate_proj:0:gate", "up_proj:0:up", "down_proj:1:down"),
},
"shared_experts:shared": (
"gate_proj:!",
"up_proj:!",
"down_proj:!",
),
},
},
]

def update_layer_replay_kwargs_from_output(self, layer, layer_output, layer_input_kwargs, target_device):
"""Pass full DSA selections to subsequent shared-indexer DSA layers."""

if not isinstance(layer_output, tuple) or len(layer_output) < 2:
return layer_input_kwargs

topk_indices = layer_output[1]
if topk_indices is not None:
layer_input_kwargs["prev_topk_indices"] = move_to(topk_indices, device=target_device)
return layer_input_kwargs


__all__ = ["Glm5NextQModel"]
22 changes: 16 additions & 6 deletions gptqmodel/models/moe_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,16 @@ def __init__(self, gate_proj_name: str = None, up_proj_name: str = None, down_pr
f"Got: gate={self.gate_proj_name}, up={self.up_proj_name}, down={self.down_proj_name}"
)

def apply_expert_activation(self, experts_module, expert, gate_out, up_out):
"""Apply the model's fused expert gate when it exposes one."""

fused_gate = getattr(experts_module, "_apply_gate", None)
if callable(fused_gate):
return fused_gate(torch.cat([gate_out, up_out], dim=-1))
if hasattr(expert, "act_fn"):
return expert.act_fn(gate_out) * up_out
return torch.nn.functional.silu(gate_out) * up_out

def _extract_moe_block_prefix(self, subset: Dict[str, Any], moe_block: nn.Module) -> Optional[str]:
"""
Extract moe_block_prefix from subset keys.
Expand Down Expand Up @@ -365,8 +375,6 @@ def forward_to_all_experts(
order they appear in the subset/module tree, then calls the original
routed forward for the final output.
"""
import torch.nn.functional as F

if not processor or not original_forward:
error_msg = "Missing processor or original_forward"
log.error(error_msg)
Expand Down Expand Up @@ -482,10 +490,12 @@ def run_routed_experts():
gate_out = gate_module(expert_input)
up_out = up_module(expert_input)

if hasattr(expert, 'act_fn'):
intermediate = expert.act_fn(gate_out) * up_out
else:
intermediate = F.silu(gate_out) * up_out
intermediate = self.apply_expert_activation(
experts_module=experts_module,
expert=expert,
gate_out=gate_out,
up_out=up_out,
)
del gate_out, up_out

get_callable_module(down_key)(intermediate)
Expand Down
46 changes: 32 additions & 14 deletions gptqmodel/utils/model_dequant.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,16 @@ def list_safetensor_files(model_path: Path) -> Tuple[list, Optional[dict]]:


def finalize_for_save(tensor: torch.Tensor, target_dtype: torch.dtype) -> torch.Tensor:
"""Cast to ``target_dtype`` when floating point and move to CPU with optimal layout."""
"""Cast floating tensors, move to CPU, and use safetensors-compatible layout."""

if torch.is_floating_point(tensor):
tensor = tensor.to(target_dtype)

tensor_cpu = tensor.to("cpu")
if tensor_cpu.ndim == 4:
tensor_cpu = tensor_cpu.contiguous(memory_format=torch.channels_last)
else:
tensor_cpu = tensor_cpu.contiguous()
return tensor_cpu
# safetensors requires the default contiguous layout. A 4D channels-last
# tensor is contiguous in PyTorch's channels-last sense, but save_file()
# rejects it because Tensor.is_contiguous() is false without a memory-format
# argument.
return tensor.to("cpu").contiguous()


def _is_deepseek_v4_routed_expert_weight_key(
Expand Down Expand Up @@ -1471,13 +1470,19 @@ def convert_compressed_pack_file(
return tensors


def copy_aux_files(model_path: Path, output_path: Path, skip: Iterable[str]) -> None:
def copy_aux_files(
model_path: Path,
output_path: Path,
skip: Iterable[str],
*,
dirs_exist_ok: bool = False,
) -> None:
for item in model_path.iterdir():
if item.name in skip:
continue
target = output_path / item.name
if item.is_dir():
shutil.copytree(item, target)
shutil.copytree(item, target, dirs_exist_ok=dirs_exist_ok)
else:
shutil.copy2(item, target)

Expand All @@ -1488,14 +1493,14 @@ def dequantize_model(
*,
target_dtype: torch.dtype = torch.bfloat16,
device: Optional[str] = None,
resume: bool = False,
) -> None:
model_path = Path(model_path)
output_path = Path(output_path)

if output_path.exists():
if output_path.exists() and not resume:
raise FileExistsError(f"Output path {output_path} already exists")

output_path.mkdir(parents=True)
output_path.mkdir(parents=True, exist_ok=resume)

config = load_json(model_path / "config.json")
quant_cfg = config.get("quantization_config", {}) or {}
Expand Down Expand Up @@ -1562,6 +1567,19 @@ def dequantize_model(
try:
for idx, filename in enumerate(files):
path = model_path / filename
output_file = output_path / filename
if resume and output_file.exists():
# Opening the shard validates its safetensors header before it
# is accepted as complete. Tensor views are memory-mapped, so
# collecting names and sizes does not load the shard into RAM.
with safe_open(output_file, framework="pt", device="cpu") as existing_reader:
for name in existing_reader.keys():
tensor = existing_reader.get_tensor(name)
weight_map[str(name)] = filename
total_size += tensor.element_size() * tensor.numel()
LOG.debug("Reusing completed output shard '%s'", filename)
pb.subtitle(f"{filename} (existing)").next().draw()
continue
LOG.debug("Processing shard '%s' for format %s on device %s", filename, fmt, open_device)
if fmt == "fp8":
with safe_open(path, framework="pt", device=open_device) as reader:
Expand Down Expand Up @@ -1620,7 +1638,7 @@ def dequantize_model(
raise ValueError(f"Unsupported format {fmt}")

if tensors:
save_file(tensors, str(output_path / filename))
save_file(tensors, str(output_file))
weight_map.update({str(name): filename for name in tensors})
total_size += sum(t.element_size() * t.numel() for t in tensors.values())
else:
Expand Down Expand Up @@ -1649,4 +1667,4 @@ def dequantize_model(
write_json(output_path / "config.json", new_config)

skip_files = set(files) | {"config.json", "model.safetensors.index.json"}
copy_aux_files(model_path, output_path, skip_files)
copy_aux_files(model_path, output_path, skip_files, dirs_exist_ok=resume)
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ datasets>=3.6.0
pyarrow>=21.0
dill>=0.3.8
torchao>=0.16.0
defuser>=0.0.26
defuser>=0.0.27
13 changes: 12 additions & 1 deletion scripts/dequantize_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def _parse_args() -> argparse.Namespace:
default="cpu",
help="Device to stage tensors during dequantization (cpu, cuda, cuda:7, ...)",
)
parser.add_argument(
"--resume",
action="store_true",
help="Reuse valid output shards already present in the output directory",
)
parser.add_argument(
"--env",
action="append",
Expand Down Expand Up @@ -106,7 +111,13 @@ def main() -> None:
}
print(f"[dequantize_model] parsed args: {debug_payload}")

dequantize_model(model_path, output_path, target_dtype=dtype, device=device)
dequantize_model(
model_path,
output_path,
target_dtype=dtype,
device=device,
resume=args.resume,
)


if __name__ == "__main__":
Expand Down
29 changes: 29 additions & 0 deletions tests/models/test_glm5_next.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
# SPDX-FileCopyrightText: 2026 [email protected]
# SPDX-License-Identifier: Apache-2.0
# Contact: [email protected], x.com/qubitium

from model_test import ModelTest


class TestGlm5Next(ModelTest):
NATIVE_MODEL_ID = "/monster/data/model/GLM-5.3-Flash-REAP50-BF16"
TRUST_REMOTE_CODE = False
USE_FLASH_ATTN = False
EVAL_TASKS_SLOW = {
"arc_challenge": {
"chat_template": False,
"acc": {"value": 0.49146757679180886, "floor_pct": 0.04},
"acc_norm": {"value": 0.5273037542662116, "floor_pct": 0.04},
},
}
EVAL_TASKS_FAST = ModelTest.derive_fast_eval_tasks(EVAL_TASKS_SLOW)

MODEL_COMPAT_FAST_LAYER_POSITION = "first"

# The REAP50 BF16 checkpoint is 331 GB. Even after 4-bit quantization, the
# model plus KDA workspaces should retain multi-GPU loading headroom.
EVAL_SINGLE_GPU = False

def test_glm5_next(self):
self.quantize_and_evaluate()
Loading