Add reproduction article + graft bundle (stage.sh, prepare_checkpoint.py, sha256 manifest)
Browse files- REPRODUCING.md +113 -0
- graft/asset-manifest.json +33 -0
- graft/configuration_glm5v.py +120 -0
- graft/prepare_checkpoint.py +152 -0
- graft/stage.sh +27 -0
REPRODUCING.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Giving a 753B Model Eyes: Reproducing GLM-5.2 Vision on 4× 96 GB GPUs
|
| 2 |
+
|
| 3 |
+
*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.*
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
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.
|
| 8 |
+
|
| 9 |
+
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.
|
| 10 |
+
|
| 11 |
+
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.
|
| 12 |
+
|
| 13 |
+
## The five-minute path
|
| 14 |
+
|
| 15 |
+
If you just want to run it:
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
hf download 0xSero/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision --local-dir ./glm52-vision
|
| 19 |
+
MODEL_DIR=./glm52-vision docker compose up # compose file ships in the repo
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
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.
|
| 23 |
+
|
| 24 |
+
## Step 1 — Graft the checkpoint
|
| 25 |
+
|
| 26 |
+
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).
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
SOURCE_DIR=/models/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid \
|
| 30 |
+
TARGET_DIR=/models/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision \
|
| 31 |
+
./stage.sh
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
`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:
|
| 35 |
+
|
| 36 |
+
1. **Verifies every downloaded file against a sha256 manifest** (`asset-manifest.json`) — a moved revision or truncated download fails loudly before any assembly starts.
|
| 37 |
+
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.
|
| 38 |
+
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.
|
| 39 |
+
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.
|
| 40 |
+
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.
|
| 41 |
+
|
| 42 |
+
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.
|
| 43 |
+
|
| 44 |
+
## Step 2 — The serving plugin
|
| 45 |
+
|
| 46 |
+
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:
|
| 47 |
+
|
| 48 |
+
- 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);
|
| 49 |
+
- 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`;
|
| 50 |
+
- 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.
|
| 51 |
+
|
| 52 |
+
## Step 3 — Build the image
|
| 53 |
+
|
| 54 |
+
The Dockerfile is four lines on top of madeby561's public serving image (which contains the NF3 kernel and hybrid loader):
|
| 55 |
+
|
| 56 |
+
```dockerfile
|
| 57 |
+
ARG BASE_IMAGE=madeby561/vllm-glm52-nvfp4-nf3-hybrid:v3
|
| 58 |
+
FROM ${BASE_IMAGE}
|
| 59 |
+
COPY pyproject.toml /opt/glm52-vision/pyproject.toml
|
| 60 |
+
COPY src /opt/glm52-vision/src
|
| 61 |
+
RUN /opt/venv/bin/pip install --no-deps /opt/glm52-vision
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
cd serving && docker build -t glm52-nf3-vision:v3-attn .
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
The prebuilt result is `ghcr.io/0xsero/glm52-nf3-vision:v3-attn`.
|
| 69 |
+
|
| 70 |
+
## Step 4 — Serve
|
| 71 |
+
|
| 72 |
+
The repo's `docker-compose.yml` is the exact measured configuration. The load-bearing choices:
|
| 73 |
+
|
| 74 |
+
| Flag | Value | Why |
|
| 75 |
+
|---|---|---|
|
| 76 |
+
| `--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 |
|
| 77 |
+
| `--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 |
|
| 78 |
+
| `--gpu-memory-utilization` | 0.974 | weights take ≈85–88 GiB/GPU; this leaves the pool below |
|
| 79 |
+
| `--max-model-len` | 250000 | fits the measured pool with 2× concurrency headroom |
|
| 80 |
+
| `--attention-backend` / `--moe-backend` | `B12X_MLA_SPARSE` / `b12x` | the custom sparse-MLA + NF3/NVFP4 MoE kernels |
|
| 81 |
+
| speculative config | **absent** | see gotchas |
|
| 82 |
+
|
| 83 |
+
Boot takes ≈2–3 min (fastsafetensors, ≤2 GB shards). The line to look for:
|
| 84 |
+
|
| 85 |
+
```text
|
| 86 |
+
GPU KV cache size: 509,467 tokens
|
| 87 |
+
Maximum concurrency for 250,000 tokens per request: 2.04x
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
## Step 5 — Validate
|
| 91 |
+
|
| 92 |
+
Text, then vision, against the OpenAI-compatible endpoint:
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
curl -s http://localhost:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{
|
| 96 |
+
"model": "GLM-5.2-Vision",
|
| 97 |
+
"messages": [{"role":"user","content":[
|
| 98 |
+
{"type":"image_url","image_url":{"url":"https://ultralytics.com/images/bus.jpg"}},
|
| 99 |
+
{"type":"text","text":"What vehicle is in this image? One word."}]}]
|
| 100 |
+
}'
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Expected: `bus`. Image tokens come out of the same 250k context pool as text.
|
| 104 |
+
|
| 105 |
+
## Gotchas, honestly
|
| 106 |
+
|
| 107 |
+
- **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.
|
| 108 |
+
- **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.
|
| 109 |
+
- **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.
|
| 110 |
+
|
| 111 |
+
## Credits
|
| 112 |
+
|
| 113 |
+
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).
|
graft/asset-manifest.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"revision": "f6eab6117386a0c69152fdf272dc65bfd0254f9f",
|
| 3 |
+
"repository": "baseten/GLM-5.2-Vision-NVFP4",
|
| 4 |
+
"files": {
|
| 5 |
+
"chat_template.jinja": {
|
| 6 |
+
"sha256": "6d172cae09cb494bd825a6b14d07e7b32a2c26988afe43d8bc2bbda91aab99c8"
|
| 7 |
+
},
|
| 8 |
+
"config.json": {
|
| 9 |
+
"sha256": "5a4c1c2e388e28373dcdd9d38c968246a23a261f7ad49908d94823f3a4616f65"
|
| 10 |
+
},
|
| 11 |
+
"kimi_k25_processor.py": {
|
| 12 |
+
"sha256": "cbfe0d8ca280568605011927fc80a515451ec6d1c01519ff89bd6c09dec702c4"
|
| 13 |
+
},
|
| 14 |
+
"kimi_k25_vision_processing.py": {
|
| 15 |
+
"sha256": "498e20753c9aa6b6224ddbcd05b04deb7e987b76595bf5c28ee8583f33ebc375"
|
| 16 |
+
},
|
| 17 |
+
"media_utils.py": {
|
| 18 |
+
"sha256": "77a6b1fed3580b0562443092928a0240c2989e3950377e93893952123ae48e3d"
|
| 19 |
+
},
|
| 20 |
+
"mm_projector.safetensors": {
|
| 21 |
+
"sha256": "e7c6ce8c27424f292e708e7bbb48ade57ea9f1aaddd28bd6a1020a860d9db80c",
|
| 22 |
+
"size": 99117136
|
| 23 |
+
},
|
| 24 |
+
"preprocessor_config.json": {
|
| 25 |
+
"sha256": "b938c8d816d50579bd8d9bba2ff0ffb738c528b9b05c6966c186d30a1a065041"
|
| 26 |
+
},
|
| 27 |
+
"vision_tower.safetensors": {
|
| 28 |
+
"sha256": "af1c927919bd073c2ab893bf2c6d9826100a06828c3b40f0128fabb4660a610d",
|
| 29 |
+
"size": 833769904
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
graft/configuration_glm5v.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import AutoConfig
|
| 2 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Glm5vVisionConfig(PretrainedConfig):
|
| 6 |
+
model_type = "glm5v_vision"
|
| 7 |
+
|
| 8 |
+
def __init__(
|
| 9 |
+
self,
|
| 10 |
+
patch_size: int = 14,
|
| 11 |
+
init_pos_emb_height: int = 64,
|
| 12 |
+
init_pos_emb_width: int = 64,
|
| 13 |
+
init_pos_emb_time: int = 4,
|
| 14 |
+
pos_emb_type: str = "divided_fixed",
|
| 15 |
+
num_attention_heads: int = 16,
|
| 16 |
+
num_hidden_layers: int = 27,
|
| 17 |
+
hidden_size: int = 1152,
|
| 18 |
+
intermediate_size: int = 4304,
|
| 19 |
+
merge_kernel_size=(2, 2),
|
| 20 |
+
video_attn_type: str = "spatial_temporal",
|
| 21 |
+
merge_type: str = "sd2_tpool",
|
| 22 |
+
mm_projector_type: str = "patchmerger",
|
| 23 |
+
mm_hidden_size: int = 6144,
|
| 24 |
+
vt_hidden_size: int | None = None,
|
| 25 |
+
projector_hidden_act: str = "gelu",
|
| 26 |
+
projector_ln_eps: float = 1e-5,
|
| 27 |
+
text_hidden_size: int = 6144,
|
| 28 |
+
**kwargs,
|
| 29 |
+
):
|
| 30 |
+
super().__init__(**kwargs)
|
| 31 |
+
self.patch_size = patch_size
|
| 32 |
+
self.init_pos_emb_height = init_pos_emb_height
|
| 33 |
+
self.init_pos_emb_width = init_pos_emb_width
|
| 34 |
+
self.init_pos_emb_time = init_pos_emb_time
|
| 35 |
+
self.pos_emb_type = pos_emb_type
|
| 36 |
+
self.num_attention_heads = num_attention_heads
|
| 37 |
+
self.num_hidden_layers = num_hidden_layers
|
| 38 |
+
self.hidden_size = hidden_size
|
| 39 |
+
self.intermediate_size = intermediate_size
|
| 40 |
+
self.merge_kernel_size = merge_kernel_size
|
| 41 |
+
self.video_attn_type = video_attn_type
|
| 42 |
+
self.merge_type = merge_type
|
| 43 |
+
self.mm_projector_type = mm_projector_type
|
| 44 |
+
self.mm_hidden_size = mm_hidden_size
|
| 45 |
+
self.vt_hidden_size = vt_hidden_size if vt_hidden_size is not None else hidden_size
|
| 46 |
+
self.projector_hidden_act = projector_hidden_act
|
| 47 |
+
self.projector_ln_eps = projector_ln_eps
|
| 48 |
+
self.text_hidden_size = text_hidden_size
|
| 49 |
+
|
| 50 |
+
def __getattr__(self, name):
|
| 51 |
+
if name.startswith("vt_"):
|
| 52 |
+
values = object.__getattribute__(self, "__dict__")
|
| 53 |
+
base_name = name[3:]
|
| 54 |
+
if base_name in values:
|
| 55 |
+
return values[base_name]
|
| 56 |
+
raise AttributeError(name)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class Glm5vConfig(PretrainedConfig):
|
| 60 |
+
model_type = "glm5v"
|
| 61 |
+
|
| 62 |
+
def __init__(
|
| 63 |
+
self,
|
| 64 |
+
text_config=None,
|
| 65 |
+
vision_config=None,
|
| 66 |
+
ignore_index: int = -100,
|
| 67 |
+
media_placeholder_token_id: int = 154854,
|
| 68 |
+
pad_token_id: int = 154820,
|
| 69 |
+
use_unified_vision_chunk: bool = True,
|
| 70 |
+
video_placeholder: str = "<|glm5v_video_placeholder|>",
|
| 71 |
+
encoder_only: bool = False,
|
| 72 |
+
language_only: bool = False,
|
| 73 |
+
**kwargs,
|
| 74 |
+
):
|
| 75 |
+
if vision_config is None:
|
| 76 |
+
self.vision_config = Glm5vVisionConfig()
|
| 77 |
+
elif isinstance(vision_config, dict):
|
| 78 |
+
self.vision_config = Glm5vVisionConfig(**vision_config)
|
| 79 |
+
else:
|
| 80 |
+
self.vision_config = vision_config
|
| 81 |
+
raw_text_config = dict(text_config) if isinstance(text_config, dict) else None
|
| 82 |
+
if text_config is None:
|
| 83 |
+
self.text_config = AutoConfig.for_model("glm_moe_dsa")
|
| 84 |
+
elif isinstance(text_config, dict):
|
| 85 |
+
normalized_text_config = dict(text_config)
|
| 86 |
+
normalized_text_config.setdefault("model_type", "glm_moe_dsa")
|
| 87 |
+
normalized_text_config.pop("layer_types", None)
|
| 88 |
+
self.text_config = AutoConfig.for_model(**normalized_text_config)
|
| 89 |
+
else:
|
| 90 |
+
self.text_config = text_config
|
| 91 |
+
if raw_text_config is not None:
|
| 92 |
+
for key in ("qk_rope_head_dim", "index_topk_freq"):
|
| 93 |
+
if key in raw_text_config:
|
| 94 |
+
setattr(self.text_config, key, raw_text_config[key])
|
| 95 |
+
if hasattr(self.text_config, "qk_nope_head_dim") and hasattr(
|
| 96 |
+
self.text_config,
|
| 97 |
+
"qk_rope_head_dim",
|
| 98 |
+
):
|
| 99 |
+
self.text_config.qk_head_dim = (
|
| 100 |
+
self.text_config.qk_nope_head_dim
|
| 101 |
+
+ self.text_config.qk_rope_head_dim
|
| 102 |
+
)
|
| 103 |
+
self.ignore_index = ignore_index
|
| 104 |
+
self.media_placeholder_token_id = media_placeholder_token_id
|
| 105 |
+
self.use_unified_vision_chunk = use_unified_vision_chunk
|
| 106 |
+
self.video_placeholder = video_placeholder
|
| 107 |
+
self.encoder_only = encoder_only
|
| 108 |
+
self.language_only = language_only
|
| 109 |
+
if getattr(self.text_config, "quantization_config", None) is not None:
|
| 110 |
+
self.quantization_config = self.text_config.quantization_config
|
| 111 |
+
super().__init__(pad_token_id=pad_token_id, **kwargs)
|
| 112 |
+
|
| 113 |
+
@property
|
| 114 |
+
def hidden_size(self) -> int:
|
| 115 |
+
return self.text_config.hidden_size
|
| 116 |
+
|
| 117 |
+
@property
|
| 118 |
+
def vocab_size(self) -> int:
|
| 119 |
+
return self.text_config.vocab_size
|
| 120 |
+
|
graft/prepare_checkpoint.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import hashlib
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import shutil
|
| 6 |
+
import struct
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
REPLACED_FILES = {
|
| 11 |
+
"chat_template.jinja",
|
| 12 |
+
"config.json",
|
| 13 |
+
"docker-compose.yml",
|
| 14 |
+
"model.safetensors.index.json",
|
| 15 |
+
"README.md",
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def sha256(path: Path) -> str:
|
| 20 |
+
digest = hashlib.sha256()
|
| 21 |
+
with path.open("rb") as handle:
|
| 22 |
+
while chunk := handle.read(8 * 1024 * 1024):
|
| 23 |
+
digest.update(chunk)
|
| 24 |
+
return digest.hexdigest()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def safetensors_header(path: Path) -> dict:
|
| 28 |
+
with path.open("rb") as handle:
|
| 29 |
+
header_size = struct.unpack("<Q", handle.read(8))[0]
|
| 30 |
+
return json.loads(handle.read(header_size))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def verify_assets(assets: Path, manifest_path: Path) -> dict:
|
| 34 |
+
manifest = json.loads(manifest_path.read_text())
|
| 35 |
+
for name, expected in manifest["files"].items():
|
| 36 |
+
path = assets / name
|
| 37 |
+
if not path.is_file():
|
| 38 |
+
raise FileNotFoundError(path)
|
| 39 |
+
if expected.get("size") is not None and path.stat().st_size != expected["size"]:
|
| 40 |
+
raise ValueError(f"Unexpected size for {path}")
|
| 41 |
+
if sha256(path) != expected["sha256"]:
|
| 42 |
+
raise ValueError(f"Unexpected hash for {path}")
|
| 43 |
+
return manifest
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def add_weight_file(index: dict, path: Path) -> tuple[int, int]:
|
| 47 |
+
header = safetensors_header(path)
|
| 48 |
+
parameters = 0
|
| 49 |
+
payload_bytes = 0
|
| 50 |
+
for name, tensor in header.items():
|
| 51 |
+
if name == "__metadata__":
|
| 52 |
+
continue
|
| 53 |
+
index["weight_map"][name] = path.name
|
| 54 |
+
tensor_parameters = 1
|
| 55 |
+
for dimension in tensor["shape"]:
|
| 56 |
+
tensor_parameters *= dimension
|
| 57 |
+
parameters += tensor_parameters
|
| 58 |
+
payload_bytes += tensor["data_offsets"][1] - tensor["data_offsets"][0]
|
| 59 |
+
return parameters, payload_bytes
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def build_config(source: Path, assets: Path) -> dict:
|
| 63 |
+
source_config = json.loads((source / "config.json").read_text())
|
| 64 |
+
baseten_config = json.loads((assets / "config.json").read_text())
|
| 65 |
+
config = dict(baseten_config)
|
| 66 |
+
config["architectures"] = ["Glm5vForConditionalGeneration"]
|
| 67 |
+
config["auto_map"] = {"AutoConfig": "configuration_glm5v.Glm5vConfig"}
|
| 68 |
+
config["model_type"] = "glm5v"
|
| 69 |
+
config["text_config"] = source_config
|
| 70 |
+
config["quantization_config"] = source_config["quantization_config"]
|
| 71 |
+
config["vision_config"] = dict(baseten_config["vision_config"])
|
| 72 |
+
config["vision_config"]["mm_hidden_size"] = source_config["hidden_size"]
|
| 73 |
+
config["vision_config"]["text_hidden_size"] = source_config["hidden_size"]
|
| 74 |
+
return config
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def assemble(source: Path, target: Path, assets: Path, bundle: Path) -> None:
|
| 78 |
+
if not source.is_dir():
|
| 79 |
+
raise FileNotFoundError(source)
|
| 80 |
+
if target.exists():
|
| 81 |
+
raise FileExistsError(target)
|
| 82 |
+
partial = target.with_name(f".{target.name}.partial-{os.getpid()}")
|
| 83 |
+
if partial.exists():
|
| 84 |
+
raise FileExistsError(partial)
|
| 85 |
+
partial.mkdir(parents=True)
|
| 86 |
+
try:
|
| 87 |
+
for source_path in source.iterdir():
|
| 88 |
+
if not source_path.is_file() or source_path.name in REPLACED_FILES:
|
| 89 |
+
continue
|
| 90 |
+
destination = partial / source_path.name
|
| 91 |
+
if source_path.name.startswith("model-") and source_path.suffix == ".safetensors":
|
| 92 |
+
os.link(source_path, destination)
|
| 93 |
+
else:
|
| 94 |
+
shutil.copy2(source_path, destination)
|
| 95 |
+
for name in (
|
| 96 |
+
"chat_template.jinja",
|
| 97 |
+
"kimi_k25_processor.py",
|
| 98 |
+
"kimi_k25_vision_processing.py",
|
| 99 |
+
"media_utils.py",
|
| 100 |
+
"preprocessor_config.json",
|
| 101 |
+
):
|
| 102 |
+
shutil.copy2(assets / name, partial / name)
|
| 103 |
+
for name in ("vision_tower.safetensors", "mm_projector.safetensors"):
|
| 104 |
+
os.link(assets / name, partial / name)
|
| 105 |
+
shutil.copy2(bundle / "configuration_glm5v.py", partial / "configuration_glm5v.py")
|
| 106 |
+
config = build_config(source, assets)
|
| 107 |
+
(partial / "config.json").write_text(json.dumps(config, indent=2) + "\n")
|
| 108 |
+
index = json.loads((source / "model.safetensors.index.json").read_text())
|
| 109 |
+
total_parameters = 0
|
| 110 |
+
total_size = 0
|
| 111 |
+
for name in ("vision_tower.safetensors", "mm_projector.safetensors"):
|
| 112 |
+
parameters, payload_bytes = add_weight_file(index, partial / name)
|
| 113 |
+
total_parameters += parameters
|
| 114 |
+
total_size += payload_bytes
|
| 115 |
+
metadata = index.setdefault("metadata", {})
|
| 116 |
+
if "total_parameters" in metadata:
|
| 117 |
+
metadata["total_parameters"] = int(metadata["total_parameters"]) + total_parameters
|
| 118 |
+
metadata["total_size"] = int(metadata.get("total_size", 0)) + total_size
|
| 119 |
+
(partial / "model.safetensors.index.json").write_text(
|
| 120 |
+
json.dumps(index, indent=2, sort_keys=True) + "\n"
|
| 121 |
+
)
|
| 122 |
+
provenance = {
|
| 123 |
+
"base_checkpoint": str(source),
|
| 124 |
+
"base_config_sha256": sha256(source / "config.json"),
|
| 125 |
+
"base_index_sha256": sha256(source / "model.safetensors.index.json"),
|
| 126 |
+
"vision_repository": "baseten/GLM-5.2-Vision-NVFP4",
|
| 127 |
+
"vision_revision": "f6eab6117386a0c69152fdf272dc65bfd0254f9f",
|
| 128 |
+
"vision_parameters": total_parameters,
|
| 129 |
+
"vision_payload_bytes": total_size,
|
| 130 |
+
}
|
| 131 |
+
(partial / "VISION_PROVENANCE.json").write_text(
|
| 132 |
+
json.dumps(provenance, indent=2, sort_keys=True) + "\n"
|
| 133 |
+
)
|
| 134 |
+
partial.rename(target)
|
| 135 |
+
except BaseException:
|
| 136 |
+
shutil.rmtree(partial, ignore_errors=True)
|
| 137 |
+
raise
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def main() -> None:
|
| 141 |
+
parser = argparse.ArgumentParser()
|
| 142 |
+
parser.add_argument("--source", type=Path, required=True)
|
| 143 |
+
parser.add_argument("--target", type=Path, required=True)
|
| 144 |
+
parser.add_argument("--assets", type=Path, required=True)
|
| 145 |
+
parser.add_argument("--bundle", type=Path, required=True)
|
| 146 |
+
args = parser.parse_args()
|
| 147 |
+
verify_assets(args.assets, args.bundle / "asset-manifest.json")
|
| 148 |
+
assemble(args.source.resolve(), args.target.resolve(), args.assets.resolve(), args.bundle.resolve())
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
main()
|
graft/stage.sh
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
BUNDLE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 5 |
+
SOURCE_DIR="${SOURCE_DIR:-/mnt/llm_models/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid}"
|
| 6 |
+
TARGET_DIR="${TARGET_DIR:-/mnt/llm_models/GLM-5.2-MXFP8-NVFP4-NF3-Hybrid-Vision}"
|
| 7 |
+
ASSET_DIR="${ASSET_DIR:-/mnt/llm_models/.glm52-vision-assets/f6eab6117386a0c69152fdf272dc65bfd0254f9f}"
|
| 8 |
+
REVISION="f6eab6117386a0c69152fdf272dc65bfd0254f9f"
|
| 9 |
+
|
| 10 |
+
mkdir -p "$ASSET_DIR"
|
| 11 |
+
hf download baseten/GLM-5.2-Vision-NVFP4 \
|
| 12 |
+
--revision "$REVISION" \
|
| 13 |
+
--include \
|
| 14 |
+
config.json \
|
| 15 |
+
chat_template.jinja \
|
| 16 |
+
preprocessor_config.json \
|
| 17 |
+
kimi_k25_processor.py \
|
| 18 |
+
kimi_k25_vision_processing.py \
|
| 19 |
+
media_utils.py \
|
| 20 |
+
vision_tower.safetensors \
|
| 21 |
+
mm_projector.safetensors \
|
| 22 |
+
--local-dir "$ASSET_DIR"
|
| 23 |
+
python3 "$BUNDLE_DIR/prepare_checkpoint.py" \
|
| 24 |
+
--source "$SOURCE_DIR" \
|
| 25 |
+
--target "$TARGET_DIR" \
|
| 26 |
+
--assets "$ASSET_DIR" \
|
| 27 |
+
--bundle "$BUNDLE_DIR"
|