| # Giving a 753B Model Eyes: Reproducing GLM-5.2 Vision on 4Γ 96 GB GPUs |
|
|
| *How [GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision](https://huggingface.co/0xSero/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision) was made: grafting the GLM-5.2-Vision tower onto madeby561's 4-card NF3 hybrid, and serving 250k context with vision on a single workstation.* |
|
|
| --- |
|
|
| The starting point is [madeby561/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid](https://huggingface.co/madeby561/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid) β the full 753B GLM-5.2, all 256 experts per layer, no pruning, compressed to β341 GB with a three-tier scheme (NVFP4 for the 64 damage-ranked experts per layer, an in-house NF3 3-bit format for the other 192, MXFP8-served BF16 for everything else). It runs on four 96 GB sm120 GPUs and scores within one GPQA-Diamond question of full precision. |
|
|
| But it's blind. The official vision variant exists as [baseten/GLM-5.2-Vision-NVFP4](https://huggingface.co/baseten/GLM-5.2-Vision-NVFP4) β 435 GB-class, needs 8 GPUs. |
|
|
| The observation that makes this work: **the vision tower and projector are tiny and quantization-independent of the text backbone.** GLM-5.2-Vision's eyes are a MoonViT-3d tower plus a multimodal projector β β0.93 GB in NVFP4. Nothing about them cares whether the text experts underneath are FP8, NVFP4, or NF3. So you can lift them off baseten's checkpoint and bolt them onto the 4-card hybrid, byte-exact on both sides. |
|
|
| ## The five-minute path |
|
|
| If you just want to run it: |
|
|
| ```bash |
| hf download 0xSero/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision --local-dir ./glm52-vision |
| MODEL_DIR=./glm52-vision docker compose up # compose file ships in the repo |
| ``` |
|
|
| Serving image: `ghcr.io/0xsero/glm52-nf3-vision:v3-attn`. Needs 4Γ 96 GB sm120 GPUs. The rest of this article is how that artifact was produced, so you can rebuild it from primary sources β or repeat the trick on the next checkpoint pair. |
|
|
| ## Step 1 β Graft the checkpoint |
|
|
| Everything in this step is in the model repo under [`graft/`](https://huggingface.co/0xSero/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision/tree/main/graft). |
|
|
| ```bash |
| SOURCE_DIR=/models/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid \ |
| TARGET_DIR=/models/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision \ |
| ./stage.sh |
| ``` |
|
|
| `stage.sh` downloads exactly eight files from baseten's repo at a **pinned revision** (`f6eab611β¦`): the vision tower and projector weights, the vision processor code (`kimi_k25_*.py`, `media_utils.py`, `preprocessor_config.json`), the vision `config.json`, and the vision chat template. `prepare_checkpoint.py` then: |
|
|
| 1. **Verifies every downloaded file against a sha256 manifest** (`asset-manifest.json`) β a moved revision or truncated download fails loudly before any assembly starts. |
| 2. **Hardlinks the 184 text shards** from the source checkpoint into the target β the graft costs β1 GB of disk, not 342 GB, and the text weights are byte-identical by construction, so every accuracy number measured on the base model carries over. |
| 3. **Performs the config surgery**: baseten's vision config becomes the outer `config.json` with `"architectures": ["Glm5vForConditionalGeneration"]` and `"model_type": "glm5v"`; the hybrid's entire config is embedded as `text_config`; the hybrid's `quantization_config` is hoisted to the top level so the loader still sees the NF3/NVFP4 tiering. |
| 4. **Merges the safetensors index**: the tower and projector tensors are appended to `model.safetensors.index.json`, and `total_parameters`/`total_size` are updated from the actual safetensors headers. |
| 5. **Writes `VISION_PROVENANCE.json`** β source hashes, vision repo + revision, parameter and byte counts β and assembles everything in a `.partial` directory that is atomically renamed at the end, so a crashed run never leaves a half-checkpoint. |
| |
| One post-graft fix that cost us a debugging session: the vision chat template changes the stop behavior, and generation initially would not terminate. The shipped `config.json` carries the EOS fix β if you re-derive the config yourself, compare against the shipped one. |
| |
| ## Step 2 β The serving plugin |
| |
| vLLM has no `Glm5vForConditionalGeneration`. The trick is that it *does* have a complete implementation of the same vision architecture under a different name: Kimi-K2.5's MoonViT stack. The [`serving/`](https://huggingface.co/0xSero/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision/tree/main/serving) folder contains `glm52-vision-vllm`, a ~200-line package that bridges the two: |
| |
| - registers `Glm5vForConditionalGeneration` in vLLM's model registry via the standard `vllm.general_plugins` entry point (no vLLM source edits β `pip install` and it's active); |
| - reuses vLLM's `KimiK25ForConditionalGeneration` / MoonViT / projector classes for the vision path, resolves GLM's `<|image|>` placeholder token, and instantiates the text backbone from the embedded `text_config`; |
| - propagates the sparse-indexer attention overrides (`use_index_cache`, the 78-character `index_topk_pattern`) from the outer config into the text config, and patches vLLM's `SpeculativeConfig` so MTP setup reads the text config rather than choking on the multimodal wrapper. |
| |
| ## Step 3 β Build the image |
| |
| The Dockerfile is four lines on top of madeby561's public serving image (which contains the NF3 kernel and hybrid loader): |
| |
| ```dockerfile |
| ARG BASE_IMAGE=madeby561/vllm-glm52-nvfp4-nf3-hybrid:v3 |
| FROM ${BASE_IMAGE} |
| COPY pyproject.toml /opt/glm52-vision/pyproject.toml |
| COPY src /opt/glm52-vision/src |
| RUN /opt/venv/bin/pip install --no-deps /opt/glm52-vision |
| ``` |
| |
| ```bash |
| cd serving && docker build -t glm52-nf3-vision:v3-attn . |
| ``` |
| |
| The prebuilt result is `ghcr.io/0xsero/glm52-nf3-vision:v3-attn`. |
| |
| ## Step 4 β Serve |
| |
| The repo's `docker-compose.yml` is the exact measured configuration. The load-bearing choices: |
| |
| | Flag | Value | Why | |
| |---|---|---| |
| | `--tensor-parallel-size` / `--decode-context-parallel-size` | 4 / 4 | DCP4 shards the KV cache across all four GPUs β this is what buys the big pool | |
| | `--kv-cache-dtype` | `fp8` | `nvfp4_ds_mla` KV is unsupported by the MLA backend for sm120 in this runtime β and costs 30β50% decode where it does run | |
| | `--gpu-memory-utilization` | 0.974 | weights take β85β88 GiB/GPU; this leaves the pool below | |
| | `--max-model-len` | 250000 | fits the measured pool with 2Γ concurrency headroom | |
| | `--attention-backend` / `--moe-backend` | `B12X_MLA_SPARSE` / `b12x` | the custom sparse-MLA + NF3/NVFP4 MoE kernels | |
| | speculative config | **absent** | see gotchas | |
| |
| Boot takes β2β3 min (fastsafetensors, β€2 GB shards). The line to look for: |
| |
| ```text |
| GPU KV cache size: 509,467 tokens |
| Maximum concurrency for 250,000 tokens per request: 2.04x |
| ``` |
| |
| ## Step 5 β Validate |
| |
| Text, then vision, against the OpenAI-compatible endpoint: |
| |
| ```bash |
| curl -s http://localhost:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{ |
| "model": "GLM-5.2-Vision", |
| "messages": [{"role":"user","content":[ |
| {"type":"image_url","image_url":{"url":"https://ultralytics.com/images/bus.jpg"}}, |
| {"type":"text","text":"What vehicle is in this image? One word."}]}] |
| }' |
| ``` |
| |
| Expected: `bus`. Image tokens come out of the same 250k context pool as text. |
| |
| ## Gotchas, honestly |
| |
| - **MTP speculation is off.** The five-token MTP draft head generates drafts on the graft, but the accepted-token counter stays at zero. Rather than ship speculation that silently never accepts, the compose omits `--speculative-config`. Single-stream decode is therefore β24β37 tok/s on a PCIe-only (no-P2P) 4-card box β if your platform has working P2P, expect meaningfully more. |
| - **Vision quality is inherited, not re-verified.** The tower rides byte-exact NVFP4 from baseten; the text tier is byte-exact madeby561 v3.6 (GPQA-Diamond 88.89 carries over). But no vision benchmark suite has been run on the combined artifact β validation so far is smoke-level. |
| - **Trust the manifest, not the tag.** Every external input is pinned: the baseten revision, the sha256 of all eight vision assets, the base image tag, and the published image digest. If any hash fails, stop β a silently different tower is the kind of bug you find weeks later. |
| |
| ## Credits |
| |
| Text hybrid, NF3 format + kernel, and serving image by [madeby561](https://huggingface.co/madeby561/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid). NVFP4 expert tier by [Luke Alonso](https://huggingface.co/lukealonso/GLM-5.2-NVFP4). Vision tower NVFP4 by [baseten](https://huggingface.co/baseten/GLM-5.2-Vision-NVFP4). MoonViT serving stack from vLLM's Kimi-K2.5 implementation. Graft + plugin: [0xSero](https://huggingface.co/0xSero). |
| |