Skip to main content

MiniCPM-V 4.5

MiniCPM-V Multimodal Series

Trade a little quality for much lower memory and storage. GGUF suits local and CPU / Metal runs, while BNB / AWQ / GPTQ target GPU inference. Use the official pre-quantized weights, or quantize yourself with the scripts on this page.

FormatPrecisionToolBest for
GGUFQ4_K_M and other levelsllama.cppLocal runs with llama.cpp / Ollama
BNBint4bitsandbytesGPU inference, simplest script
AWQ4-bitAutoAWQGPU inference, needs calibration data
GPTQ4-bitAutoGPTQGPU inference, needs calibration data

GGUF​

This guide will walk you through the process of converting the MiniCPM-V 4.5 PyTorch model to GGUF format and performing quantization.

Models in this guide need to be used with llama.cpp/ollama.

1. Download the PyTorch Model​

First, obtain the original PyTorch model files from one of the following sources:

2. Convert the PyTorch Model to GGUF Format​

Run the following commands in sequence to perform model surgery, convert the vision encoder, and then convert the language model.

Bash
# Step 1: Pre-process the model structure
python ./tools/mtmd/legacy-models/minicpmv-surgery.py -m ../MiniCPM-V-4_5

# Step 2: Convert the vision encoder to GGUF format
python ./tools/mtmd/legacy-models/minicpmv-convert-image-encoder-to-gguf.py -m ../MiniCPM-V-4_5 --minicpmv-projector ../MiniCPM-V-4_5/minicpmv.projector --output-dir ../MiniCPM-V-4_5/ --minicpmv_version 6

# Step 3: Convert the language model to GGUF format
python ./convert-hf-to-gguf.py ../MiniCPM-V-4_5/model

3. Perform INT4 Quantization​

Once the conversion is complete, use the llama-quantize tool to quantize the F16 precision GGUF model to INT4.

Bash
./llama-quantize ../MiniCPM-V-4_5/model/ggml-model-f16.gguf ../MiniCPM-V-4_5/model/ggml-model-Q4_K_M.gguf Q4_K_M

BNB​

Support: MiniCPM-V 4.5 / MiniCPM-V 4.0 / MiniCPM-V 2.6 / MiniCPM-V 2.5

1.Download the Model​

Download the MiniCPM-V-4_5 model from HuggingFace and extract it to your local directory.

2.Quantization Script​

The following script loads the original model, quantizes it to 4-bit using bitsandbytes, and saves the quantized model.

Python
import torch
from transformers import AutoModel, AutoTokenizer, BitsAndBytesConfig
from PIL import Image
import time
import GPUtil
import os

assert torch.cuda.is_available(), "CUDA is not available, but this code requires a GPU."

device = 'cuda' # Select GPU
model_path = '/model/MiniCPM-V-4_5' # Model path
save_path = './model/MiniCPM-V-4_5-int4' # Path to save quantized model
image_path = './assets/airplane.jpeg'

# Quantization configuration
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
load_in_8bit=False,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_storage=torch.uint8,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
llm_int8_enable_fp32_cpu_offload=False,
llm_int8_has_fp16_weight=False,
llm_int8_skip_modules=["out_proj", "kv_proj", "lm_head"],
llm_int8_threshold=6.0
)

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModel.from_pretrained(
model_path,
device_map=device,
quantization_config=quantization_config,
trust_remote_code=True
)

gpu_usage = GPUtil.getGPUs()[0].memoryUsed
start = time.time()
response = model.chat(
image=Image.open(image_path).convert("RGB"),
msgs=[
{
"role": "user",
"content": "What is in this picture?"
}
],
tokenizer=tokenizer
)
print('Output after quantization:', response)
print('Inference time after quantization:', time.time() - start)
print(f"GPU memory usage after quantization: {round(gpu_usage/1024, 2)}GB")

# Save the quantized model and tokenizer
os.makedirs(save_path, exist_ok=True)
model.save_pretrained(save_path, safe_serialization=True)
tokenizer.save_pretrained(save_path)

3.Expected Output​

After quantization, you should see output similar to:

Plain Text
Output after quantization: The image depicts an Airbus A380-800 aircraft in mid-flight against a clear blue sky. The airplane is predominantly white with a distinctive blue tail fin featuring a red and white logo. The fuselage has the text "Airbus A380-800" written on it, and there are Chinese characters along the side of the aircraft. The landing gear is partially extended, indicating that the plane is either taking off or preparing to land. The engines are visible under the wings, and the overall design showcases the large size and advanced engineering of the Airbus A380 model.
Inference time after quantization: 9.316158771514893
GPU memory usage after quantization: 18.97GB

The quantized model will be saved in the directory specified by save_path and can be used for further fine-tuning or inference.


To customize the model path, image, or save path, modify the corresponding variables in the script.

AWQ​

Support: MiniCPM-V 4.5

Method 1 (Use the pre-quantized model with vllm)​

1.Download the Model​

Download the 4-bit quantized MiniCPM-V-4_5 model with AutoAWQ from HuggingFace

Bash
git clone https://huggingface.co/openbmb/MiniCPM-V-4_5-AWQ

2.Run with vllm​

Python
import os
from PIL import Image
from transformers import AutoTokenizer
from vllm import LLM, SamplingParams


# Quantized model name or path
MODEL_NAME = "openbmb/MiniCPM-V-4_5-AWQ"

# List of image file paths
IMAGES = [
"image.png",
]

# Open and convert image
image = Image.open(IMAGES[0]).convert("RGB")

# Initialize tokenizer
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)

# Initialize LLM
llm = LLM(
model=MODEL_NAME,
# gpu_memory_utilization=0.9,
max_model_len=2048,
trust_remote_code=True,
# disable_mm_preprocessor_cache=True,
# limit_mm_per_prompt={"image": 5}
)

# Build messages
messages = [{
"role": "user",
"content": "(<image>./</image>)\nPlease describe the content of this image",
# "content": "(<image>./</image>)\n请描述这张图片的内容",
}]

# Apply chat template to the messages
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

# Set stop token IDs
stop_tokens = ['<|im_end|>', '</s>']
stop_token_ids = [tokenizer.convert_tokens_to_ids(i) for i in stop_tokens]

# Set generation parameters
sampling_params = SamplingParams(
stop_token_ids=stop_token_ids,
temperature=0.7,
# detokenize=True,
top_p=0.8,
# top_k=100,
# seed=3472,
max_tokens=1024,
# min_tokens=150,
)

# Get model output
outputs = llm.generate({
"prompt": prompt,
"multi_modal_data": {
"image": image
}
}, sampling_params=sampling_params)
print(outputs[0].outputs[0].text)

Method 2 (Use the pre-quantized model)​

1.Download the Model​

Download the MiniCPM-V 4.5 model from HuggingFace

Bash
git clone https://huggingface.co/openbmb/MiniCPM-V-4_5

2.Download and build AutoAWQ​

Since the official AutoAWQ repository is no longer maintained, please download and build our fork instead.

Bash
git clone https://github.com/tc-mb/AutoAWQ.git
cd AutoAWQ
pip install -e .

3.Inference Script​

Use the following script to directly use the AWQ quantized model for inference.

Python
import os
from PIL import Image
from transformers import AutoTokenizer, TextStreamer
from awq import AutoAWQForCausalLM
import torch

# Quantized model name or path
model_path = "openbmb/MiniCPM-V-4_5-AWQ"
device = 'cuda'
# List of image file paths
image_path = './assets/airplane.jpeg'

model = AutoAWQForCausalLM.from_quantized(model_path, trust_remote_code=True).to('cuda')
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

gpu_usage = GPUtil.getGPUs()[0].memoryUsed
response = model.chat(
image=Image.open(image_path).convert("RGB"),
msgs=[
{
"role": "user",
"content": "What is in this picture?"
}
],
tokenizer=tokenizer
) # 模型推理

print('Output:',response)

Method 3 (Quantize the model yourself)​

1.Download the Model​

Download the MiniCPM-V 4.5 model from HuggingFace

Bash
git clone https://huggingface.co/openbmb/MiniCPM-V-4_5

2.Download and build AutoAWQ​

Since the official AutoAWQ repository is no longer maintained, please download and build our fork instead.

Bash
git clone https://github.com/tc-mb/AutoAWQ.git
cd AutoAWQ
pip install -e .

3.Quantization Script​

Run the following quantization script (replace model_path and quant_path with the paths to the original model and the quantized model, respectively).

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

# Set the path to the original model (can be a local path or model ID)
model_path = '/openbmb/MiniCPM-V-4_5'

# Path to save the quantized model
quant_path = '/model_quantized/minicpmv4_5_awq'

# Quantization configuration: 4-bit weights, group size 128, GEMM backend
quant_config = { "zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM" } # "w_bit":4 or 8


# Load the original model and tokenizer
model = AutoAWQForCausalLM.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.bfloat16)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

# Copy files that exist in model_path but not in quant_path (excluding weight files)
def copy_files_not_in_B(A_path, B_path):
"""
Copies files from directory A to directory B if they exist in A but not in B.

:param A_path: Path to the source directory (A).
:param B_path: Path to the destination directory (B).
"""
# Ensure source directory exists
if not os.path.exists(A_path):
raise FileNotFoundError(f"The directory {A_path} does not exist.")
if not os.path.exists(B_path):
os.makedirs(B_path)

# List all files in directory A except weight files (e.g., .bin or safetensors)
files_in_A = os.listdir(A_path)
files_in_A = set([file for file in files_in_A if not (".bin" in file or "safetensors" in file )])
# List all files in directory B
files_in_B = set(os.listdir(B_path))

# Determine which files need to be copied
files_to_copy = files_in_A - files_in_B

# Copy each missing file from A to B
for file in files_to_copy:
src_file = os.path.join(A_path, file)
dst_file = os.path.join(B_path, file)
if os.path.isfile(src_file):
shutil.copy2(src_file, dst_file)

# Define data loading methods
# Load the Alpaca dataset
def load_alpaca():
data = load_dataset("tatsu-lab/alpaca", split="train")

# Convert each example into a chat-style prompt
def concatenate_data(x):
if x['input'] and x['instruction']:
msgs = [
{"role": "system", "content": x['instruction']},
{"role": "user", "content": x['input']},
{"role": "assistant", "content": x['output']},
]
elif x['input']:
msgs = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": x['input']},
{"role": "assistant", "content": x['output']}
]
else:
msgs = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": x['instruction']},
{"role": "assistant", "content": x['output']}
]

data = tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
return {"text": data}

concatenated = data.map(concatenate_data)
return [text for text in concatenated["text"]][:1024]

# Load Wikitext dataset
def load_wikitext():
data = load_dataset('wikitext', 'wikitext-2-raw-v1', split="train")
return [text for text in data["text"] if text.strip() != '' and len(text.split(' ')) > 20]


# Load calibration data
calib_data = load_alpaca()
# Quantize
model.quantize(tokenizer, quant_config=quant_config, calib_data=calib_data)

# shutil.rmtree(quant_path, ignore_errors=True)

# Save the quantized model
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)

copy_files_not_in_B(model_path, quant_path)
print(f'Model is quantized and saved at "{quant_path}"')

GPTQ​

Support: MiniCPM-V 4.5

Method 1 (Use the pre-quantized model)​

1.Download the Model​

Download the 4-bit GPTQ-quantized MiniCPM-V-4_5 from one of:

Bash
git clone https://huggingface.co/openbmb/MiniCPM-V-4_5-GPTQ

2.Run with vllm​

Python
import os
from PIL import Image
from transformers import AutoTokenizer
from vllm import LLM, SamplingParams


# Quantized model name or path
MODEL_NAME = "openbmb/MiniCPM-V-4_5-GPTQ"

# List of image file paths
IMAGES = [
"image.png",
]

# Open and convert image
image = Image.open(IMAGES[0]).convert("RGB")

# Initialize tokenizer
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)

# Initialize LLM
llm = LLM(
model=MODEL_NAME,
# gpu_memory_utilization=0.9,
max_model_len=2048,
trust_remote_code=True,
# disable_mm_preprocessor_cache=True,
# limit_mm_per_prompt={"image": 5}
)

# Build messages
messages = [{
"role": "user",
"content": "(<image>./</image>)\nPlease describe the content of this image",
# "content": "(<image>./</image>)\n请描述这张图片的内容",
}]

# Apply chat template to the messages
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

# Set stop token IDs
stop_tokens = ['<|im_end|>', '</s>']
stop_token_ids = [tokenizer.convert_tokens_to_ids(i) for i in stop_tokens]

# Set generation parameters
sampling_params = SamplingParams(
stop_token_ids=stop_token_ids,
temperature=0.7,
top_p=0.8,
max_tokens=1024,
)

# Get model output
outputs = llm.generate({
"prompt": prompt,
"multi_modal_data": {
"image": image
}
}, sampling_params=sampling_params)
print(outputs[0].outputs[0].text)

Method 2 (Quantize the model yourself)​

1.Download the Model​

Download the MiniCPM-V 4.5 model from HuggingFace

Bash
git clone https://huggingface.co/openbmb/MiniCPM-V-4_5

2.Install AutoGPTQ​

Bash
git clone https://github.com/tc-mb/AutoGPTQ.git
cd AutoGPTQ
pip install -e .

3.Quantization Script​

The following script extracts the LLM backbone (Qwen3) from MiniCPM-V-4_5, quantizes it to 4-bit using GPTQ, and reassembles the full model with the original multimodal components.

Run the following quantization script (replace MODEL_PATH and OUTPUT_PATH with the paths to the original model and the quantized model, respectively).

Python
import os
import sys
import json
import shutil
import logging
from pathlib import Path

import torch
from datasets import load_dataset
from transformers import AutoModel, AutoTokenizer, AutoConfig


def _ensure_hf_dynamic_module_cache(model_path: str) -> None:
"""Pre-copy all .py files so transitive relative imports are available."""
from transformers.dynamic_module_utils import (
HF_MODULES_CACHE,
TRANSFORMERS_DYNAMIC_MODULE_NAME,
_sanitize_module_name,
init_hf_modules,
)
init_hf_modules()
submodule = _sanitize_module_name(Path(model_path).name)
cache_dir = Path(HF_MODULES_CACHE) / TRANSFORMERS_DYNAMIC_MODULE_NAME / submodule
cache_dir.mkdir(parents=True, exist_ok=True)
for py in Path(model_path).glob("*.py"):
dst = cache_dir / py.name
if not dst.exists():
shutil.copy2(str(py), str(dst))


logging.basicConfig(
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
level=logging.INFO,
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)

MODEL_PATH = '/model/MiniCPM-V-4_5'
OUTPUT_PATH = './model/MiniCPM-V-4_5-gptq-int4'

BITS = 4
GROUP_SIZE = 128
NUM_CALIBRATION_SAMPLES = 128
MAX_SEQ_LEN = 512


def prepare_calibration_data(tokenizer, num_samples, max_length):
"""Prepare calibration data from Alpaca dataset."""
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
tokenized = tokenizer(
text,
truncation=True,
max_length=max_length,
return_tensors="pt",
)
examples.append({
"input_ids": tokenized["input_ids"].squeeze(0),
"attention_mask": tokenized["attention_mask"].squeeze(0),
})

logger.info(f"Prepared {len(examples)} calibration samples")
return examples


def main():
logger.info(f"Loading tokenizer from {MODEL_PATH}")
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)

logger.info("Preparing calibration data...")
examples = prepare_calibration_data(tokenizer, NUM_CALIBRATION_SAMPLES, MAX_SEQ_LEN)

_ensure_hf_dynamic_module_cache(MODEL_PATH)
logger.info(f"Loading full MiniCPM-V model from {MODEL_PATH}")
full_model = AutoModel.from_pretrained(
MODEL_PATH,
trust_remote_code=True,
torch_dtype=torch.float16,
device_map="cpu",
)

llm = full_model.llm
logger.info(f"Extracted LLM backbone: {llm.__class__.__name__}")

original_model_type = llm.config.model_type
llm.config.model_type = "qwen3"

from auto_gptq import BaseQuantizeConfig
from auto_gptq.modeling.qwen3 import Qwen3GPTQForCausalLM

quantize_config = BaseQuantizeConfig(
bits=BITS,
group_size=GROUP_SIZE,
desc_act=False,
sym=True,
)
logger.info(f"Quantize config: bits={BITS}, group_size={GROUP_SIZE}")

llm.seqlen = MAX_SEQ_LEN
gptq_model = Qwen3GPTQForCausalLM(llm, False, quantize_config)

logger.info("Starting GPTQ quantization...")
gptq_model.quantize(examples, batch_size=1)
logger.info("Quantization complete!")

os.makedirs(OUTPUT_PATH, exist_ok=True)

llm_temp_dir = os.path.join(OUTPUT_PATH, "_llm_temp")
os.makedirs(llm_temp_dir, exist_ok=True)
gptq_model.save_quantized(llm_temp_dir, use_safetensors=True)

# Copy non-weight files from original model
for fname in os.listdir(MODEL_PATH):
src = os.path.join(MODEL_PATH, fname)
dst = os.path.join(OUTPUT_PATH, fname)
if fname.startswith("model") and fname.endswith(".safetensors"):
continue
if fname in ("config.json", "quantize_config.json", "model.safetensors.index.json"):
continue
if os.path.isfile(src) and not os.path.exists(dst):
shutil.copy2(src, dst)
elif os.path.isdir(src) and not os.path.exists(dst):
shutil.copytree(src, dst)

# Combine quantized LLM weights with original multimodal weights
from safetensors.torch import load_file as safe_load, save_file as safe_save

quantized_llm_files = [f for f in os.listdir(llm_temp_dir) if f.endswith(".safetensors")]
llm_state_dict = {}
for f in quantized_llm_files:
sd = safe_load(os.path.join(llm_temp_dir, f))
llm_state_dict.update(sd)

non_llm_state_dict = {}
full_model_sd = full_model.state_dict()
for key, value in full_model_sd.items():
if not key.startswith("llm."):
non_llm_state_dict[key] = value

combined_sd = {}
for key, value in llm_state_dict.items():
combined_sd[f"llm.{key}"] = value
for key, value in non_llm_state_dict.items():
combined_sd[key] = value

MAX_SHARD_SIZE_BYTES = 4 * 1024 * 1024 * 1024 # 4GB per shard
sorted_keys = sorted(combined_sd.keys())

shards = []
current_shard = {}
current_size = 0
weight_map = {}

for key in sorted_keys:
tensor = combined_sd[key]
tensor_size = tensor.nelement() * tensor.element_size()
if current_size > 0 and current_size + tensor_size > MAX_SHARD_SIZE_BYTES:
shards.append(current_shard)
current_shard = {}
current_size = 0
current_shard[key] = tensor.clone().contiguous()
current_size += tensor_size

if current_shard:
shards.append(current_shard)

if len(shards) == 1:
shard_name = "model.safetensors"
safe_save(shards[0], os.path.join(OUTPUT_PATH, shard_name))
for key in shards[0]:
weight_map[key] = shard_name
else:
for i, shard in enumerate(shards):
shard_name = f"model-{i+1:05d}-of-{len(shards):05d}.safetensors"
safe_save(shard, os.path.join(OUTPUT_PATH, shard_name))
for key in shard:
weight_map[key] = shard_name

index = {
"metadata": {"total_size": sum(t.nelement() * t.element_size() for t in combined_sd.values())},
"weight_map": weight_map,
}
with open(os.path.join(OUTPUT_PATH, "model.safetensors.index.json"), "w") as f:
json.dump(index, f, indent=2)

# Save config with quantization info
config = AutoConfig.from_pretrained(MODEL_PATH, trust_remote_code=True)
config_dict = config.to_dict()
config_dict["quantization_config"] = {
"bits": BITS,
"group_size": GROUP_SIZE,
"damp_percent": 0.01,
"desc_act": False,
"static_groups": False,
"sym": True,
"true_sequential": True,
"quant_method": "gptq",
"checkpoint_format": "gptq",
"model_name_or_path": None,
"model_file_base_name": None,
}
config_dict["model_type"] = original_model_type

with open(os.path.join(OUTPUT_PATH, "config.json"), "w") as f:
json.dump(config_dict, f, indent=2)

quant_config_dict = quantize_config.to_dict()
with open(os.path.join(OUTPUT_PATH, "quantize_config.json"), "w") as f:
json.dump(quant_config_dict, f, indent=2)

shutil.rmtree(llm_temp_dir)

del combined_sd, llm_state_dict, non_llm_state_dict, full_model_sd
torch.cuda.empty_cache()

logger.info(f"Done! Quantized model saved to: {OUTPUT_PATH}")
logger.info(f"Quantization: W{BITS}A16 GPTQ (weight-only {BITS}-bit, activation fp16)")
logger.info("Compatible with: transformers (via optimum) and vLLM")


if __name__ == "__main__":
main()