0xSero commited on
Commit
b07fd83
·
verified ·
1 Parent(s): c74195b

Add reproduction article + graft bundle (stage.sh, prepare_checkpoint.py, sha256 manifest)

Browse files
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"