0xSero's picture
Add reproduction article + graft bundle (stage.sh, prepare_checkpoint.py, sha256 manifest)
b07fd83 verified
|
Raw
History Blame Contribute Delete
8.55 kB
# 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).