MiniCPM-V 4.7
MiniCPM-V Multimodal Series
用更小的权重换取更低的显存与存储占用。GGUF 适用于本地与 CPU / Metal 运行,BNB / AWQ / GPTQ 面向 GPU 推理,35B-A3B 的 MoE 专家则需要 llm-compressor。可直接使用官方预量化权重,也可按本页脚本自行量化。
| 格式 | 1B | 35B-A3B | 仓库后缀 | 工具 |
|---|---|---|---|---|
| GGUF | ✅ | ✅ | -gguf | llama.cpp |
| BNB (int4) | ✅ | 需用 GGUF / 预量化权重 | -int4 | bitsandbytes |
| AWQ | ✅ | 需用 GGUF / 预量化权重 | -AWQ | AutoAWQ |
| GPTQ | ✅ | 需用 GGUF / 预量化权重 | -GPTQ | AutoGPTQ |
| FP8 / INT4 | — | ✅ | -FP8 / -INT4 | llm-compressor |
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。
可直接使用预构建 GGUF:openbmb/MiniCPM-V-4.7-1B-gguf / openbmb/MiniCPM-V-4.7-35B-A3B-gguf。
转换为 GGUF
在当前版本(含 PR #29416)的 llama.cpp 根目录执行:
# 步骤 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 量化:
./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 内容:
ffn_(gate|up|down)_exps=Q3_K
带 imatrix 量化:
./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,跑一次推理自检,然后保存量化模型。
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_method | bitsandbytes |
| 体积 | ~1.5 GB(bf16 为 3.0 GB) |
AWQ
MiniCPM-V 使用的 AutoAWQ 工作流托管在 tc-mb/AutoAWQ(上游 casper-hansen/AutoAWQ 已归档)。该 fork 增加了 minicpmv4_7 架构注册。
使用预量化模型 + vLLM
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 权重
git clone https://github.com/tc-mb/AutoAWQ.git
cd AutoAWQ
pip install -e .
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))
自行量化
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 size | 4 / 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
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,再用原始多模态组件重组完整模型。
git clone https://github.com/tc-mb/AutoGPTQ.git
cd AutoGPTQ
pip install -e .
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 size | 4 / 128 |
desc_act / sym | false / 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 加载。
加载预量化权重
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,建议用独立环境):
pip install llmcompressor
配方 —— FP8(W8A8,动态):
# 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):
# 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
运行:
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 即其实际使用的配方。