Image-Text-to-Text
GGUF
English
Chinese
multimodal
vision
llama.cpp
glm_moe_dsa
unsloth
conversational
imatrix
Instructions to use QuixiAI/GLM-5.2-Vision-GGUF with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use QuixiAI/GLM-5.2-Vision-GGUF with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL # Run inference directly in the terminal: llama cli -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL # Run inference directly in the terminal: llama cli -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL # Run inference directly in the terminal: ./llama-cli -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL # Run inference directly in the terminal: ./build/bin/llama-cli -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Use Docker
docker model run hf.co/QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
- LM Studio
- Jan
- vLLM
How to use QuixiAI/GLM-5.2-Vision-GGUF with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "QuixiAI/GLM-5.2-Vision-GGUF" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "QuixiAI/GLM-5.2-Vision-GGUF", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
- Ollama
How to use QuixiAI/GLM-5.2-Vision-GGUF with Ollama:
ollama run hf.co/QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
- Unsloth Studio
How to use QuixiAI/GLM-5.2-Vision-GGUF with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for QuixiAI/GLM-5.2-Vision-GGUF to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for QuixiAI/GLM-5.2-Vision-GGUF to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for QuixiAI/GLM-5.2-Vision-GGUF to start chatting
- Pi
How to use QuixiAI/GLM-5.2-Vision-GGUF with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use QuixiAI/GLM-5.2-Vision-GGUF with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Run Hermes
hermes
- Atomic Chat new
- OpenClaw new
How to use QuixiAI/GLM-5.2-Vision-GGUF with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
- Docker Model Runner
How to use QuixiAI/GLM-5.2-Vision-GGUF with Docker Model Runner:
docker model run hf.co/QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
- Lemonade
How to use QuixiAI/GLM-5.2-Vision-GGUF with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull QuixiAI/GLM-5.2-Vision-GGUF:UD-Q6_K_XL
Run and chat with the model
lemonade run user.GLM-5.2-Vision-GGUF-UD-Q6_K_XL
List all available models
lemonade list
| #!/usr/bin/env python3 | |
| """Create the shared F16 GLM-5.2-Vision mmproj GGUF for llama.cpp.""" | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| import os | |
| import shutil | |
| import struct | |
| import subprocess | |
| import sys | |
| import tempfile | |
| from pathlib import Path | |
| from typing import Any, Iterable, Sequence | |
| DEFAULT_SOURCE = Path("../GLM-5.2-Vision-FP8") | |
| DEFAULT_OUTPUT = Path("mmproj-GLM-5.2-Vision-f16.gguf") | |
| DEFAULT_LLAMA_CPP = Path("../../llama.cpp") | |
| LLAMA_CPP_PR_URL = "https://github.com/ggml-org/llama.cpp/pull/26126" | |
| VISION_FILENAME = "vision_tower.safetensors" | |
| PROJECTOR_FILENAME = "mm_projector.safetensors" | |
| EXPECTED_VISION_TENSORS = 329 | |
| EXPECTED_PROJECTOR_TENSORS = 6 | |
| EXPECTED_OUTPUT_TENSORS = EXPECTED_VISION_TENSORS + EXPECTED_PROJECTOR_TENSORS | |
| MIN_FREE_BYTES = 2_000_000_000 | |
| BUILD_TARGETS = ("llama-server", "llama-mtmd-cli", "llama-mtmd-debug") | |
| class GeneratorError(RuntimeError): | |
| pass | |
| def info(message: str) -> None: | |
| print(f"[glm5v-mmproj] {message}", flush=True) | |
| def display_command(command: Sequence[str]) -> str: | |
| import shlex | |
| displayed: list[str] = [] | |
| previous = "" | |
| for part in command: | |
| if previous == "-c" and len(part) > 120: | |
| displayed.append("<inline Python>") | |
| else: | |
| displayed.append(part) | |
| previous = part | |
| return " ".join(shlex.quote(part) for part in displayed) | |
| def run( | |
| command: Sequence[str], | |
| *, | |
| cwd: Path | None = None, | |
| input_bytes: bytes | None = None, | |
| capture: bool = False, | |
| check: bool = True, | |
| ) -> subprocess.CompletedProcess[bytes]: | |
| info(f"Running: {display_command(command)}") | |
| result = subprocess.run( | |
| list(command), | |
| cwd=cwd, | |
| input=input_bytes, | |
| stdout=subprocess.PIPE if capture else None, | |
| stderr=subprocess.PIPE if capture else None, | |
| check=False, | |
| ) | |
| if check and result.returncode != 0: | |
| details = "" | |
| if capture: | |
| output = (result.stdout + result.stderr).decode("utf-8", errors="replace").strip() | |
| if output: | |
| details = f"\n{output}" | |
| raise GeneratorError(f"Command failed with exit code {result.returncode}: {display_command(command)}{details}") | |
| return result | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE, help=f"GLM-5.2-Vision Hugging Face directory (default: {DEFAULT_SOURCE})") | |
| parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT, help=f"Output GGUF path (default: {DEFAULT_OUTPUT})") | |
| parser.add_argument("--llama-cpp", type=Path, default=DEFAULT_LLAMA_CPP, help=f"llama.cpp checkout (default: {DEFAULT_LLAMA_CPP})") | |
| parser.add_argument("--python", type=Path, help="Python interpreter with llama.cpp conversion dependencies") | |
| parser.add_argument("--jobs", type=int, default=max(1, os.cpu_count() or 1), help="Parallel CMake build jobs") | |
| parser.add_argument("--force", action="store_true", help="Regenerate and atomically replace an existing output") | |
| parser.add_argument("--check-only", action="store_true", help="Validate prerequisites without building or converting") | |
| return parser.parse_args() | |
| def resolve_directory(path: Path, label: str) -> Path: | |
| resolved = path.expanduser().resolve() | |
| if not resolved.is_dir(): | |
| raise GeneratorError(f"{label} is not a directory: {resolved}") | |
| return resolved | |
| def read_json(path: Path) -> dict[str, Any]: | |
| try: | |
| with path.open("r", encoding="utf-8") as handle: | |
| value = json.load(handle) | |
| except (OSError, json.JSONDecodeError) as exc: | |
| raise GeneratorError(f"Cannot read JSON file {path}: {exc}") from exc | |
| if not isinstance(value, dict): | |
| raise GeneratorError(f"Expected a JSON object in {path}") | |
| return value | |
| def read_safetensors_header(path: Path) -> dict[str, dict[str, Any]]: | |
| try: | |
| file_size = path.stat().st_size | |
| with path.open("rb") as handle: | |
| raw_length = handle.read(8) | |
| if len(raw_length) != 8: | |
| raise GeneratorError(f"Invalid Safetensors header in {path}") | |
| header_length = struct.unpack("<Q", raw_length)[0] | |
| if header_length <= 0 or header_length > file_size - 8: | |
| raise GeneratorError(f"Invalid Safetensors header length in {path}: {header_length}") | |
| header = json.loads(handle.read(header_length)) | |
| except (OSError, json.JSONDecodeError) as exc: | |
| raise GeneratorError(f"Cannot read Safetensors header {path}: {exc}") from exc | |
| tensors = {name: value for name, value in header.items() if name != "__metadata__"} | |
| if not tensors or not all(isinstance(value, dict) for value in tensors.values()): | |
| raise GeneratorError(f"No valid tensor entries found in {path}") | |
| data_size = file_size - 8 - header_length | |
| for name, tensor in tensors.items(): | |
| offsets = tensor.get("data_offsets") | |
| if not isinstance(offsets, list) or len(offsets) != 2: | |
| raise GeneratorError(f"Invalid data offsets for {name} in {path}") | |
| start, end = offsets | |
| if not isinstance(start, int) or not isinstance(end, int) or not 0 <= start <= end <= data_size: | |
| raise GeneratorError(f"Out-of-range data offsets for {name} in {path}") | |
| return tensors | |
| def validate_source(source: Path) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]: | |
| config_path = source / "config.json" | |
| preprocessor_path = source / "preprocessor_config.json" | |
| vision_path = source / VISION_FILENAME | |
| projector_path = source / PROJECTOR_FILENAME | |
| for path in (config_path, preprocessor_path, vision_path, projector_path): | |
| if not path.is_file(): | |
| raise GeneratorError(f"Required source file is missing: {path}") | |
| config = read_json(config_path) | |
| architectures = config.get("architectures", []) | |
| if "Glm5vForConditionalGeneration" not in architectures: | |
| raise GeneratorError(f"Unexpected model architecture in {config_path}: {architectures}") | |
| text_config = config.get("text_config") | |
| vision_config = config.get("vision_config") | |
| if not isinstance(text_config, dict) or text_config.get("hidden_size") != 6144: | |
| raise GeneratorError("GLM-5.2 text hidden size must be 6144") | |
| expected_vision = { | |
| "hidden_size": 1152, | |
| "intermediate_size": 4304, | |
| "num_attention_heads": 16, | |
| "num_hidden_layers": 27, | |
| "patch_size": 14, | |
| "merge_kernel_size": [2, 2], | |
| } | |
| if not isinstance(vision_config, dict): | |
| raise GeneratorError("vision_config is missing from config.json") | |
| mismatches = {key: (vision_config.get(key), expected) for key, expected in expected_vision.items() if vision_config.get(key) != expected} | |
| if mismatches: | |
| raise GeneratorError(f"Unexpected GLM-5.2 vision configuration values: {mismatches}") | |
| preprocessor = read_json(preprocessor_path) | |
| media_config = preprocessor.get("media_proc_cfg") | |
| if not isinstance(media_config, dict) or media_config.get("patch_size") != 14: | |
| raise GeneratorError("Expected MoonViT media_proc_cfg with patch_size 14") | |
| vision_tensors = read_safetensors_header(vision_path) | |
| projector_tensors = read_safetensors_header(projector_path) | |
| if len(vision_tensors) != EXPECTED_VISION_TENSORS: | |
| raise GeneratorError(f"Expected {EXPECTED_VISION_TENSORS} vision tensors, found {len(vision_tensors)}") | |
| if len(projector_tensors) != EXPECTED_PROJECTOR_TENSORS: | |
| raise GeneratorError(f"Expected {EXPECTED_PROJECTOR_TENSORS} projector tensors, found {len(projector_tensors)}") | |
| if any(not name.startswith("vision_tower.") for name in vision_tensors): | |
| raise GeneratorError("The vision Safetensors file contains a non-vision tensor") | |
| if any(not name.startswith("mm_projector.") for name in projector_tensors): | |
| raise GeneratorError("The projector Safetensors file contains a non-projector tensor") | |
| dtypes = {tensor.get("dtype") for tensor in [*vision_tensors.values(), *projector_tensors.values()]} | |
| if dtypes != {"BF16"}: | |
| raise GeneratorError(f"Expected BF16 vision and projector source tensors, found: {sorted(str(value) for value in dtypes)}") | |
| info(f"Validated source: {len(vision_tensors)} vision tensors + {len(projector_tensors)} projector tensors, all BF16") | |
| return vision_tensors, projector_tensors | |
| def validate_glm5v_support(llama_cpp: Path) -> None: | |
| # GLM-5.2-Vision reuses the Kimi-K2.5 projector; the image begin/end tokens are | |
| # resolved at runtime from the text model vocab instead of a dedicated projector type | |
| required_markers = { | |
| "conversion/__init__.py": '"Glm5vForConditionalGeneration": "kimivl"', | |
| "conversion/kimivl.py": '@ModelBase.register("Glm5vForConditionalGeneration")', | |
| "tools/mtmd/clip-impl.h": "PROJECTOR_TYPE_KIMIK25", | |
| "tools/mtmd/mtmd.cpp": 'lookup_token("<|begin_of_image|>")', | |
| } | |
| missing: list[str] = [] | |
| for relative_path, marker in required_markers.items(): | |
| path = llama_cpp / relative_path | |
| try: | |
| contents = path.read_text(encoding="utf-8") | |
| except OSError: | |
| missing.append(relative_path) | |
| continue | |
| if marker not in contents: | |
| missing.append(relative_path) | |
| if missing: | |
| raise GeneratorError(f"llama.cpp does not contain the GLM-5.2-Vision support from {LLAMA_CPP_PR_URL}; missing support in: {', '.join(missing)}") | |
| info(f"Validated llama.cpp GLM-5.2-Vision support from {LLAMA_CPP_PR_URL}") | |
| def interpreter_has_dependencies(python: Path, llama_cpp: Path) -> bool: | |
| probe = "import numpy, safetensors, torch, transformers; print('conversion dependencies OK')" | |
| result = run([str(python), "-c", probe], capture=True, check=False) | |
| if result.returncode != 0: | |
| return False | |
| converter = llama_cpp / "convert_hf_to_gguf.py" | |
| return run([str(python), str(converter), "--help"], capture=True, check=False).returncode == 0 | |
| def select_python(explicit: Path | None, llama_cpp: Path) -> Path: | |
| if explicit is not None: | |
| candidates: Iterable[Path] = (explicit.expanduser(),) | |
| else: | |
| candidates = (Path(sys.executable), Path("/usr/bin/python3")) | |
| checked: set[Path] = set() | |
| for candidate in candidates: | |
| candidate = candidate.expanduser() | |
| executable = candidate if candidate.is_absolute() else Path(shutil.which(str(candidate)) or candidate) | |
| try: | |
| resolved = executable.resolve(strict=True) | |
| except OSError: | |
| continue | |
| if resolved in checked or not os.access(executable, os.X_OK): | |
| continue | |
| checked.add(resolved) | |
| if interpreter_has_dependencies(executable, llama_cpp): | |
| info(f"Using converter Python: {executable}") | |
| return executable | |
| if explicit is not None: | |
| raise GeneratorError(f"The requested Python interpreter lacks llama.cpp conversion dependencies: {explicit}") | |
| raise GeneratorError("No Python interpreter with torch, transformers, numpy, and safetensors was found; use --python PATH") | |
| def validate_build(llama_cpp: Path) -> Path: | |
| converter = llama_cpp / "convert_hf_to_gguf.py" | |
| build_dir = llama_cpp / "build" | |
| if not converter.is_file(): | |
| raise GeneratorError(f"llama.cpp converter is missing: {converter}") | |
| if not (llama_cpp / ".git").exists(): | |
| raise GeneratorError(f"llama.cpp path is not a Git checkout: {llama_cpp}") | |
| if not (build_dir / "CMakeCache.txt").is_file(): | |
| raise GeneratorError(f"llama.cpp build directory is not configured: {build_dir}") | |
| return build_dir | |
| def build_llama_cpp(llama_cpp: Path, build_dir: Path, jobs: int) -> None: | |
| if jobs < 1: | |
| raise GeneratorError("--jobs must be at least 1") | |
| cmake = shutil.which("cmake") | |
| if cmake is None: | |
| raise GeneratorError("cmake is not available on PATH") | |
| info("Refreshing the existing llama.cpp CMake configuration") | |
| run([cmake, "-S", str(llama_cpp), "-B", str(build_dir)], cwd=llama_cpp) | |
| info(f"Building GLM5V-capable llama.cpp targets with {jobs} jobs") | |
| run([cmake, "--build", str(build_dir), "--parallel", str(jobs), "--target", *BUILD_TARGETS], cwd=llama_cpp) | |
| missing = [name for name in BUILD_TARGETS if not (build_dir / "bin" / name).is_file()] | |
| if missing: | |
| raise GeneratorError(f"Build completed without expected executables: {missing}") | |
| def tensor_data_size(tensors: dict[str, dict[str, Any]]) -> int: | |
| return sum(int(tensor["data_offsets"][1]) - int(tensor["data_offsets"][0]) for tensor in tensors.values()) | |
| def stage_mmproj_source( | |
| source: Path, | |
| staging: Path, | |
| vision_tensors: dict[str, dict[str, Any]], | |
| projector_tensors: dict[str, dict[str, Any]], | |
| ) -> None: | |
| for name in ("config.json", "preprocessor_config.json", "README.md", "generation_config.json"): | |
| source_file = source / name | |
| if source_file.is_file(): | |
| shutil.copy2(source_file, staging / name) | |
| vision_part = "model-00001-of-00002.safetensors" | |
| projector_part = "model-00002-of-00002.safetensors" | |
| (staging / vision_part).symlink_to(source / VISION_FILENAME) | |
| (staging / projector_part).symlink_to(source / PROJECTOR_FILENAME) | |
| weight_map = {name: vision_part for name in vision_tensors} | |
| weight_map.update({name: projector_part for name in projector_tensors}) | |
| index = { | |
| "metadata": {"total_size": tensor_data_size(vision_tensors) + tensor_data_size(projector_tensors)}, | |
| "weight_map": weight_map, | |
| } | |
| with (staging / "model.safetensors.index.json").open("w", encoding="utf-8") as handle: | |
| json.dump(index, handle, indent=2, sort_keys=True) | |
| handle.write("\n") | |
| GGUF_VALIDATOR = r""" | |
| import json | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, sys.argv[2]) | |
| from gguf import GGUFReader | |
| reader = GGUFReader(sys.argv[1], "r") | |
| def value(name): | |
| field = reader.get_field(name) | |
| if field is None: | |
| raise RuntimeError(f"missing GGUF metadata field: {name}") | |
| return field.contents() | |
| expected = { | |
| "general.type": "mmproj", | |
| "general.architecture": "clip", | |
| "clip.projector_type": "kimik25", | |
| "clip.has_vision_encoder": True, | |
| "clip.vision.projection_dim": 6144, | |
| } | |
| for key, wanted in expected.items(): | |
| actual = value(key) | |
| if actual != wanted: | |
| raise RuntimeError(f"unexpected {key}: expected {wanted!r}, got {actual!r}") | |
| if len(reader.tensors) != 335: | |
| raise RuntimeError(f"expected 335 tensors, found {len(reader.tensors)}") | |
| names = {tensor.name for tensor in reader.tensors} | |
| required_names = {"v.patch_embd.weight", "mm.input_norm.weight", "mm.1.weight", "mm.2.weight"} | |
| missing = sorted(required_names - names) | |
| if missing: | |
| raise RuntimeError(f"missing expected tensors: {missing}") | |
| types = sorted({tensor.tensor_type.name for tensor in reader.tensors}) | |
| if not set(types).issubset({"F16", "F32"}): | |
| raise RuntimeError(f"unexpected tensor types in F16 mmproj: {types}") | |
| print(json.dumps({"tensor_count": len(reader.tensors), "tensor_types": types, "projector_type": value("clip.projector_type"), "projection_dim": value("clip.vision.projection_dim")})) | |
| """ | |
| def validate_output(output: Path, python: Path, llama_cpp: Path) -> dict[str, Any]: | |
| if not output.is_file(): | |
| raise GeneratorError(f"Output GGUF does not exist: {output}") | |
| result = run( | |
| [str(python), "-c", GGUF_VALIDATOR, str(output), str(llama_cpp / "gguf-py")], | |
| capture=True, | |
| check=False, | |
| ) | |
| if result.returncode != 0: | |
| details = (result.stdout + result.stderr).decode("utf-8", errors="replace").strip() | |
| raise GeneratorError(f"GGUF validation failed for {output}:\n{details}") | |
| try: | |
| summary = json.loads(result.stdout.decode("utf-8")) | |
| except json.JSONDecodeError as exc: | |
| raise GeneratorError(f"GGUF validator returned invalid output: {result.stdout!r}") from exc | |
| info(f"Validated GGUF: {summary['tensor_count']} tensors, types {summary['tensor_types']}, projector {summary['projector_type']}, output dimension {summary['projection_dim']}") | |
| return summary | |
| def sha256_file(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| for chunk in iter(lambda: handle.read(16 * 1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def convert( | |
| source: Path, | |
| output: Path, | |
| llama_cpp: Path, | |
| python: Path, | |
| vision_tensors: dict[str, dict[str, Any]], | |
| projector_tensors: dict[str, dict[str, Any]], | |
| ) -> None: | |
| output.parent.mkdir(parents=True, exist_ok=True) | |
| if shutil.disk_usage(output.parent).free < MIN_FREE_BYTES: | |
| raise GeneratorError(f"At least {MIN_FREE_BYTES:,} free bytes are required in {output.parent}") | |
| with tempfile.TemporaryDirectory(prefix=".glm5v-mmproj-", dir=output.parent) as temporary: | |
| staging = Path(temporary) | |
| stage_mmproj_source(source, staging, vision_tensors, projector_tensors) | |
| temporary_output = staging / output.name | |
| command = [ | |
| str(python), | |
| str(llama_cpp / "convert_hf_to_gguf.py"), | |
| str(staging), | |
| "--mmproj", | |
| "--outtype", "f16", | |
| "--model-name", "GLM-5.2-Vision", | |
| "--outfile", str(temporary_output), | |
| ] | |
| info("Converting the BF16 MoonViT tower and projector to an F16 GGUF") | |
| run(command, cwd=llama_cpp) | |
| validate_output(temporary_output, python, llama_cpp) | |
| os.replace(temporary_output, output) | |
| def main() -> int: | |
| args = parse_args() | |
| if args.jobs < 1: | |
| raise GeneratorError("--jobs must be at least 1") | |
| source = resolve_directory(args.source, "Source model") | |
| llama_cpp = resolve_directory(args.llama_cpp, "llama.cpp checkout") | |
| output = args.output.expanduser().resolve() | |
| vision_tensors, projector_tensors = validate_source(source) | |
| build_dir = validate_build(llama_cpp) | |
| validate_glm5v_support(llama_cpp) | |
| python = select_python(args.python, llama_cpp) | |
| if args.check_only: | |
| if output.exists(): | |
| validate_output(output, python, llama_cpp) | |
| info("All prerequisite checks passed; no files were changed") | |
| return 0 | |
| build_llama_cpp(llama_cpp, build_dir, args.jobs) | |
| if output.exists() and not args.force: | |
| try: | |
| validate_output(output, python, llama_cpp) | |
| except GeneratorError as exc: | |
| raise GeneratorError(f"Output already exists but is invalid; use --force to replace it: {exc}") from exc | |
| info(f"Valid output already exists; use --force to regenerate it: {output}") | |
| else: | |
| convert(source, output, llama_cpp, python, vision_tensors, projector_tensors) | |
| digest = sha256_file(output) | |
| info(f"Created: {output}") | |
| info(f"Size: {output.stat().st_size:,} bytes") | |
| info(f"SHA-256: {digest}") | |
| return 0 | |
| if __name__ == "__main__": | |
| try: | |
| raise SystemExit(main()) | |
| except (GeneratorError, KeyboardInterrupt) as exc: | |
| print(f"[glm5v-mmproj] ERROR: {exc}", file=sys.stderr) | |
| raise SystemExit(1) | |