跳到主要内容

MiniCPM-V 4.7

MiniCPM-V 多模态系列

用更小的权重换取更低的显存与存储占用。GGUF 适用于本地与 CPU / Metal 运行,BNB / AWQ / GPTQ 面向 GPU 推理,35B-A3B 的 MoE 专家则需要 llm-compressor。可直接使用官方预量化权重,也可按本页脚本自行量化。

格式1B35B-A3B仓库后缀工具
GGUF✅✅-ggufllama.cpp
BNB (int4)✅需用 GGUF / 预量化权重-int4bitsandbytes
AWQ✅需用 GGUF / 预量化权重-AWQAutoAWQ
GPTQ✅需用 GGUF / 预量化权重-GPTQAutoGPTQ
FP8 / INT4—✅-FP8 / -INT4llm-compressor
35B-A3B 的融合专家

MiniCPM-V 4.7 35B-A3B 的 MoE 专家以融合参数存储(mlp.experts.gate_up_proj、mlp.experts.down_proj)。bitsandbytes / AutoAWQ / AutoGPTQ 只量化 nn.Linear,会把占权重约 92% 的专家留在 bf16(保存出来仍有 ~60 GB)。35B-A3B 要可用的 4-bit,请使用 GGUF 或已发布的 -INT4 / -FP8 权重,见 FP8 / INT4。

GGUF​

把 PyTorch 模型转换为 GGUF 并量化,产出用于 llama.cpp 或 Ollama。

MiniCPM-V 4.7 使用 canvas M-RoPE,引入了新的 GGUF 契约:元数据键 rope.section_order 与 mmproj 张量 v.tok_embd_sep。请使用 PR #29416 之后(含)的 llama.cpp 重新转换;旧版转换器产出的 GGUF 在近期构建里 M-RoPE 会算错。

可直接使用预构建 GGUF:openbmb/MiniCPM-V-4.7-1B-gguf / openbmb/MiniCPM-V-4.7-35B-A3B-gguf。

转换为 GGUF​

在当前版本(含 PR #29416)的 llama.cpp 根目录执行:

Bash
# 步骤 1 — 转换语言模型 + 视觉 merger 为 F16 GGUF
python ./convert_hf_to_gguf.py /path/to/MiniCPM-V-4.7-1B \
--outfile /path/to/MiniCPM-V-4.7-1B/MiniCPM-V-4_7-1B-F16.gguf \
--outtype f16

# 步骤 2 — 转换视觉投影(mmproj)
python ./convert_hf_to_gguf.py /path/to/MiniCPM-V-4.7-1B \
--mmproj \
--outfile /path/to/MiniCPM-V-4.7-1B/mmproj-model-f16.gguf

convert_hf_to_gguf.py 会从 config.json 自动识别 MiniCPMV4_7ForConditionalGeneration。

导出 4× 视觉变体:复制模型目录,把其 preprocessor_config.json 里的 "downsample_mode" 改成 "4x",再跑一次 --mmproj,输出 mmproj-model-f16-4x.gguf。

量化为 INT4​

拿到 F16 LM GGUF 后,用 llama-quantize 量化:

Bash
./llama-quantize \
/path/to/MiniCPM-V-4.7-1B/MiniCPM-V-4_7-1B-F16.gguf \
/path/to/MiniCPM-V-4.7-1B/MiniCPM-V-4_7-1B-Q4_K_M.gguf \
Q4_K_M

其他常用档位:Q5_K_M、Q6_K、Q8_0,按精度 / 显存权衡选择。mmproj 体积已很小,保持 F16,与所选 LM 量化搭配即可。

35B-A3B 的混合量化(推荐)​

35B-A3B 的 MoE 专家张量约占权重 92%。在 Q4_K_M 默认 mixture 之上,只把专家 gate / up / down 降到 Q3_K,并配合 imatrix,可得到约 14.4 GiB、精度与 Q4_K_M 相当的模型。

tensor-types.txt 内容:

Plain Text
ffn_(gate|up|down)_exps=Q3_K

带 imatrix 量化:

Bash
./llama-quantize \
--imatrix /path/to/v47-35a3.imatrix.gguf \
--tensor-type-file ./tensor-types.txt \
/path/to/MiniCPM-V-4.7-35B-A3B/MiniCPM-V-4_7-35B-A3B-F16.gguf \
/path/to/MiniCPM-V-4.7-35B-A3B/MiniCPM-V-4_7-35B-A3B-mix_q3gud_im.gguf \
Q4_K_M 32

--tensor-type-file 是首个匹配生效 —— 更具体的规则要放前面。

BNB (int4)​

加载原始模型,用 bitsandbytes 量化为 NF4,跑一次推理自检,然后保存量化模型。

Python
import os
import time
import torch
from PIL import Image
from transformers import AutoProcessor, AutoModelForImageTextToText, BitsAndBytesConfig

assert torch.cuda.is_available(), "需要 GPU。"

model_path = "openbmb/MiniCPM-V-4.7-1B" # 原始模型
save_path = "./MiniCPM-V-4.7-1B-int4" # 保存路径
image_path = "./assets/airplane.jpeg"

quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
load_in_8bit=False,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_quant_storage=torch.uint8,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
llm_int8_skip_modules=["lm_head"],
llm_int8_threshold=6.0,
)

processor = AutoProcessor.from_pretrained(model_path)
model = AutoModelForImageTextToText.from_pretrained(
model_path,
device_map="auto",
quantization_config=quantization_config,
)

image = Image.open(image_path).convert("RGB")
messages = [{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": "这张图片里有什么?"},
],
}]
inputs = processor.apply_chat_template(
messages, add_generation_prompt=True, tokenize=True,
return_dict=True, return_tensors="pt",
).to(model.device)

start = time.time()
with torch.inference_mode():
out_ids = model.generate(**inputs, max_new_tokens=256)
response = processor.decode(out_ids[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
print("量化后输出:", response)
print("量化后推理耗时:", time.time() - start)

os.makedirs(save_path, exist_ok=True)
model.save_pretrained(save_path, safe_serialization=True)
processor.save_pretrained(save_path)

量化模型保存在 save_path,可用 AutoModelForImageTextToText.from_pretrained(save_path) 重新加载。

结果(1B):

项值
位宽4(NF4,double quant)
quant_methodbitsandbytes
体积~1.5 GB(bf16 为 3.0 GB)

AWQ​

MiniCPM-V 使用的 AutoAWQ 工作流托管在 tc-mb/AutoAWQ(上游 casper-hansen/AutoAWQ 已归档)。该 fork 增加了 minicpmv4_7 架构注册。

使用预量化模型 + vLLM​

Python
from PIL import Image
from transformers import AutoProcessor
from vllm import LLM, SamplingParams

MODEL_NAME = "openbmb/MiniCPM-V-4.7-1B-AWQ"
image = Image.open("image.png").convert("RGB")

processor = AutoProcessor.from_pretrained(MODEL_NAME)
llm = LLM(model=MODEL_NAME, max_model_len=8192)

messages = [{
"role": "user",
"content": [
{"type": "image"},
{"type": "text", "text": "请描述这张图片的内容"},
],
}]
prompt = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

outputs = llm.generate(
{"prompt": prompt, "multi_modal_data": {"image": image}},
SamplingParams(temperature=0.0, max_tokens=1024),
)
print(outputs[0].outputs[0].text)

用 AutoAWQ 加载 AWQ 权重​

Bash
git clone https://github.com/tc-mb/AutoAWQ.git
cd AutoAWQ
pip install -e .
Python
import torch
from PIL import Image
from transformers import AutoProcessor
from awq import AutoAWQForCausalLM

model_path = "openbmb/MiniCPM-V-4.7-1B-AWQ"
image_path = "./assets/airplane.jpeg"

model = AutoAWQForCausalLM.from_quantized(model_path).to("cuda")
processor = AutoProcessor.from_pretrained(model_path)

image = Image.open(image_path).convert("RGB")
messages = [{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": "这张图片里有什么?"},
],
}]
inputs = processor.apply_chat_template(
messages, add_generation_prompt=True, tokenize=True,
return_dict=True, return_tensors="pt",
).to("cuda")

with torch.inference_mode():
out_ids = model.generate(**inputs, max_new_tokens=256)
print("输出:", processor.decode(out_ids[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True))

自行量化​

Python
import os
import shutil
import torch
from datasets import load_dataset
from transformers import AutoTokenizer
from awq import AutoAWQForCausalLM

model_path = "openbmb/MiniCPM-V-4.7-1B"
quant_path = "./MiniCPM-V-4.7-1B-AWQ"
quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"}

model = AutoAWQForCausalLM.from_pretrained(model_path, torch_dtype=torch.bfloat16)
model.model.to("cuda:0") # 确认骨干在 GPU 上
tokenizer = AutoTokenizer.from_pretrained(model_path)

def copy_files_not_in_B(A_path, B_path):
if not os.path.exists(B_path):
os.makedirs(B_path)
files_in_A = {f for f in os.listdir(A_path) if not (".bin" in f or "safetensors" in f)}
for f in files_in_A - set(os.listdir(B_path)):
src, dst = os.path.join(A_path, f), os.path.join(B_path, f)
if os.path.isfile(src):
shutil.copy2(src, dst)

def load_alpaca():
data = load_dataset("tatsu-lab/alpaca", split="train")

def concatenate(x):
msgs = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": x["instruction"]},
{"role": "assistant", "content": x["output"]},
]
return {"text": tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)}

return [r["text"] for r in data.map(concatenate)][:1024]

calib_data = load_alpaca()
model.quantize(tokenizer, quant_config=quant_config, calib_data=calib_data)
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)
copy_files_not_in_B(model_path, quant_path)
print(f'模型已量化并保存到 "{quant_path}"')

结果(1B):

项值
量化方法awq(GEMM)
位宽 / group size4 / 128
Zero point是
体积~2.3 GB(bf16 为 3.0 GB)

GPTQ​

MiniCPM-V 使用的 AutoGPTQ fork 为 tc-mb/AutoGPTQ。MiniCPM-V 4.7 1B 的文本骨干是 qwen3_5_text,因此直接复用既有的 Qwen3_5GPTQForCausalLM 量化器。

使用预量化模型 + vLLM​

Python
from PIL import Image
from transformers import AutoProcessor
from vllm import LLM, SamplingParams

MODEL_NAME = "openbmb/MiniCPM-V-4.7-1B-GPTQ"
image = Image.open("image.png").convert("RGB")

processor = AutoProcessor.from_pretrained(MODEL_NAME)
llm = LLM(model=MODEL_NAME, max_model_len=2048)

messages = [{"role": "user", "content": [
{"type": "text", "text": "(<image>./</image>)\n请描述这张图片的内容"},
]}]
prompt = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

outputs = llm.generate(
{"prompt": prompt, "multi_modal_data": {"image": image}},
SamplingParams(temperature=0.0, max_tokens=1024),
)
print(outputs[0].outputs[0].text)

自行量化​

从 MiniCPM-V 4.7 中抽出 Qwen3.5 文本骨干,用 GPTQ 量化为 4-bit,再用原始多模态组件重组完整模型。

Bash
git clone https://github.com/tc-mb/AutoGPTQ.git
cd AutoGPTQ
pip install -e .
Python
import os, json, shutil, logging
import torch
from datasets import load_dataset
from transformers import AutoTokenizer, AutoConfig, AutoModelForImageTextToText
from safetensors.torch import load_file as safe_load, save_file as safe_save

MODEL_PATH = "openbmb/MiniCPM-V-4.7-1B"
OUTPUT_PATH = "./MiniCPM-V-4.7-1B-GPTQ"
BITS, GROUP_SIZE = 4, 128
NUM_CALIBRATION_SAMPLES, MAX_SEQ_LEN = 128, 256

def prepare_calibration_data(tokenizer, num_samples, max_length):
dataset = load_dataset("tatsu-lab/alpaca", split="train")
dataset = dataset.shuffle(seed=42).select(range(min(num_samples, len(dataset))))
examples = []
for sample in dataset:
text = sample.get("text", "") or sample.get("output", "") or sample.get("instruction", "")
if not text:
continue
tok = tokenizer(text, truncation=True, max_length=max_length, return_tensors="pt")
examples.append({"input_ids": tok["input_ids"].squeeze(0),
"attention_mask": tok["attention_mask"].squeeze(0)})
return examples

class LLMWrapper(torch.nn.Module):
def __init__(self, text_model, lm_head, config):
super().__init__()
self.model, self.lm_head, self.config = text_model, lm_head, config
self.seqlen = MAX_SEQ_LEN

def forward(self, input_ids=None, attention_mask=None, **kwargs):
hidden = self.model(input_ids=input_ids, attention_mask=attention_mask, **kwargs)[0]
return self.lm_head(hidden)

tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
examples = prepare_calibration_data(tokenizer, NUM_CALIBRATION_SAMPLES, MAX_SEQ_LEN)

full_model = AutoModelForImageTextToText.from_pretrained(
MODEL_PATH, dtype=torch.float16, device_map="cpu"
)
text_model, lm_head = full_model.model.language_model, full_model.lm_head
config = full_model.config.text_config
config.model_type = "qwen3_5"

from auto_gptq import BaseQuantizeConfig
from auto_gptq.modeling.qwen3_5 import Qwen3_5GPTQForCausalLM

gptq_model = Qwen3_5GPTQForCausalLM(
LLMWrapper(text_model, lm_head, config), False,
BaseQuantizeConfig(bits=BITS, group_size=GROUP_SIZE, desc_act=False, sym=True),
)
gptq_model.quantize(examples, batch_size=1)

# 之后重组完整 state dict:把量化后的 LLM 张量加前缀 "model.language_model.",
# 再合并非 LLM(视觉)张量,保存分片 + index + 带 quantization_config 的 config.json。

结果(1B):

项值
量化方法gptq
位宽 / group size4 / 128
desc_act / symfalse / true
体积~1.9 GB(bf16 为 3.0 GB)

FP8 / INT4(llm-compressor)​

35B-A3B 要做可用的 4-bit,需用支持融合专家的工具,例如 llm-compressor(产出 compressed-tensors 权重)。

已发布的 35B-A3B 仓库 —— -FP8、-INT4、-int4、-AWQ、-GPTQ —— 都是 compressed-tensors 权重,可直接用 vLLM 加载。

加载预量化权重​

Python
from vllm import LLM, SamplingParams
from transformers import AutoProcessor
from PIL import Image

MODEL = "openbmb/MiniCPM-V-4.7-35B-A3B-FP8" # 或 -INT4 / -int4 / -AWQ / -GPTQ

processor = AutoProcessor.from_pretrained(MODEL)
llm = LLM(model=MODEL, max_model_len=8192, limit_mm_per_prompt={"image": 5})

image = Image.open("example.jpg").convert("RGB")
messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "描述这张图片"}]}]
prompt = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

out = llm.generate({"prompt": prompt, "multi_modal_data": {"image": image}},
SamplingParams(stop_token_ids=[248044, 248046], max_tokens=1024))
print(out[0].outputs[0].text)

自行量化​

安装 llm-compressor(它固定了 transformers,建议用独立环境):

Bash
pip install llmcompressor

配方 —— FP8(W8A8,动态):

YAML
# fp8_recipe.yaml
default_stage:
default_modifiers:
QuantizationModifier:
targets: [Linear]
ignore:
- 're:.*lm_head.*'
- 're:.*vision_tower.*'
- 're:.*vit_merger.*'
- 're:.*merger.*'
- 're:.*linear_attn.*'
- 're:.*mlp\.gate$'
- 're:.*shared_expert.*'
- 're:.*embed_tokens.*'
scheme: FP8_DYNAMIC
requires_calibration_data: false

配方 —— INT4(W4A16,group 128):

YAML
# w4a16_recipe.yaml
default_stage:
default_modifiers:
QuantizationModifier:
targets: [Linear]
ignore:
- 're:.*lm_head.*'
- 're:.*vision_tower.*'
- 're:.*vit_merger.*'
- 're:.*merger.*'
- 're:.*self_attn.*'
- 're:.*linear_attn.*'
- 're:.*mlp\.gate$'
- 're:.*shared_expert.*'
- 're:.*embed_tokens.*'
scheme: W4A16
requires_calibration_data: false

运行:

Python
from llmcompressor import oneshot
from llmcompressor.modifiers.quantization import QuantizationModifier
from transformers import AutoModelForImageTextToText, AutoProcessor

MODEL = "openbmb/MiniCPM-V-4.7-35B-A3B"
SAVE = "./MiniCPM-V-4.7-35B-A3B-INT4"

model = AutoModelForImageTextToText.from_pretrained(MODEL, dtype="auto", device_map="auto")
processor = AutoProcessor.from_pretrained(MODEL)

recipe = QuantizationModifier(
targets="Linear",
scheme="W4A16",
ignore=["lm_head", "re:.*vision_tower.*", "re:.*vit_merger.*", "re:.*merger.*",
"re:.*self_attn.*", "re:.*linear_attn.*", "re:.*mlp\\.gate$",
"re:.*shared_expert.*", "re:.*embed_tokens.*"],
)

oneshot(model=model, recipe=recipe)
model.save_pretrained(SAVE, save_compressed=True)
processor.save_pretrained(SAVE)

ignore 列表把视觉塔、merger、router / gate、shared expert、linear-attention 与 embedding 保持在完整精度 —— 量化它们要么破坏 canvas M-RoPE 视觉链路,要么精度损失大而体积收益小。融合的专家 gate_up_proj / down_proj 会被量化,这正是 MoE 模型的关键。

各 35B-A3B 量化仓库内的 recipe.yaml 即其实际使用的配方。