drowzeys commited on
Commit
c226d57
·
verified ·
1 Parent(s): 1898d08

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. README.md +106 -0
  2. chat_template.jinja +251 -0
  3. config.json +792 -0
  4. generation_config.json +12 -0
  5. model-00001-of-00041.safetensors +3 -0
  6. model-00005-of-00041.safetensors +3 -0
  7. model-00009-of-00041.safetensors +3 -0
  8. model-00010-of-00041.safetensors +3 -0
  9. model-00011-of-00041.safetensors +3 -0
  10. model-00013-of-00041.safetensors +3 -0
  11. model-00014-of-00041.safetensors +3 -0
  12. model-00019-of-00041.safetensors +3 -0
  13. model-00020-of-00041.safetensors +3 -0
  14. model-00021-of-00041.safetensors +3 -0
  15. model-00023-of-00041.safetensors +3 -0
  16. model-00024-of-00041.safetensors +3 -0
  17. model-00030-of-00041.safetensors +3 -0
  18. model-00031-of-00041.safetensors +3 -0
  19. model-00032-of-00041.safetensors +3 -0
  20. model-00034-of-00041.safetensors +3 -0
  21. model-00035-of-00041.safetensors +3 -0
  22. quant/build.sh +14 -0
  23. quant/exl3_farm.py +185 -0
  24. quant/make_recipe.py +53 -0
  25. quant/orchestrate.py +378 -0
  26. quant/patch_aarch64.py +77 -0
  27. quant/patch_farm.py +122 -0
  28. quant/recipe.yaml +0 -0
  29. quant/start-orch.sh +2 -0
  30. serving/Dockerfile.dcp +21 -0
  31. serving/Dockerfile.keys +2 -0
  32. serving/Dockerfile.keys3 +3 -0
  33. serving/Dockerfile.mul1 +2 -0
  34. serving/Dockerfile.v145 +6 -0
  35. serving/ablit_runtime.py +460 -0
  36. serving/donor_diff.py +14 -0
  37. serving/env.tp4.example +47 -0
  38. serving/exl3.py +862 -0
  39. serving/finalize_config.py +81 -0
  40. serving/glm53_probe.py +32 -0
  41. serving/keys-launch.sh +15 -0
  42. serving/mla_attn_dcp.py +0 -0
  43. serving/patch_ablit_dsv2.py +55 -0
  44. serving/patch_overlay_mtp.py +40 -0
  45. serving/patch_overlay_mul1.py +201 -0
  46. serving/patch_tp4_launcher.py +49 -0
  47. serving/sm120_dcp.py +284 -0
  48. serving/start-tp4.sh +1776 -0
  49. serving/taskbench.py +52 -0
  50. tokenizer_config.json +33 -0
README.md ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ base_model: zai-org/GLM-5.3
4
+ tags:
5
+ - exl3
6
+ - exllamav3
7
+ - glm-5.3
8
+ - dgx-spark
9
+ - gb10
10
+ - vllm
11
+ - 1m-context
12
+ - mtp
13
+ library_name: vllm
14
+ pipeline_tag: text-generation
15
+ ---
16
+
17
+ # keys-GLM-5.3-EXL3 — full GLM-5.3 (753B MoE) as a 3-bit EXL3, quantized *for* a serving container
18
+
19
+ **What this is:** the complete `zai-org/GLM-5.3` (78 layers, 256 routed experts, DeepSeek-style sparse MLA attention with indexer, one MTP block) with the **routed experts quantized to 3-bit EXL3 trellis** and everything else kept bf16/fp16. 308 GB in 41 safetensors shards. It serves on **four NVIDIA DGX Spark (GB10) with TP=4 + decode-context-parallel 4 at a 1,000,000-token context**, inside Mia AI Lab's vLLM container for the GLM-5.3-*Flash* EXL3 (`ghcr.io/miaai-lab/glm-5.3-flash-2x-dgx-sparks:exl3`) plus the small patch set shipped in `serving/`.
20
+
21
+ | | |
22
+ |---|---|
23
+ | Source | `zai-org/GLM-5.3` (FP8 block-scaled, 756 GB, 141 shards) |
24
+ | Quantizer | exllamav3 **1.4.5** (`convert.py`, `-hb 16 -mb 16 -cpi 0`, calibration 250 rows × 2048) |
25
+ | Routed experts | 57,600 matrices (75 MoE layers × 256 experts × gate/up/down) at **3.00 bpw**, `out_scales: always` |
26
+ | Codebook | layer 3 = `mcg`, layers 4–77 = `mul1` (see *Codebook note*); the serving overlay handles both per layer |
27
+ | Everything else | attention (q_a/q_b/kv_a/o_proj/indexer), dense MLP (layers 0–2), shared experts, router, norms, embed, lm_head: **bf16/fp16 (unquantized)** |
28
+ | MTP block (layer 78) | **bf16**, kept — usable as an MTP speculator |
29
+ | Per-rank footprint (TP=4) | ~75 GiB weights (+~5 GiB with the MTP block loaded) |
30
+ | Context verified | needle-in-haystack PASS at 8.5K / 25K / 50K / 105K / 263K / **979K** tokens (DCP=4 sharded KV) |
31
+
32
+ ## Why we made it this way
33
+
34
+ Every previous EXL3 GLM-5.3 we tried to serve on DGX Spark cost hours-to-days of patching: the published quants were made for TabbyAPI/exllamav3 (full-model EXL3, 2-bit heads, mul1 everywhere) and vLLM has no full-model EXL3 — Mia's container only implements EXL3 for the **routed experts**, with attention/dense/head native. So this time we quantized **to the container's ABI first**:
35
+
36
+ * scope = routed experts only (the only thing the overlay's `Exl3Config` quantizes), uniform integer bits in the overlay's allowed set {3,4,5,6} → **3-bit**, because 4-bit experts (≈101 GB/rank) do not fit four 128 GB GB10s next to a 1M KV cache; 3-bit ≈ 78 GB/rank does.
37
+ * `-hb 16 -mb 16`: head and MTP stay bf16 so the stock vLLM paths load them.
38
+ * suffix layout `trellis/suh/svh/<codebook marker>` exactly as the overlay's loader expects, `quantization_config.json` with `quant_method=exl3, bits=3, scope=glm53_routed_experts_only`.
39
+
40
+ It still needed a handful of patches (all in `serving/`), but they are small, mechanical, and now documented — the point of this repo.
41
+
42
+ ## How it was made (4× DGX Spark "encode farm")
43
+
44
+ One 753B model does not fit a single 128 GB Spark for quantization, and exllamav3 has no multi-GPU convert. We built a rolling pipeline (`quant/orchestrate.py`):
45
+
46
+ 1. **Rolling source shards.** The FP8 source is 756 GB and not layer-ordered; the orchestrator keeps a 5-shard download window (sha-checked), runs `convert.py` for **two layers at a time** in a fresh container (`--max_module L+1`, then `-r` resume), and after each step rewrites finished shards **dropping only `model.layers.L.mlp.experts.*`** (norms/attention stay — the compile step needs them). Peak disk ≈ 660 GB; permanent shards = embed/lm_head, final norm, and the layer-78 MTP shard (must exist at every resume).
47
+ 2. **Remote expert-encoding farm** (`quant/exl3_farm.py` + `quant/patch_farm.py`). exllamav3's per-layer H (Hessian) capture is serial, but the 768 expert matrices per layer are independent once H is final. The patch adds remote work threads to `quantize_linears_parallel`: it ships each matrix + its finalized H over TCP (length-prefixed `torch.save`) to worker containers on the other three Sparks, which return `trellis/suh/svh/marker`. Verified bit-identical to local encoding. **One MoE layer: 49 min single-GPU → ~20–22 min on four.** Whole model: ~27 wall-clock hours including a power-cut restart.
48
+ 3. **Resume-safe.** A power interruption at 60/78 lost nothing: exllamav3 checkpoints after every layer; the in-flight layer is simply redone.
49
+ 4. **Compile.** `compile.py` needs `marisa_trie` (not in the base image) — `pip install marisa-trie` in the quant container.
50
+
51
+ `quant/recipe.yaml` (58,167 tensor entries) is included; `quant/build.sh` + `quant/patch_aarch64.py` build exllamav3 1.4.5 on aarch64/CUDA 13 inside the Mia container image (AVX all-reduce/CPU-MoE stubs + pause intrinsic).
52
+
53
+ ### Codebook note (read before you quantize with exllamav3 ≥ 1.4)
54
+
55
+ `convert.py -r` (resume) lets the **command-line default win over the stored args** for the codebook (`("codebook", True, "mul1")` in the arg table). We passed `-cb mcg` on the first run only; every resumed 2-layer run silently quantized with `mul1`. Result: layer 3 is `mcg`, layers 4–77 are `mul1`. Quality is identical (both are 3-bit trellis codebooks), but Mia's overlay was **mcg-only** — which is why `serving/exl3.py` now detects the codebook per layer and drives the kernels' `(mcg, mul1)` flag pairs. **If you resume an exllamav3 job, re-pass every quant flag.**
56
+
57
+ ## Serving on 4× DGX Spark (what `serving/` contains and why)
58
+
59
+ Base: `ghcr.io/miaai-lab/glm-5.3-flash-2x-dgx-sparks:exl3` + its `start-tp4.sh` launcher (github.com/MiaAI-Lab/GLM-5.3-Flash-EXL3-2x-DGX-Sparks). Image layers we add (Dockerfiles included, build order `dcp → mul1 → v145 → keys → keys3`):
60
+
61
+ | Patch | Why |
62
+ |---|---|
63
+ | `exl3.py` (overlay) | + `mul1` marker tensors and per-layer codebook detection; markers may be 0-d scalars; `NullConfig/InferParams` stub for exllamav3 ≥ 1.x's `LinearEXL3`; **MTP block exempt from EXL3** so `SPEC_METHOD=mtp` loads the bf16 MTP experts through the stock path |
64
+ | exllamav3 **1.4.5** swap (`Dockerfile.v145`) | the image's exllamav3 0.0.43 `exl3_moe` kernel hard-rejects `mul1` (`TORCH_CHECK`); 1.4.5 has the same 29-arg signature (+`num_active`) and accepts it. Built against the same base torch, so the `.so` drops in |
65
+ | `sparse_attn_indexer.py` top-k fallback (`Dockerfile.keys`) | prefill `persistent_topk` for top-k 2048 oversubscribes the GB10's 48 SMs; Mia's runtime patch only covers the decode ("kpool") path |
66
+ | `sm120_dcp.py` + `mla_attn_dcp.py` (`Dockerfile.dcp`) | decode-context-parallel port of the SM120 sparse-MLA backend (KV sharded across TP ranks → 1M context on four GB10s; the stock stack replicates KV and tops out ~300K) |
67
+ | `patch_ablit_dsv2.py` + `ablit_runtime.py` | optional load-time o_proj transplant hook for the full-model classes (`DeepseekV2Model`/`DeepSeekMTP`); off by default |
68
+ | `patch_tp4_launcher.py` | adds `LOCAL_MODEL_DIR`/`LOCAL_DFLASH_DIR` (serve from an NFS dir) and `ABLIT_HOST_DIR` to `start-tp4.sh` |
69
+ | `keys-launch.sh`, `env.tp4.example` | our launch wrapper (drops page caches on all ranks first) and the working `.env.tp4` |
70
+
71
+ Launcher-level gotchas we hit (all in `env.tp4.example` comments): `--ulimit nofile=1048576` (NCCL "Too many open files" at 4 ranks), a rank whose fabric IP sits on the *second* CX7 port needs its own `WORKER_CX7_IF/IB`, the NFS server node must bind-mount its own export at the shared path, `gpu_memory_utilization` above ~0.82 is refused by vLLM's startup guard (the worker already holds ~14 GiB of CUDA/NCCL state at check time) — use `--kv-cache-memory-bytes` to size the KV cache explicitly.
72
+
73
+ ### Measured (4× GB10, TP=4, DCP=4, CUDA graphs, fp8 KV)
74
+
75
+ | Config | C1 per-stream | C2 | C4 | C8 aggregate | TTFT @C8 |
76
+ |---|---|---|---|---|---|
77
+ | no speculation | 8–9 tok/s | 16 | 25 | 38 | ~2 s |
78
+ | **MTP k=3** (bf16 MTP block) — prose | 10.4 | 15.9 | 21.9 | 31.5 | 2.1 s |
79
+ | **MTP k=3** — code | 16.0 | 22.0 | 28.2 | 43.1 | 1.9 s |
80
+
81
+ 256-token outputs, thinking off. Prefill ≈ 320–410 tok/s (the limiter at depth: a 979K prompt takes ~52 min to first token; decode there is 12 tok/s). DFlash2 draft: loads, but the draft's sliding-window layers are rejected by vLLM's DCP path (`DCP not support sliding window`) — parked.
82
+
83
+ ## Quick start (4 Sparks)
84
+
85
+ ```bash
86
+ git clone https://github.com/MiaAI-Lab/GLM-5.3-Flash-EXL3-2x-DGX-Sparks glm53-mia && cd glm53-mia
87
+ python3 serving/patch_tp4_launcher.py start-tp4.sh # LOCAL_MODEL_DIR + ABLIT_HOST_DIR knobs
88
+ # build the image chain on EVERY rank (see serving/Dockerfile.* ; ~1 min each on top of the Mia base)
89
+ cp serving/env.tp4.example .env.tp4 # set rank IPs / NIC pins / LOCAL_MODEL_DIR=<this repo on shared storage>
90
+ SKIP_PULL=1 SKIP_SHIP=1 SKIP_BUILD=1 SKIP_DOWNLOAD=1 SKIP_SYNC=1 SKIP_OVERLAY_VERIFY=1 ./start-tp4.sh
91
+ ```
92
+ OpenAI-compatible API on `:8888`, served model name `GLM-5.3-EXL3`. Sampling: temperature 1.0, top_p 0.95, thinking on (GLM defaults).
93
+
94
+ ## Lessons for "quantize with the serving container in mind"
95
+
96
+ 1. Read the serving overlay's **loader** first: allowed scope, bits set, tensor suffixes, marker dtype/shape, codebook — and quantize to that ABI, not to the quantizer's defaults.
97
+ 2. Keep anything the container serves natively (attention, dense, head, MTP) unquantized; the memory cost is small next to the experts and it removes whole classes of patches.
98
+ 3. Verify tensor suffixes after the **first resumed** step, not at the end (see the codebook note).
99
+ 4. Budget memory per rank for weights + KV + the ~14 GiB the process holds before vLLM measures anything; size KV explicitly.
100
+ 5. A per-layer encode farm turns a multi-day single-GPU convert into an overnight job on the same boxes that will serve the model.
101
+
102
+ ## Credits
103
+
104
+ Zhipu / zai-org for GLM-5.3 · turboderp for exllamav3 · Mia AI Lab for the GB10 EXL3 vLLM container and launcher · Blackfrost-Research for the DERISKED GLM-5.3 (source of the optional ablit donors; **not applied** in these weights) · Quantized and served by Kevin (@drowzeys) on a 4× DGX Spark cluster, 2026-09.
105
+
106
+ License: the weights inherit the `zai-org/GLM-5.3` license (MIT). Patches in `serving/` are MIT; they modify Apache-2.0 vLLM/SGLang/exllamav3 code — see their headers.
chat_template.jinja ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [gMASK]<sop>
2
+ {%- set effective_reasoning_effort = reasoning_effort if reasoning_effort is defined and reasoning_effort in ['low', 'high'] else 'max' -%}
3
+ {%- if effective_reasoning_effort is not none -%}<|system|>Reasoning Effort: {{ effective_reasoning_effort | capitalize }}{%- endif -%}
4
+ {%- set clear_thinking = clear_thinking if clear_thinking is defined else false -%}
5
+ {%- if tools -%}
6
+ {%- macro tool_to_json(tool) -%}
7
+ {%- set ns_tool = namespace(first=true) -%}
8
+ {{ '{' -}}
9
+ {%- for k, v in tool.items() -%}
10
+ {%- if k != 'defer_loading' and k != 'strict' -%}
11
+ {%- if not ns_tool.first -%}{{- ', ' -}}{%- endif -%}
12
+ {%- set ns_tool.first = false -%}
13
+ "{{ k }}": {{ v | tojson(ensure_ascii=False) }}
14
+ {%- endif -%}
15
+ {%- endfor -%}
16
+ {{- '}' -}}
17
+ {%- endmacro -%}
18
+ {%- macro tool_references_to_response(refs) -%}
19
+ {{- '<tool_response><tools>\n' -}}
20
+ {%- for tr in refs -%}
21
+ {%- for tool in tools -%}
22
+ {%- if 'function' in tool -%}
23
+ {%- set tool = tool['function'] -%}
24
+ {%- endif -%}
25
+ {%- if tool.name == tr.name -%}
26
+ {{- tool_to_json(tool) + '\n' -}}
27
+ {%- endif -%}
28
+ {%- endfor -%}
29
+ {%- endfor -%}
30
+ {{- '</tools></tool_response>' -}}
31
+ {%- endmacro -%}
32
+ <|system|>
33
+ # Tools
34
+
35
+ You may call one or more functions to assist with the user query.
36
+
37
+ You are provided with function signatures within <tools></tools> XML tags:
38
+ <tools>
39
+ {% for tool in tools %}
40
+ {%- if 'function' in tool -%}
41
+ {%- set tool = tool['function'] -%}
42
+ {%- endif -%}
43
+ {% if tool.defer_loading is not defined or not tool.defer_loading %}
44
+ {{ tool_to_json(tool) }}
45
+ {% endif %}
46
+ {% endfor %}
47
+ </tools>
48
+
49
+ For each function call, output the function name and arguments within the following XML format:
50
+ <tool_call>{function-name}<arg_key>{arg-key-1}</arg_key><arg_value>{arg-value-1}</arg_value><arg_key>{arg-key-2}</arg_key><arg_value>{arg-value-2}</arg_value>...</tool_call>{%- endif -%}
51
+ {%- macro visible_text(content) -%}
52
+ {%- if content is string -%}
53
+ {{- content }}
54
+ {%- elif content is iterable and content is not mapping -%}
55
+ {%- for item in content -%}
56
+ {%- if item is mapping and item.type == 'text' -%}
57
+ {{- item.text }}
58
+ {%- elif item is string -%}
59
+ {{- item }}
60
+ {%- elif item is mapping and item.type in ['image', 'image_url', 'video', 'video_url', 'audio', 'audio_url', 'input_audio'] -%}
61
+ {%- set media_type = item.type | replace('_url', '') | replace('input_', '') -%}
62
+ {{- "<reminder>You are unable to process this " ~ media_type ~ " because you don't have multi-modal input ability. Try different methods.</reminder>" }}
63
+ {%- endif -%}
64
+ {%- endfor -%}
65
+ {%- else -%}
66
+ {{- content }}
67
+ {%- endif -%}
68
+ {%- endmacro -%}
69
+ {%- macro tool_response(text) -%}
70
+ {{- '<tool_response>' + text + '</tool_response>' -}}
71
+ {%- endmacro -%}
72
+ {%- macro render_tool_response(m) -%}
73
+ {%- if m.content is string -%}
74
+ {{- tool_response(m.content) -}}
75
+ {%- elif m.content and m.content is not mapping and m.content.0.type == "tool_reference" -%}
76
+ {{- tool_references_to_response(m.content) -}}
77
+ {%- elif is_list_of_outputs(m) -%}
78
+ {%- for tr in m.content -%}
79
+ {%- if tr.output is iterable and tr.output is not string and tr.output is not mapping and tr.output and tr.output.0.type == "tool_reference" -%}
80
+ {{- tool_references_to_response(tr.output) -}}
81
+ {%- else -%}
82
+ {{- tool_response(visible_text(tr.output)) -}}
83
+ {%- endif -%}
84
+ {%- endfor -%}
85
+ {%- else -%}
86
+ {{- tool_response(visible_text(m.content)) -}}
87
+ {%- endif -%}
88
+ {%- endmacro -%}
89
+ {%- macro id_of(obj) -%}
90
+ {%- if obj.tool_call_id -%}
91
+ {{- obj.tool_call_id -}}
92
+ {%- elif obj.id -%}
93
+ {{- obj.id -}}
94
+ {%- endif -%}
95
+ {%- endmacro -%}
96
+ {%- macro is_list_of_outputs(m) -%}
97
+ {%- if m.content and m.content.0.output is defined -%}1{%- endif -%}
98
+ {%- endmacro -%}
99
+ {%- macro has_dup_tool_result_id(lo, hi, target) -%}
100
+ {%- set ns_cnt = namespace(n=0) -%}
101
+ {%- for k in range(lo, hi + 1) -%}
102
+ {%- set m = messages[k] -%}
103
+ {%- if is_list_of_outputs(m) -%}
104
+ {%- for entry in m.content -%}
105
+ {%- if id_of(entry) == target -%}
106
+ {%- set ns_cnt.n = ns_cnt.n + 1 -%}
107
+ {%- endif -%}
108
+ {%- endfor -%}
109
+ {%- elif id_of(m) == target -%}
110
+ {%- set ns_cnt.n = ns_cnt.n + 1 -%}
111
+ {%- endif -%}
112
+ {%- if ns_cnt.n > 1 -%}{%- break -%}{%- endif -%}
113
+ {%- endfor -%}
114
+ {%- if ns_cnt.n > 1 -%}1{%- endif -%}
115
+ {%- endmacro -%}
116
+ {%- macro tc_id_exists(tcs, target) -%}
117
+ {%- set ns_f = namespace(found=false) -%}
118
+ {%- for tc in tcs -%}
119
+ {%- if id_of(tc) == target -%}
120
+ {%- set ns_f.found = true -%}
121
+ {%- break -%}
122
+ {%- endif -%}
123
+ {%- endfor -%}
124
+ {%- if ns_f.found -%}1{%- endif -%}
125
+ {%- endmacro -%}
126
+ {%- set ns = namespace(last_user_index=-1) -%}
127
+ {%- for m in messages %}
128
+ {%- if m.role == 'user' %}
129
+ {%- set ns.last_user_index = loop.index0 -%}
130
+ {%- endif %}
131
+ {%- endfor %}
132
+ {%- for m in messages -%}
133
+ {%- if m.role == 'user' -%}<|user|>{{ visible_text(m.content) }}
134
+ {%- elif m.role == 'assistant' -%}
135
+ <|assistant|>
136
+ {%- set content = visible_text(m.content) %}
137
+ {%- if m.reasoning_content is string %}
138
+ {%- set reasoning_content = m.reasoning_content %}
139
+ {%- elif '</think>' in content %}
140
+ {%- set reasoning_content = content.split('</think>')[0].split('<think>')[-1] %}
141
+ {%- set content = content.split('</think>')[-1] %}
142
+ {%- endif %}
143
+ {%- if (not clear_thinking or loop.index0 > ns.last_user_index) and reasoning_content is defined -%}
144
+ {{ '<think>' + reasoning_content + '</think>'}}
145
+ {%- else -%}
146
+ {{ '<think></think>' }}
147
+ {%- endif -%}
148
+ {%- if content.strip() -%}
149
+ {{ content.strip() }}
150
+ {%- endif -%}
151
+ {% if m.tool_calls %}
152
+ {% for tc in m.tool_calls %}
153
+ {%- if tc.function %}
154
+ {%- set tc = tc.function %}
155
+ {%- endif %}
156
+ {{- '<tool_call>' + tc.name -}}
157
+ {% set _args = tc.arguments %}{% for k, v in _args.items() %}<arg_key>{{ k }}</arg_key><arg_value>{{ v | tojson(ensure_ascii=False) if v is not string else v }}</arg_value>{% endfor %}</tool_call>{% endfor %}
158
+ {% endif %}
159
+ {%- elif m.role == 'tool' -%}
160
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
161
+ {{- '<|observation|>' -}}
162
+ {%- set block_start = loop.index0 -%}
163
+ {%- set ns_blk = namespace(end=block_start) -%}
164
+ {%- for j in range(block_start, messages|length) -%}
165
+ {%- if messages[j].role == 'tool' -%}
166
+ {%- set ns_blk.end = j -%}
167
+ {%- else -%}
168
+ {%- break -%}
169
+ {%- endif -%}
170
+ {%- endfor -%}
171
+ {%- set ns_a = namespace(tool_calls=none) -%}
172
+ {%- if block_start > 0 and messages[block_start - 1].role == 'assistant' and messages[block_start - 1].tool_calls -%}
173
+ {%- set ns_a.tool_calls = messages[block_start - 1].tool_calls -%}
174
+ {%- endif -%}
175
+ {%- set ns_chk = namespace(can_sort=true) -%}
176
+ {%- if not ns_a.tool_calls -%}
177
+ {%- set ns_chk.can_sort = false -%}
178
+ {%- else -%}
179
+ {%- for k in range(block_start, ns_blk.end + 1) -%}
180
+ {%- set m = messages[k] -%}
181
+ {%- if is_list_of_outputs(m) -%}
182
+ {%- for entry in m.content -%}
183
+ {%- set eid = id_of(entry) -%}
184
+ {%- if not eid -%}
185
+ {%- set ns_chk.can_sort = false -%}
186
+ {%- elif has_dup_tool_result_id(block_start, ns_blk.end, eid) -%}
187
+ {%- set ns_chk.can_sort = false -%}
188
+ {%- elif not tc_id_exists(ns_a.tool_calls, eid) -%}
189
+ {%- set ns_chk.can_sort = false -%}
190
+ {%- endif -%}
191
+ {%- endfor -%}
192
+ {%- else -%}
193
+ {%- set tk_id = id_of(m) -%}
194
+ {%- if not tk_id -%}
195
+ {%- set ns_chk.can_sort = false -%}
196
+ {%- elif has_dup_tool_result_id(block_start, ns_blk.end, tk_id) -%}
197
+ {%- set ns_chk.can_sort = false -%}
198
+ {%- elif not tc_id_exists(ns_a.tool_calls, tk_id) -%}
199
+ {%- set ns_chk.can_sort = false -%}
200
+ {%- endif -%}
201
+ {%- endif -%}
202
+ {%- endfor -%}
203
+ {%- for i in range(ns_a.tool_calls | length) -%}
204
+ {%- set tc_id = id_of(ns_a.tool_calls[i]) -%}
205
+ {%- if not tc_id -%}
206
+ {%- set ns_chk.can_sort = false -%}
207
+ {%- endif -%}
208
+ {%- for j in range(i + 1, ns_a.tool_calls | length) -%}
209
+ {%- if id_of(ns_a.tool_calls[j]) == tc_id -%}
210
+ {%- set ns_chk.can_sort = false -%}
211
+ {%- endif -%}
212
+ {%- endfor -%}
213
+ {%- endfor -%}
214
+ {%- endif -%}
215
+ {%- if ns_chk.can_sort -%}
216
+ {%- for tc in ns_a.tool_calls -%}
217
+ {%- set tc_id = id_of(tc) -%}
218
+ {%- for k in range(block_start, ns_blk.end + 1) -%}
219
+ {%- set m = messages[k] -%}
220
+ {%- if is_list_of_outputs(m) -%}
221
+ {%- for entry in m.content -%}
222
+ {%- set eid = id_of(entry) -%}
223
+ {%- if eid == tc_id -%}
224
+ {%- if entry.output is iterable and entry.output is not string and entry.output is not mapping and entry.output and entry.output.0.type == "tool_reference" -%}
225
+ {{- tool_references_to_response(entry.output) -}}
226
+ {%- else -%}
227
+ {{- tool_response(visible_text(entry.output)) -}}
228
+ {%- endif -%}
229
+ {%- endif -%}
230
+ {%- endfor -%}
231
+ {%- else -%}
232
+ {%- set tk_id = id_of(m) -%}
233
+ {%- if tk_id == tc_id -%}
234
+ {{- render_tool_response(m) -}}
235
+ {%- endif -%}
236
+ {%- endif -%}
237
+ {%- endfor -%}
238
+ {%- endfor -%}
239
+ {%- else -%}
240
+ {%- for k in range(block_start, ns_blk.end + 1) -%}
241
+ {{- render_tool_response(messages[k]) -}}
242
+ {%- endfor -%}
243
+ {%- endif -%}
244
+ {% endif -%}
245
+ {%- elif m.role == 'system' -%}
246
+ <|system|>{{ visible_text(m.content) }}
247
+ {%- endif -%}
248
+ {%- endfor -%}
249
+ {%- if add_generation_prompt -%}
250
+ <|assistant|>{{- '<think>' -}}
251
+ {%- endif -%}
config.json ADDED
@@ -0,0 +1,792 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "GlmMoeDsaForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "dtype": "bfloat16",
8
+ "eos_token_id": [
9
+ 154820,
10
+ 154827,
11
+ 154829
12
+ ],
13
+ "ep_size": 1,
14
+ "first_k_dense_replace": 3,
15
+ "head_dim": 192,
16
+ "hidden_act": "silu",
17
+ "hidden_size": 6144,
18
+ "index_head_dim": 128,
19
+ "index_n_heads": 32,
20
+ "index_share_for_mtp_iteration": true,
21
+ "index_skip_topk_offset": 3,
22
+ "index_topk": 2048,
23
+ "index_topk_freq": 4,
24
+ "index_topk_pattern": null,
25
+ "indexer_rope_interleave": true,
26
+ "indexer_types": [
27
+ "full",
28
+ "full",
29
+ "full",
30
+ "shared",
31
+ "shared",
32
+ "shared",
33
+ "full",
34
+ "shared",
35
+ "shared",
36
+ "shared",
37
+ "full",
38
+ "shared",
39
+ "shared",
40
+ "shared",
41
+ "full",
42
+ "shared",
43
+ "shared",
44
+ "shared",
45
+ "full",
46
+ "shared",
47
+ "shared",
48
+ "shared",
49
+ "full",
50
+ "shared",
51
+ "shared",
52
+ "shared",
53
+ "full",
54
+ "shared",
55
+ "shared",
56
+ "shared",
57
+ "full",
58
+ "shared",
59
+ "shared",
60
+ "shared",
61
+ "full",
62
+ "shared",
63
+ "shared",
64
+ "shared",
65
+ "full",
66
+ "shared",
67
+ "shared",
68
+ "shared",
69
+ "full",
70
+ "shared",
71
+ "shared",
72
+ "shared",
73
+ "full",
74
+ "shared",
75
+ "shared",
76
+ "shared",
77
+ "full",
78
+ "shared",
79
+ "shared",
80
+ "shared",
81
+ "full",
82
+ "shared",
83
+ "shared",
84
+ "shared",
85
+ "full",
86
+ "shared",
87
+ "shared",
88
+ "shared",
89
+ "full",
90
+ "shared",
91
+ "shared",
92
+ "shared",
93
+ "full",
94
+ "shared",
95
+ "shared",
96
+ "shared",
97
+ "full",
98
+ "shared",
99
+ "shared",
100
+ "shared",
101
+ "full",
102
+ "shared",
103
+ "shared",
104
+ "shared"
105
+ ],
106
+ "initializer_range": 0.02,
107
+ "intermediate_size": 12288,
108
+ "kv_lora_rank": 512,
109
+ "max_position_embeddings": 1048576,
110
+ "mlp_layer_types": [
111
+ "dense",
112
+ "dense",
113
+ "dense",
114
+ "sparse",
115
+ "sparse",
116
+ "sparse",
117
+ "sparse",
118
+ "sparse",
119
+ "sparse",
120
+ "sparse",
121
+ "sparse",
122
+ "sparse",
123
+ "sparse",
124
+ "sparse",
125
+ "sparse",
126
+ "sparse",
127
+ "sparse",
128
+ "sparse",
129
+ "sparse",
130
+ "sparse",
131
+ "sparse",
132
+ "sparse",
133
+ "sparse",
134
+ "sparse",
135
+ "sparse",
136
+ "sparse",
137
+ "sparse",
138
+ "sparse",
139
+ "sparse",
140
+ "sparse",
141
+ "sparse",
142
+ "sparse",
143
+ "sparse",
144
+ "sparse",
145
+ "sparse",
146
+ "sparse",
147
+ "sparse",
148
+ "sparse",
149
+ "sparse",
150
+ "sparse",
151
+ "sparse",
152
+ "sparse",
153
+ "sparse",
154
+ "sparse",
155
+ "sparse",
156
+ "sparse",
157
+ "sparse",
158
+ "sparse",
159
+ "sparse",
160
+ "sparse",
161
+ "sparse",
162
+ "sparse",
163
+ "sparse",
164
+ "sparse",
165
+ "sparse",
166
+ "sparse",
167
+ "sparse",
168
+ "sparse",
169
+ "sparse",
170
+ "sparse",
171
+ "sparse",
172
+ "sparse",
173
+ "sparse",
174
+ "sparse",
175
+ "sparse",
176
+ "sparse",
177
+ "sparse",
178
+ "sparse",
179
+ "sparse",
180
+ "sparse",
181
+ "sparse",
182
+ "sparse",
183
+ "sparse",
184
+ "sparse",
185
+ "sparse",
186
+ "sparse",
187
+ "sparse",
188
+ "sparse"
189
+ ],
190
+ "model_type": "glm_moe_dsa",
191
+ "moe_intermediate_size": 2048,
192
+ "moe_layer_freq": 1,
193
+ "moe_router_dtype": "float32",
194
+ "n_group": 1,
195
+ "n_routed_experts": 256,
196
+ "n_shared_experts": 1,
197
+ "norm_topk_prob": true,
198
+ "num_attention_heads": 64,
199
+ "num_experts_per_tok": 8,
200
+ "num_hidden_layers": 78,
201
+ "num_key_value_heads": 64,
202
+ "num_nextn_predict_layers": 1,
203
+ "pad_token_id": 154820,
204
+ "pretraining_tp": 1,
205
+ "q_lora_rank": 2048,
206
+ "qk_head_dim": 256,
207
+ "qk_nope_head_dim": 192,
208
+ "qk_rope_head_dim": 64,
209
+ "rms_norm_eps": 1e-05,
210
+ "rope_interleave": true,
211
+ "rope_parameters": {
212
+ "rope_theta": 8000000,
213
+ "rope_type": "default"
214
+ },
215
+ "routed_scaling_factor": 2.5,
216
+ "scoring_func": "sigmoid",
217
+ "tie_word_embeddings": false,
218
+ "topk_group": 1,
219
+ "topk_method": "noaux_tc",
220
+ "transformers_version": "5.15.0",
221
+ "use_cache": true,
222
+ "v_head_dim": 256,
223
+ "vocab_size": 154880,
224
+ "quantization_config": {
225
+ "quant_method": "exl3",
226
+ "version": "1.4.5",
227
+ "bits": 3,
228
+ "head_bits": 16,
229
+ "calibration": {
230
+ "rows": 250,
231
+ "cols": 2048
232
+ },
233
+ "out_scales": "always",
234
+ "codebook": "mul1",
235
+ "original_quantization_config": {
236
+ "activation_scheme": "dynamic",
237
+ "fmt": "e4m3",
238
+ "quant_method": "fp8",
239
+ "weight_block_size": [
240
+ 128,
241
+ 128
242
+ ],
243
+ "modules_to_not_convert": [
244
+ "model.layers.59.post_attention_layernorm",
245
+ "model.layers.47.mlp.gate.e_score_correction_bias",
246
+ "model.layers.71.self_attn.kv_a_layernorm",
247
+ "model.layers.46.mlp.gate.e_score_correction_bias",
248
+ "model.layers.30.self_attn.kv_a_layernorm",
249
+ "model.layers.23.self_attn.kv_a_layernorm",
250
+ "model.layers.73.mlp.gate.e_score_correction_bias",
251
+ "model.layers.36.mlp.gate",
252
+ "model.layers.12.self_attn.q_a_layernorm",
253
+ "model.layers.51.mlp.gate",
254
+ "model.layers.47.self_attn.kv_a_layernorm",
255
+ "model.layers.45.self_attn.q_a_layernorm",
256
+ "model.layers.15.self_attn.q_a_layernorm",
257
+ "model.layers.39.input_layernorm",
258
+ "model.layers.50.self_attn.kv_a_layernorm",
259
+ "model.layers.19.post_attention_layernorm",
260
+ "model.layers.71.input_layernorm",
261
+ "model.layers.72.self_attn.kv_a_layernorm",
262
+ "model.layers.70.mlp.gate",
263
+ "model.layers.52.input_layernorm",
264
+ "model.layers.78.self_attn.kv_a_layernorm",
265
+ "model.layers.48.post_attention_layernorm",
266
+ "model.layers.4.self_attn.q_a_layernorm",
267
+ "model.layers.62.self_attn.q_a_layernorm",
268
+ "model.layers.4.post_attention_layernorm",
269
+ "model.layers.38.self_attn.indexers_proj",
270
+ "model.layers.61.mlp.gate.e_score_correction_bias",
271
+ "model.layers.14.mlp.gate.e_score_correction_bias",
272
+ "model.layers.36.self_attn.kv_a_layernorm",
273
+ "model.layers.65.mlp.gate",
274
+ "model.layers.13.self_attn.kv_a_layernorm",
275
+ "model.layers.57.mlp.gate",
276
+ "model.layers.65.self_attn.kv_a_layernorm",
277
+ "model.layers.68.input_layernorm",
278
+ "model.layers.70.self_attn.kv_a_layernorm",
279
+ "model.layers.24.mlp.gate.e_score_correction_bias",
280
+ "model.layers.4.input_layernorm",
281
+ "model.layers.20.input_layernorm",
282
+ "model.layers.65.self_attn.q_a_layernorm",
283
+ "model.layers.38.mlp.gate",
284
+ "model.layers.76.mlp.gate.e_score_correction_bias",
285
+ "model.layers.62.input_layernorm",
286
+ "model.layers.45.input_layernorm",
287
+ "model.layers.68.post_attention_layernorm",
288
+ "model.layers.72.input_layernorm",
289
+ "model.layers.32.mlp.gate.e_score_correction_bias",
290
+ "model.layers.27.input_layernorm",
291
+ "model.layers.16.post_attention_layernorm",
292
+ "model.layers.35.self_attn.q_a_layernorm",
293
+ "model.layers.58.self_attn.indexers_proj",
294
+ "model.layers.3.mlp.gate.e_score_correction_bias",
295
+ "model.layers.49.self_attn.q_a_layernorm",
296
+ "model.layers.66.mlp.gate.e_score_correction_bias",
297
+ "model.layers.10.self_attn.q_a_layernorm",
298
+ "model.layers.51.mlp.gate.e_score_correction_bias",
299
+ "model.layers.64.self_attn.q_a_layernorm",
300
+ "model.layers.70.input_layernorm",
301
+ "model.layers.71.post_attention_layernorm",
302
+ "model.layers.67.post_attention_layernorm",
303
+ "model.layers.6.self_attn.indexer.k_norm",
304
+ "model.layers.3.mlp.gate",
305
+ "model.layers.11.mlp.gate",
306
+ "model.layers.17.input_layernorm",
307
+ "model.layers.13.mlp.gate",
308
+ "model.layers.15.mlp.gate",
309
+ "model.layers.71.self_attn.q_a_layernorm",
310
+ "model.layers.33.mlp.gate.e_score_correction_bias",
311
+ "model.layers.72.mlp.gate.e_score_correction_bias",
312
+ "model.layers.65.post_attention_layernorm",
313
+ "model.layers.78.hnorm",
314
+ "model.layers.40.post_attention_layernorm",
315
+ "model.layers.12.mlp.gate.e_score_correction_bias",
316
+ "model.layers.8.input_layernorm",
317
+ "model.layers.73.mlp.gate",
318
+ "model.layers.11.post_attention_layernorm",
319
+ "model.layers.61.input_layernorm",
320
+ "model.layers.14.self_attn.kv_a_layernorm",
321
+ "model.layers.39.self_attn.kv_a_layernorm",
322
+ "model.layers.70.self_attn.indexer.k_norm",
323
+ "model.layers.45.mlp.gate",
324
+ "model.layers.51.self_attn.q_a_layernorm",
325
+ "model.layers.64.post_attention_layernorm",
326
+ "model.layers.20.post_attention_layernorm",
327
+ "model.layers.66.self_attn.kv_a_layernorm",
328
+ "model.layers.10.self_attn.indexer.k_norm.bias",
329
+ "model.layers.23.input_layernorm",
330
+ "model.layers.38.self_attn.indexer.k_norm",
331
+ "model.layers.19.self_attn.kv_a_layernorm",
332
+ "model.layers.40.self_attn.q_a_layernorm",
333
+ "model.layers.10.self_attn.indexer.k_norm",
334
+ "model.layers.44.post_attention_layernorm",
335
+ "model.layers.70.post_attention_layernorm",
336
+ "model.layers.41.mlp.gate.e_score_correction_bias",
337
+ "model.layers.36.post_attention_layernorm",
338
+ "model.layers.30.self_attn.indexer.k_norm.bias",
339
+ "model.layers.54.self_attn.q_a_layernorm",
340
+ "model.layers.59.mlp.gate",
341
+ "model.layers.69.post_attention_layernorm",
342
+ "lm_head",
343
+ "model.layers.77.self_attn.kv_a_layernorm",
344
+ "model.layers.75.self_attn.q_a_layernorm",
345
+ "model.layers.56.self_attn.q_a_layernorm",
346
+ "model.layers.50.self_attn.indexer.k_norm.bias",
347
+ "model.layers.54.mlp.gate",
348
+ "model.layers.6.self_attn.indexer.k_norm.bias",
349
+ "model.layers.15.input_layernorm",
350
+ "model.layers.29.self_attn.q_a_layernorm",
351
+ "model.layers.54.self_attn.indexers_proj",
352
+ "model.layers.73.self_attn.kv_a_layernorm",
353
+ "model.layers.0.self_attn.indexer.k_norm",
354
+ "model.layers.58.self_attn.indexer.k_norm",
355
+ "model.layers.36.input_layernorm",
356
+ "model.layers.47.self_attn.q_a_layernorm",
357
+ "model.layers.26.mlp.gate.e_score_correction_bias",
358
+ "model.layers.50.input_layernorm",
359
+ "model.layers.16.self_attn.q_a_layernorm",
360
+ "model.layers.19.mlp.gate.e_score_correction_bias",
361
+ "model.layers.78.input_layernorm",
362
+ "model.layers.27.mlp.gate",
363
+ "model.layers.19.self_attn.q_a_layernorm",
364
+ "model.layers.25.input_layernorm",
365
+ "model.layers.33.self_attn.kv_a_layernorm",
366
+ "model.layers.32.mlp.gate",
367
+ "model.layers.61.self_attn.kv_a_layernorm",
368
+ "model.layers.50.self_attn.indexers_proj",
369
+ "model.layers.34.self_attn.indexers_proj",
370
+ "model.layers.4.mlp.gate.e_score_correction_bias",
371
+ "model.layers.76.post_attention_layernorm",
372
+ "model.layers.57.post_attention_layernorm",
373
+ "model.layers.73.post_attention_layernorm",
374
+ "model.layers.68.mlp.gate.e_score_correction_bias",
375
+ "model.layers.34.self_attn.kv_a_layernorm",
376
+ "model.layers.78.enorm",
377
+ "model.layers.30.mlp.gate",
378
+ "model.layers.10.self_attn.indexers_proj",
379
+ "model.layers.34.input_layernorm",
380
+ "model.layers.34.post_attention_layernorm",
381
+ "model.layers.20.mlp.gate.e_score_correction_bias",
382
+ "model.layers.54.self_attn.indexer.k_norm.bias",
383
+ "model.layers.44.mlp.gate",
384
+ "model.layers.25.mlp.gate.e_score_correction_bias",
385
+ "model.layers.26.self_attn.indexers_proj",
386
+ "model.layers.66.self_attn.indexer.k_norm.bias",
387
+ "model.layers.41.post_attention_layernorm",
388
+ "model.layers.51.post_attention_layernorm",
389
+ "model.layers.22.self_attn.indexer.k_norm.bias",
390
+ "model.layers.21.mlp.gate.e_score_correction_bias",
391
+ "model.layers.15.self_attn.kv_a_layernorm",
392
+ "model.layers.25.self_attn.kv_a_layernorm",
393
+ "model.layers.55.mlp.gate",
394
+ "model.layers.11.self_attn.q_a_layernorm",
395
+ "model.layers.72.mlp.gate",
396
+ "model.layers.21.self_attn.kv_a_layernorm",
397
+ "model.layers.59.mlp.gate.e_score_correction_bias",
398
+ "model.layers.2.input_layernorm",
399
+ "model.layers.5.mlp.gate",
400
+ "model.layers.54.self_attn.kv_a_layernorm",
401
+ "model.layers.0.self_attn.indexers_proj",
402
+ "model.layers.71.mlp.gate.e_score_correction_bias",
403
+ "model.layers.6.self_attn.q_a_layernorm",
404
+ "model.layers.14.mlp.gate",
405
+ "model.layers.44.self_attn.kv_a_layernorm",
406
+ "model.layers.61.post_attention_layernorm",
407
+ "model.layers.64.self_attn.kv_a_layernorm",
408
+ "model.layers.1.post_attention_layernorm",
409
+ "model.layers.4.mlp.gate",
410
+ "model.layers.22.post_attention_layernorm",
411
+ "model.layers.21.self_attn.q_a_layernorm",
412
+ "model.layers.74.self_attn.q_a_layernorm",
413
+ "model.layers.18.self_attn.indexers_proj",
414
+ "model.layers.75.input_layernorm",
415
+ "model.layers.70.self_attn.q_a_layernorm",
416
+ "model.layers.22.self_attn.indexer.k_norm",
417
+ "model.layers.42.mlp.gate.e_score_correction_bias",
418
+ "model.layers.5.self_attn.kv_a_layernorm",
419
+ "model.layers.31.mlp.gate.e_score_correction_bias",
420
+ "model.layers.62.self_attn.indexer.k_norm.bias",
421
+ "model.layers.29.post_attention_layernorm",
422
+ "model.layers.6.self_attn.kv_a_layernorm",
423
+ "model.layers.48.input_layernorm",
424
+ "model.layers.74.self_attn.indexer.k_norm.bias",
425
+ "model.layers.7.input_layernorm",
426
+ "model.layers.39.self_attn.q_a_layernorm",
427
+ "model.layers.41.input_layernorm",
428
+ "model.layers.54.self_attn.indexer.k_norm",
429
+ "model.layers.32.self_attn.q_a_layernorm",
430
+ "model.layers.39.mlp.gate.e_score_correction_bias",
431
+ "model.layers.42.input_layernorm",
432
+ "model.layers.43.mlp.gate",
433
+ "model.layers.72.self_attn.q_a_layernorm",
434
+ "model.layers.37.mlp.gate.e_score_correction_bias",
435
+ "model.layers.5.post_attention_layernorm",
436
+ "model.layers.28.mlp.gate",
437
+ "model.layers.2.post_attention_layernorm",
438
+ "model.layers.49.post_attention_layernorm",
439
+ "model.layers.43.self_attn.kv_a_layernorm",
440
+ "model.layers.12.input_layernorm",
441
+ "model.layers.56.mlp.gate",
442
+ "model.layers.5.input_layernorm",
443
+ "model.layers.29.mlp.gate.e_score_correction_bias",
444
+ "model.layers.4.self_attn.kv_a_layernorm",
445
+ "model.layers.20.self_attn.q_a_layernorm",
446
+ "model.layers.59.self_attn.q_a_layernorm",
447
+ "model.layers.68.mlp.gate",
448
+ "model.layers.21.post_attention_layernorm",
449
+ "model.layers.26.self_attn.q_a_layernorm",
450
+ "model.layers.52.self_attn.q_a_layernorm",
451
+ "model.layers.29.self_attn.kv_a_layernorm",
452
+ "model.layers.24.input_layernorm",
453
+ "model.layers.34.self_attn.indexer.k_norm.bias",
454
+ "model.layers.43.input_layernorm",
455
+ "model.layers.27.self_attn.kv_a_layernorm",
456
+ "model.layers.31.input_layernorm",
457
+ "model.layers.69.mlp.gate",
458
+ "model.layers.26.post_attention_layernorm",
459
+ "model.layers.3.self_attn.q_a_layernorm",
460
+ "model.layers.37.post_attention_layernorm",
461
+ "model.layers.52.mlp.gate",
462
+ "model.layers.73.input_layernorm",
463
+ "model.layers.19.input_layernorm",
464
+ "model.layers.13.self_attn.q_a_layernorm",
465
+ "model.layers.76.input_layernorm",
466
+ "model.layers.8.mlp.gate",
467
+ "model.layers.63.post_attention_layernorm",
468
+ "model.layers.58.mlp.gate",
469
+ "model.layers.31.post_attention_layernorm",
470
+ "model.layers.23.mlp.gate.e_score_correction_bias",
471
+ "model.layers.34.self_attn.q_a_layernorm",
472
+ "model.layers.78.self_attn.indexer.k_norm.bias",
473
+ "model.layers.13.mlp.gate.e_score_correction_bias",
474
+ "model.layers.71.mlp.gate",
475
+ "model.layers.14.input_layernorm",
476
+ "model.layers.17.mlp.gate.e_score_correction_bias",
477
+ "model.layers.8.post_attention_layernorm",
478
+ "model.layers.45.self_attn.kv_a_layernorm",
479
+ "model.layers.46.self_attn.indexer.k_norm",
480
+ "model.layers.20.self_attn.kv_a_layernorm",
481
+ "model.layers.78.self_attn.indexer.k_norm",
482
+ "model.layers.37.mlp.gate",
483
+ "model.layers.7.mlp.gate",
484
+ "model.layers.30.mlp.gate.e_score_correction_bias",
485
+ "model.layers.67.mlp.gate",
486
+ "model.layers.73.self_attn.q_a_layernorm",
487
+ "model.layers.10.self_attn.kv_a_layernorm",
488
+ "model.layers.61.mlp.gate",
489
+ "model.layers.22.mlp.gate.e_score_correction_bias",
490
+ "model.layers.58.input_layernorm",
491
+ "model.layers.8.self_attn.kv_a_layernorm",
492
+ "model.layers.33.self_attn.q_a_layernorm",
493
+ "model.layers.55.self_attn.q_a_layernorm",
494
+ "model.layers.32.input_layernorm",
495
+ "model.layers.17.self_attn.kv_a_layernorm",
496
+ "model.layers.46.self_attn.indexer.k_norm.bias",
497
+ "model.layers.11.mlp.gate.e_score_correction_bias",
498
+ "model.layers.9.input_layernorm",
499
+ "model.layers.77.input_layernorm",
500
+ "model.layers.16.self_attn.kv_a_layernorm",
501
+ "model.layers.45.post_attention_layernorm",
502
+ "model.layers.74.mlp.gate",
503
+ "model.layers.68.self_attn.q_a_layernorm",
504
+ "model.layers.55.self_attn.kv_a_layernorm",
505
+ "model.layers.44.input_layernorm",
506
+ "model.layers.60.self_attn.kv_a_layernorm",
507
+ "model.layers.14.post_attention_layernorm",
508
+ "model.layers.62.self_attn.indexers_proj",
509
+ "model.layers.0.self_attn.q_a_layernorm",
510
+ "model.layers.72.post_attention_layernorm",
511
+ "model.layers.78.post_attention_layernorm",
512
+ "model.layers.60.mlp.gate.e_score_correction_bias",
513
+ "model.layers.26.self_attn.indexer.k_norm.bias",
514
+ "model.layers.38.post_attention_layernorm",
515
+ "model.layers.50.post_attention_layernorm",
516
+ "model.layers.28.input_layernorm",
517
+ "model.layers.30.self_attn.indexer.k_norm",
518
+ "model.layers.37.self_attn.q_a_layernorm",
519
+ "model.layers.56.input_layernorm",
520
+ "model.layers.27.post_attention_layernorm",
521
+ "model.layers.74.input_layernorm",
522
+ "model.layers.78.self_attn.q_a_layernorm",
523
+ "model.layers.42.self_attn.q_a_layernorm",
524
+ "model.layers.26.self_attn.indexer.k_norm",
525
+ "model.layers.42.self_attn.kv_a_layernorm",
526
+ "model.layers.1.self_attn.indexer.k_norm",
527
+ "model.layers.39.post_attention_layernorm",
528
+ "model.layers.48.mlp.gate.e_score_correction_bias",
529
+ "model.layers.74.post_attention_layernorm",
530
+ "model.layers.69.self_attn.q_a_layernorm",
531
+ "model.layers.13.input_layernorm",
532
+ "model.layers.74.self_attn.indexer.k_norm",
533
+ "model.layers.29.mlp.gate",
534
+ "model.layers.7.mlp.gate.e_score_correction_bias",
535
+ "model.layers.3.self_attn.kv_a_layernorm",
536
+ "model.layers.35.self_attn.kv_a_layernorm",
537
+ "model.layers.46.self_attn.indexers_proj",
538
+ "model.layers.0.self_attn.kv_a_layernorm",
539
+ "model.layers.43.self_attn.q_a_layernorm",
540
+ "model.norm",
541
+ "model.layers.1.self_attn.kv_a_layernorm",
542
+ "model.layers.38.self_attn.indexer.k_norm.bias",
543
+ "model.layers.30.self_attn.q_a_layernorm",
544
+ "model.layers.0.input_layernorm",
545
+ "model.layers.47.input_layernorm",
546
+ "model.layers.8.self_attn.q_a_layernorm",
547
+ "model.layers.61.self_attn.q_a_layernorm",
548
+ "model.layers.1.self_attn.indexer.k_norm.bias",
549
+ "model.layers.62.mlp.gate.e_score_correction_bias",
550
+ "model.layers.66.self_attn.q_a_layernorm",
551
+ "model.layers.46.self_attn.kv_a_layernorm",
552
+ "model.layers.66.mlp.gate",
553
+ "model.layers.69.input_layernorm",
554
+ "model.layers.69.self_attn.kv_a_layernorm",
555
+ "model.layers.22.input_layernorm",
556
+ "model.layers.30.post_attention_layernorm",
557
+ "model.layers.24.self_attn.q_a_layernorm",
558
+ "model.layers.48.mlp.gate",
559
+ "model.layers.7.self_attn.kv_a_layernorm",
560
+ "model.layers.67.input_layernorm",
561
+ "model.layers.27.self_attn.q_a_layernorm",
562
+ "model.layers.23.mlp.gate",
563
+ "model.layers.45.mlp.gate.e_score_correction_bias",
564
+ "model.layers.76.self_attn.kv_a_layernorm",
565
+ "model.layers.22.self_attn.q_a_layernorm",
566
+ "model.layers.78.mlp.gate",
567
+ "model.layers.9.mlp.gate",
568
+ "model.layers.57.self_attn.q_a_layernorm",
569
+ "model.layers.78.mlp.gate.e_score_correction_bias",
570
+ "model.layers.23.post_attention_layernorm",
571
+ "model.layers.38.self_attn.q_a_layernorm",
572
+ "model.layers.53.mlp.gate",
573
+ "model.layers.77.self_attn.q_a_layernorm",
574
+ "model.layers.62.self_attn.kv_a_layernorm",
575
+ "model.layers.12.mlp.gate",
576
+ "model.layers.34.self_attn.indexer.k_norm",
577
+ "model.layers.46.self_attn.q_a_layernorm",
578
+ "model.layers.36.self_attn.q_a_layernorm",
579
+ "model.layers.58.self_attn.indexer.k_norm.bias",
580
+ "model.layers.60.post_attention_layernorm",
581
+ "model.layers.28.post_attention_layernorm",
582
+ "model.layers.2.self_attn.kv_a_layernorm",
583
+ "model.layers.70.self_attn.indexers_proj",
584
+ "model.layers.16.mlp.gate.e_score_correction_bias",
585
+ "model.layers.2.self_attn.indexer.k_norm",
586
+ "model.layers.31.self_attn.kv_a_layernorm",
587
+ "model.layers.64.input_layernorm",
588
+ "model.layers.6.mlp.gate.e_score_correction_bias",
589
+ "model.layers.50.self_attn.indexer.k_norm",
590
+ "model.layers.58.self_attn.q_a_layernorm",
591
+ "model.layers.57.mlp.gate.e_score_correction_bias",
592
+ "model.layers.13.post_attention_layernorm",
593
+ "model.layers.28.self_attn.kv_a_layernorm",
594
+ "model.layers.31.self_attn.q_a_layernorm",
595
+ "model.layers.1.self_attn.q_a_layernorm",
596
+ "model.layers.17.mlp.gate",
597
+ "model.layers.15.post_attention_layernorm",
598
+ "model.layers.18.self_attn.indexer.k_norm.bias",
599
+ "model.layers.18.self_attn.indexer.k_norm",
600
+ "model.layers.43.mlp.gate.e_score_correction_bias",
601
+ "model.layers.11.input_layernorm",
602
+ "model.layers.9.post_attention_layernorm",
603
+ "model.layers.33.input_layernorm",
604
+ "model.layers.66.input_layernorm",
605
+ "model.layers.78.self_attn.indexers_proj",
606
+ "model.layers.54.post_attention_layernorm",
607
+ "model.layers.53.mlp.gate.e_score_correction_bias",
608
+ "model.layers.5.mlp.gate.e_score_correction_bias",
609
+ "model.layers.19.mlp.gate",
610
+ "model.layers.49.mlp.gate",
611
+ "model.layers.66.self_attn.indexers_proj",
612
+ "model.layers.14.self_attn.indexer.k_norm.bias",
613
+ "model.layers.41.mlp.gate",
614
+ "model.layers.42.self_attn.indexer.k_norm.bias",
615
+ "model.layers.65.input_layernorm",
616
+ "model.layers.75.mlp.gate",
617
+ "model.layers.12.self_attn.kv_a_layernorm",
618
+ "model.layers.53.input_layernorm",
619
+ "model.layers.58.self_attn.kv_a_layernorm",
620
+ "model.layers.60.self_attn.q_a_layernorm",
621
+ "model.layers.6.mlp.gate",
622
+ "model.layers.49.self_attn.kv_a_layernorm",
623
+ "model.layers.16.mlp.gate",
624
+ "model.layers.22.mlp.gate",
625
+ "model.layers.58.post_attention_layernorm",
626
+ "model.layers.15.mlp.gate.e_score_correction_bias",
627
+ "model.layers.26.input_layernorm",
628
+ "model.layers.3.post_attention_layernorm",
629
+ "model.layers.6.input_layernorm",
630
+ "model.layers.22.self_attn.kv_a_layernorm",
631
+ "model.layers.16.input_layernorm",
632
+ "model.layers.33.post_attention_layernorm",
633
+ "model.layers.17.post_attention_layernorm",
634
+ "model.layers.35.mlp.gate",
635
+ "model.layers.56.post_attention_layernorm",
636
+ "model.layers.7.self_attn.q_a_layernorm",
637
+ "model.layers.24.mlp.gate",
638
+ "model.layers.24.post_attention_layernorm",
639
+ "model.layers.36.mlp.gate.e_score_correction_bias",
640
+ "model.layers.56.mlp.gate.e_score_correction_bias",
641
+ "model.layers.32.post_attention_layernorm",
642
+ "model.layers.47.post_attention_layernorm",
643
+ "model.layers.40.mlp.gate",
644
+ "model.layers.41.self_attn.kv_a_layernorm",
645
+ "model.layers.52.self_attn.kv_a_layernorm",
646
+ "model.layers.9.self_attn.kv_a_layernorm",
647
+ "model.layers.77.mlp.gate.e_score_correction_bias",
648
+ "model.layers.78.shared_head.norm",
649
+ "model.layers.53.post_attention_layernorm",
650
+ "model.layers.63.self_attn.kv_a_layernorm",
651
+ "model.layers.48.self_attn.kv_a_layernorm",
652
+ "model.layers.28.mlp.gate.e_score_correction_bias",
653
+ "model.layers.8.mlp.gate.e_score_correction_bias",
654
+ "model.layers.52.mlp.gate.e_score_correction_bias",
655
+ "model.layers.52.post_attention_layernorm",
656
+ "model.layers.18.self_attn.q_a_layernorm",
657
+ "model.layers.50.mlp.gate.e_score_correction_bias",
658
+ "model.layers.64.mlp.gate.e_score_correction_bias",
659
+ "model.layers.38.mlp.gate.e_score_correction_bias",
660
+ "model.layers.59.input_layernorm",
661
+ "model.layers.21.input_layernorm",
662
+ "model.layers.39.mlp.gate",
663
+ "model.layers.77.post_attention_layernorm",
664
+ "model.layers.76.self_attn.q_a_layernorm",
665
+ "model.layers.12.post_attention_layernorm",
666
+ "model.layers.43.post_attention_layernorm",
667
+ "model.layers.35.input_layernorm",
668
+ "model.layers.62.mlp.gate",
669
+ "model.layers.38.self_attn.kv_a_layernorm",
670
+ "model.layers.63.mlp.gate",
671
+ "model.layers.27.mlp.gate.e_score_correction_bias",
672
+ "model.layers.18.mlp.gate",
673
+ "model.layers.25.post_attention_layernorm",
674
+ "model.layers.55.mlp.gate.e_score_correction_bias",
675
+ "model.layers.66.self_attn.indexer.k_norm",
676
+ "model.layers.48.self_attn.q_a_layernorm",
677
+ "model.layers.78.eh_proj",
678
+ "model.layers.77.mlp.gate",
679
+ "model.layers.6.self_attn.indexers_proj",
680
+ "model.layers.14.self_attn.q_a_layernorm",
681
+ "model.layers.2.self_attn.q_a_layernorm",
682
+ "model.layers.42.mlp.gate",
683
+ "model.layers.54.mlp.gate.e_score_correction_bias",
684
+ "model.layers.60.input_layernorm",
685
+ "model.layers.40.self_attn.kv_a_layernorm",
686
+ "model.layers.51.self_attn.kv_a_layernorm",
687
+ "model.layers.18.input_layernorm",
688
+ "model.layers.30.self_attn.indexers_proj",
689
+ "model.layers.53.self_attn.kv_a_layernorm",
690
+ "model.layers.6.post_attention_layernorm",
691
+ "model.layers.70.self_attn.indexer.k_norm.bias",
692
+ "model.layers.54.input_layernorm",
693
+ "model.layers.41.self_attn.q_a_layernorm",
694
+ "model.layers.18.mlp.gate.e_score_correction_bias",
695
+ "model.layers.34.mlp.gate.e_score_correction_bias",
696
+ "model.layers.47.mlp.gate",
697
+ "model.layers.46.mlp.gate",
698
+ "model.layers.25.self_attn.q_a_layernorm",
699
+ "model.layers.63.mlp.gate.e_score_correction_bias",
700
+ "model.layers.66.post_attention_layernorm",
701
+ "model.layers.76.mlp.gate",
702
+ "model.embed_tokens",
703
+ "model.layers.67.self_attn.kv_a_layernorm",
704
+ "model.layers.67.mlp.gate.e_score_correction_bias",
705
+ "model.layers.26.self_attn.kv_a_layernorm",
706
+ "model.layers.31.mlp.gate",
707
+ "model.layers.18.self_attn.kv_a_layernorm",
708
+ "model.layers.34.mlp.gate",
709
+ "model.layers.55.post_attention_layernorm",
710
+ "model.layers.30.input_layernorm",
711
+ "model.layers.50.mlp.gate",
712
+ "model.layers.63.input_layernorm",
713
+ "model.layers.11.self_attn.kv_a_layernorm",
714
+ "model.layers.24.self_attn.kv_a_layernorm",
715
+ "model.layers.75.mlp.gate.e_score_correction_bias",
716
+ "model.layers.10.post_attention_layernorm",
717
+ "model.layers.1.self_attn.indexers_proj",
718
+ "model.layers.42.self_attn.indexers_proj",
719
+ "model.layers.46.input_layernorm",
720
+ "model.layers.23.self_attn.q_a_layernorm",
721
+ "model.layers.63.self_attn.q_a_layernorm",
722
+ "model.layers.14.self_attn.indexer.k_norm",
723
+ "model.layers.14.self_attn.indexers_proj",
724
+ "model.layers.60.mlp.gate",
725
+ "model.layers.70.mlp.gate.e_score_correction_bias",
726
+ "model.layers.53.self_attn.q_a_layernorm",
727
+ "model.layers.0.post_attention_layernorm",
728
+ "model.layers.35.mlp.gate.e_score_correction_bias",
729
+ "model.layers.75.self_attn.kv_a_layernorm",
730
+ "model.layers.1.input_layernorm",
731
+ "model.layers.42.post_attention_layernorm",
732
+ "model.layers.37.input_layernorm",
733
+ "model.layers.9.mlp.gate.e_score_correction_bias",
734
+ "model.layers.46.post_attention_layernorm",
735
+ "model.layers.26.mlp.gate",
736
+ "model.layers.65.mlp.gate.e_score_correction_bias",
737
+ "model.layers.67.self_attn.q_a_layernorm",
738
+ "model.layers.44.mlp.gate.e_score_correction_bias",
739
+ "model.layers.33.mlp.gate",
740
+ "model.layers.29.input_layernorm",
741
+ "model.layers.18.post_attention_layernorm",
742
+ "model.layers.75.post_attention_layernorm",
743
+ "model.layers.44.self_attn.q_a_layernorm",
744
+ "model.layers.10.mlp.gate",
745
+ "model.layers.21.mlp.gate",
746
+ "model.layers.58.mlp.gate.e_score_correction_bias",
747
+ "model.layers.7.post_attention_layernorm",
748
+ "model.layers.50.self_attn.q_a_layernorm",
749
+ "model.layers.9.self_attn.q_a_layernorm",
750
+ "model.layers.2.self_attn.indexers_proj",
751
+ "model.layers.0.self_attn.indexer.k_norm.bias",
752
+ "model.layers.74.self_attn.kv_a_layernorm",
753
+ "model.layers.74.mlp.gate.e_score_correction_bias",
754
+ "model.layers.49.mlp.gate.e_score_correction_bias",
755
+ "model.layers.62.post_attention_layernorm",
756
+ "model.layers.57.self_attn.kv_a_layernorm",
757
+ "model.layers.40.input_layernorm",
758
+ "model.layers.69.mlp.gate.e_score_correction_bias",
759
+ "model.layers.35.post_attention_layernorm",
760
+ "model.layers.74.self_attn.indexers_proj",
761
+ "model.layers.55.input_layernorm",
762
+ "model.layers.49.input_layernorm",
763
+ "model.layers.40.mlp.gate.e_score_correction_bias",
764
+ "model.layers.25.mlp.gate",
765
+ "model.layers.2.self_attn.indexer.k_norm.bias",
766
+ "model.layers.68.self_attn.kv_a_layernorm",
767
+ "model.layers.51.input_layernorm",
768
+ "model.layers.22.self_attn.indexers_proj",
769
+ "model.layers.10.input_layernorm",
770
+ "model.layers.42.self_attn.indexer.k_norm",
771
+ "model.layers.56.self_attn.kv_a_layernorm",
772
+ "model.layers.17.self_attn.q_a_layernorm",
773
+ "model.layers.59.self_attn.kv_a_layernorm",
774
+ "model.layers.3.input_layernorm",
775
+ "model.layers.57.input_layernorm",
776
+ "model.layers.38.input_layernorm",
777
+ "model.layers.62.self_attn.indexer.k_norm",
778
+ "model.layers.20.mlp.gate",
779
+ "model.layers.64.mlp.gate",
780
+ "model.layers.28.self_attn.q_a_layernorm",
781
+ "model.layers.32.self_attn.kv_a_layernorm",
782
+ "model.layers.10.mlp.gate.e_score_correction_bias",
783
+ "model.layers.5.self_attn.q_a_layernorm",
784
+ "model.layers.37.self_attn.kv_a_layernorm"
785
+ ]
786
+ },
787
+ "mtp_bits": 16,
788
+ "scope": "glm53_routed_experts_only",
789
+ "keys_profile": "routed-experts-3bpw-mul1; attn/dense/shared/head bf16",
790
+ "keys_source": "zai-org/GLM-5.3 (FP8) via exllamav3 1.4.5 + keys encode farm"
791
+ }
792
+ }
generation_config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "eos_token_id": [
4
+ 154820,
5
+ 154827,
6
+ 154829
7
+ ],
8
+ "pad_token_id": 154820,
9
+ "temperature": 1.0,
10
+ "top_p": 0.95,
11
+ "transformers_version": "5.12.0"
12
+ }
model-00001-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fec34deec06cd68b9ced94494ab653836c7e645bf98dadee46cdf1e98432ca2b
3
+ size 8357250403
model-00005-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:966889d2a98cede970ea40b1d006ebcd40d24e372b81ed1cac6b807340b645b2
3
+ size 8111403478
model-00009-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cdf80c8975f78f82ef7200be56f0ef342cc534b01b5d4c0ba94b9483a8ad46af
3
+ size 8111403478
model-00010-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b383a7f2c463d59745dbcaf9883d656328550611f84475ce83e035b848c37db
3
+ size 8092659062
model-00011-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6488dcf79d89daee948e4ea43c54e8eb4ec23857f12a1a874c04870f7a5700a2
3
+ size 8111403478
model-00013-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed1ba921b27c719cf09cefa0f80a92deee6633964bd6990612a142e2378cb6f2
3
+ size 8111403478
model-00014-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:92ecb399799bb0d1d11c8b4469f80c4f84cf63d086ce5d38dc9e7bf65a97819c
3
+ size 8092659062
model-00019-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7c12183bb06e161c2df208eb208a80729d3034ab1c02607e5ebd44384709096c
3
+ size 8111403478
model-00020-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe76908fb0495d600f2cca59d1d6802d3529d74959e9685712a9eba025817c40
3
+ size 8092659062
model-00021-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a8f4340829f726f92fdb5b10823ffe0f4a4578db1d73b8eeefdeeff45b3e3261
3
+ size 8111403478
model-00023-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5ba92d7e5210f9565ac587f776cbaed146febbf6b9ce1f2de9b835c14a6b6cf3
3
+ size 8111403478
model-00024-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dca97644a9978c7f42b498bb84df2bc81af17f9b55000781a26d992bf2c2052c
3
+ size 8092659062
model-00030-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5f0c68eab43e468fa8ae8607a069ecb02bca8b670645322cbe56dacd3eb38916
3
+ size 8092659062
model-00031-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4ccc7fb59c8f4a3c81b3d4d64c6d0880aee12e1748c750f351aa8c80410dc581
3
+ size 8111403478
model-00032-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98525b8f4757949a7e60ec075c5dec20b2d2208f220a673b3db8c89d31e1823d
3
+ size 8092659062
model-00034-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:72777c7367f3973d9d10ca88e8fe95896d65342bc8a27511f057079f67599911
3
+ size 8092659062
model-00035-of-00041.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e8f21f16c2a599412edecfaee695d48cb0c2ac7ffa6b18fc37941f30137aa7b3
3
+ size 8111403478
quant/build.sh ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # exllamav3 1.4.5 native sm_121a build inside the Mia image (run via docker exec)
3
+ set -x
4
+ CU=/usr/local/lib/python3.12/dist-packages/nvidia/cu13/include
5
+ for h in $CU/*.h; do b=$(basename $h); [ -e /usr/local/cuda/include/$b ] || ln -s $h /usr/local/cuda/include/$b; done
6
+ cd /work/exllamav3
7
+ unset EXLLAMA_NOCOMPILE
8
+ export TORCH_CUDA_ARCH_LIST=12.1a MAX_JOBS=20
9
+ pip install --no-deps --no-build-isolation -v . 2>&1 | tail -40
10
+ echo BUILD_EXIT=${PIPESTATUS[0]}
11
+ ls -la /usr/local/lib/python3.12/dist-packages/exllamav3_ext*.so
12
+ python3 -c "import exllamav3, exllamav3.ext; print(\"IMPORT_OK\", exllamav3.__version__)"
13
+ python3 -c "from exllamav3.architecture.glm_moe_dsa import GlmMoeDsaConfig; print(\"ARCH_OK\")"
14
+ echo BUILD_DONE
quant/exl3_farm.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """EXL3 encode farm: run exllamav3's trellis quantization of individual linears on remote GPUs.
3
+
4
+ The converter's layer chain is sequential (each layer is calibrated on the quantized output of the
5
+ previous ones), but inside a layer every expert matrix is encoded independently given its Hessian.
6
+ This module lets the master (running convert.py) ship (weights, finalized H_data, quant_args) for a
7
+ group of linears to a worker GPU on another node and get back the packed EXL3 tensors.
8
+
9
+ Worker: python3 exl3_farm.py serve --port 5599
10
+ Client: quantize_remote(host, port, weights, H_datas, quant_args_list) -> (results, quant_args_list)
11
+
12
+ Wire format: 8-byte little-endian length + torch.save payload (pickled dict of CPU tensors).
13
+ """
14
+ import argparse, io, socket, struct, sys, time, threading, traceback
15
+
16
+ import torch
17
+
18
+ MAGIC_OK = b"OK"
19
+ MAGIC_ERR = b"ER"
20
+
21
+
22
+ def _send(sock: socket.socket, obj) -> None:
23
+ buf = io.BytesIO()
24
+ torch.save(obj, buf)
25
+ data = buf.getvalue()
26
+ sock.sendall(struct.pack("<Q", len(data)))
27
+ sock.sendall(data)
28
+
29
+
30
+ def _recv_exact(sock: socket.socket, n: int) -> bytes:
31
+ chunks = []
32
+ got = 0
33
+ while got < n:
34
+ c = sock.recv(min(n - got, 64 << 20))
35
+ if not c:
36
+ raise ConnectionError("socket closed")
37
+ chunks.append(c)
38
+ got += len(c)
39
+ return b"".join(chunks)
40
+
41
+
42
+ def _recv(sock: socket.socket, map_location="cpu"):
43
+ n = struct.unpack("<Q", _recv_exact(sock, 8))[0]
44
+ data = _recv_exact(sock, n)
45
+ return torch.load(io.BytesIO(data), map_location=map_location, weights_only=False)
46
+
47
+
48
+ # ------------------------------------------------------------------ client (master side)
49
+ _H_KEYS = ("H", "first_key", "count", "finalized", "num_total", "inf_nan", "su", "L", "diag", "q_fallback")
50
+
51
+
52
+ def _export_H(hd: dict) -> dict:
53
+ out = {}
54
+ for k in _H_KEYS:
55
+ v = hd.get(k)
56
+ if isinstance(v, torch.Tensor):
57
+ v = v.detach().to("cpu")
58
+ out[k] = v
59
+ return out
60
+
61
+
62
+ def quantize_remote(host: str, port: int, weights, H_datas, quant_args_list, timeout=3600):
63
+ """Quantize a group of linears on a remote worker. weights: list of (in,out) fp16/fp32 tensors
64
+ (CPU ok); H_datas: list of FINALIZED H_data dicts (may repeat the same object); returns
65
+ ([(proxy_err, out_tensors_cpu), ...], updated quant_args_list)."""
66
+ pool, idx = [], []
67
+ for hd in H_datas:
68
+ for i, p in enumerate(pool):
69
+ if p is hd:
70
+ idx.append(i)
71
+ break
72
+ else:
73
+ pool.append(hd)
74
+ idx.append(len(pool) - 1)
75
+ req = {
76
+ "weights": [w.detach().to("cpu") for w in weights],
77
+ "H_pool": [_export_H(hd) for hd in pool],
78
+ "H_idx": idx,
79
+ "quant_args_list": [{k: v for k, v in qa.items() if k != "devices"} for qa in quant_args_list],
80
+ }
81
+ sock = socket.create_connection((host, port), timeout=timeout)
82
+ try:
83
+ sock.settimeout(timeout)
84
+ _send(sock, req)
85
+ rep = _recv(sock, map_location="cpu")
86
+ finally:
87
+ sock.close()
88
+ if rep.get("status") != "ok":
89
+ raise RuntimeError(f"remote {host}:{port} failed: {rep.get('error')}")
90
+ for qa, nqa in zip(quant_args_list, rep["quant_args_list"]):
91
+ for k, v in nqa.items():
92
+ if k != "devices":
93
+ qa[k] = v
94
+ return rep["results"], quant_args_list
95
+
96
+
97
+ # ------------------------------------------------------------------ server (worker side)
98
+ def _handle(conn: socket.socket, device: torch.device, log):
99
+ from exllamav3.modules.quant.exl3_lib.quantize import quantize_exl3, quantize_exl3_batch
100
+
101
+ t0 = time.time()
102
+ req = _recv(conn, map_location="cpu")
103
+ t1 = time.time()
104
+ try:
105
+ with torch.inference_mode():
106
+ pool = req["H_pool"]
107
+ for hd in pool:
108
+ for k, v in list(hd.items()):
109
+ if isinstance(v, torch.Tensor):
110
+ hd[k] = v.to(device)
111
+ hd["device"] = device
112
+ H_datas = [pool[i] for i in req["H_idx"]]
113
+ weights = req["weights"]
114
+ qal = req["quant_args_list"]
115
+ for qa in qal:
116
+ qa["devices"] = [device.index or 0]
117
+ qa["device_ratios"] = None
118
+ qa["debug_dir"] = "/tmp/exl3-farm-debug"
119
+ if len(weights) > 1:
120
+ results = quantize_exl3_batch(weights, H_datas, qal)
121
+ else:
122
+ _, pe, out = quantize_exl3(weights[0].to(device).float(), H_datas[0], qal[0], False)
123
+ results = [(pe, out)]
124
+ cpu_results = []
125
+ for pe, out in results:
126
+ cpu_results.append((float(pe), {k: v.detach().to("cpu") for k, v in out.items() if v is not None}))
127
+ rep = {
128
+ "status": "ok",
129
+ "results": cpu_results,
130
+ "quant_args_list": [{k: (v.detach().to("cpu") if isinstance(v, torch.Tensor) else v)
131
+ for k, v in qa.items() if k not in ("devices",)} for qa in qal],
132
+ }
133
+ torch.cuda.synchronize(device)
134
+ except Exception as e: # noqa: BLE001
135
+ rep = {"status": "error", "error": f"{e!r}\n{traceback.format_exc()}"}
136
+ log(f"ERROR: {e!r}")
137
+ t2 = time.time()
138
+ _send(conn, rep)
139
+ n = len(req["weights"])
140
+ log(f"group of {n}: recv {t1 - t0:.2f}s quant {t2 - t1:.2f}s send {time.time() - t2:.2f}s")
141
+
142
+
143
+ def serve(port: int, device_index: int, concurrency: int):
144
+ device = torch.device(f"cuda:{device_index}")
145
+ torch.cuda.set_device(device)
146
+ lock = threading.Semaphore(concurrency)
147
+
148
+ def log(msg):
149
+ print(f"[farm {time.strftime('%H:%M:%S')}] {msg}", flush=True)
150
+
151
+ # warm the kernels once so the first real request does not pay JIT/allocation
152
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
153
+ srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
154
+ srv.bind(("0.0.0.0", port))
155
+ srv.listen(8)
156
+ log(f"listening on :{port} device={device} concurrency={concurrency}")
157
+
158
+ def worker(conn):
159
+ with lock:
160
+ try:
161
+ _handle(conn, device, log)
162
+ except Exception as e: # noqa: BLE001
163
+ log(f"connection failed: {e!r}")
164
+ finally:
165
+ conn.close()
166
+
167
+ while True:
168
+ conn, addr = srv.accept()
169
+ conn.settimeout(3600)
170
+ threading.Thread(target=worker, args=(conn,), daemon=True).start()
171
+
172
+
173
+ if __name__ == "__main__":
174
+ ap = argparse.ArgumentParser()
175
+ sub = ap.add_subparsers(dest="cmd")
176
+ s = sub.add_parser("serve")
177
+ s.add_argument("--port", type=int, default=5599)
178
+ s.add_argument("--device", type=int, default=0)
179
+ s.add_argument("--concurrency", type=int, default=1)
180
+ a = ap.parse_args()
181
+ if a.cmd == "serve":
182
+ serve(a.port, a.device, a.concurrency)
183
+ else:
184
+ ap.print_help()
185
+ sys.exit(1)
quant/make_recipe.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Generate the exllamav3 per-tensor recipe for a Mia-native GLM-5.3 EXL3 profile.
3
+
4
+ Profile (matches MiaAI-Lab GLM-5.3-Flash EXL3 scope=glm53_routed_experts_only):
5
+ * routed experts gate/up/down (layers first_k_dense..77) -> EXPERT_BITS (uniform, mcg codebook)
6
+ * everything else that exllamav3 budgets (MLA attention projections, indexer wq_b,
7
+ dense MLP layers 0-2, shared experts) -> 16 (stored unquantized)
8
+ * lm_head -> --head_bits 16, MTP layer 78 -> --mtp_bits 16 (not in the recipe)
9
+ Usage: make_recipe.py <model.safetensors.index.json> <config.json> <out.yaml> [expert_bits]
10
+ """
11
+ import json, re, sys
12
+
13
+ index, config, out = sys.argv[1:4]
14
+ EXPERT_BITS = int(sys.argv[4]) if len(sys.argv) > 4 else 3
15
+
16
+ cfg = json.load(open(config))
17
+ n_layers = cfg["num_hidden_layers"]
18
+ first_dense = cfg.get("first_k_dense_replace", 3)
19
+ wm = json.load(open(index))["weight_map"]
20
+
21
+ NONEXPERT = {
22
+ "self_attn.q_a_proj", "self_attn.q_b_proj", "self_attn.kv_a_proj_with_mqa",
23
+ "self_attn.o_proj", "self_attn.indexer.wq_b",
24
+ "mlp.gate_proj", "mlp.up_proj", "mlp.down_proj",
25
+ "mlp.shared_experts.gate_proj", "mlp.shared_experts.up_proj", "mlp.shared_experts.down_proj",
26
+ }
27
+ EXPERT_RE = re.compile(r"mlp\.experts\.(\d+)\.(gate|up|down)_proj$")
28
+
29
+ tensors = {}
30
+ for k in wm:
31
+ if not k.endswith(".weight"):
32
+ continue
33
+ m = re.match(r"model\.layers\.(\d+)\.(.*)\.weight$", k)
34
+ if not m:
35
+ continue
36
+ layer, rest = int(m.group(1)), m.group(2)
37
+ if layer >= n_layers: # MTP layer(s): governed by --mtp_bits
38
+ continue
39
+ key = f"model.layers.{layer}.{rest}"
40
+ if EXPERT_RE.match(rest):
41
+ assert layer >= first_dense, key
42
+ tensors[key] = EXPERT_BITS
43
+ elif rest in NONEXPERT:
44
+ tensors[key] = 16
45
+
46
+ n_exp = sum(1 for v in tensors.values() if v == EXPERT_BITS)
47
+ n_16 = sum(1 for v in tensors.values() if v == 16)
48
+ with open(out, "w") as f:
49
+ f.write(f"# GLM-5.3 (753B) Mia-native EXL3 recipe: routed experts {EXPERT_BITS}b mcg, rest bf16\n")
50
+ f.write(f"target_bpw: {EXPERT_BITS}\nhead_bits: 16\ntensors:\n")
51
+ for k in sorted(tensors, key=lambda s: (int(s.split('.')[2]), s)):
52
+ f.write(f' "{k}": {tensors[k]}\n')
53
+ print(f"recipe: {len(tensors)} tensors experts@{EXPERT_BITS}b={n_exp} unquantized(16)={n_16} -> {out}")
quant/orchestrate.py ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Rolling-shard driver for exllamav3 convert.py on a disk-limited node (stdlib only, runs on host).
3
+
4
+ Why: the FP8 source (zai-org/GLM-5.3, 141 shards, 756 GB) does not fit next to the ~300 GB work
5
+ dir and ~300 GB output. exllamav3 walks modules in order and checkpoints per module, so we:
6
+ 1. keep a rolling window of source shards (download ahead in a thread),
7
+ 2. run convert.py in a fresh container per step with --max_module so it stops after layer L,
8
+ 3. between steps, rewrite finished shards dropping only `model.layers.{L}.mlp.experts.*`
9
+ (norms/attn/shared/router stay, compile needs them), then advance.
10
+ Every convert process rescans the source dir at start, so rewritten shards are always consistent.
11
+
12
+ Layout (ROOT): src/ (rolling source), work/ (exllamav3 work dir), out/ (final), runs/ (logs),
13
+ state.json (progress), shards.json (HF sizes + sha256).
14
+ """
15
+ import json, os, re, sys, time, struct, subprocess, threading, hashlib, shutil, urllib.request
16
+
17
+ ROOT = os.environ.get("ROOT", "/home/keyspark/glm53-full-exl3")
18
+ SRC, WORK, OUT, RUNS = (os.path.join(ROOT, d) for d in ("src", "work", "out", "runs"))
19
+ STUB = os.path.join(ROOT, "src-stub")
20
+ STATE = os.path.join(ROOT, "state.json")
21
+ LOG = os.path.join(ROOT, "orchestrate.log")
22
+ REPO = "zai-org/GLM-5.3"
23
+ HF = f"https://huggingface.co/{REPO}/resolve/main/"
24
+ IMAGE = os.environ.get("IMAGE", "glm53-exl3-quant:1.4.5")
25
+ CONTAINER = "glm53-exl3-convert"
26
+ RECIPE = os.path.join(ROOT, "recipe.yaml")
27
+ EXPERT_BITS = int(os.environ.get("EXPERT_BITS", "3"))
28
+ LAYERS_PER_RUN = int(os.environ.get("LAYERS_PER_RUN", "2"))
29
+ WINDOW = int(os.environ.get("WINDOW", "5")) # layers downloaded ahead of the current one
30
+ MIN_FREE_GB = int(os.environ.get("MIN_FREE_GB", "80"))
31
+ MEM_CAP = os.environ.get("MEM_CAP", "112g")
32
+ NUM_LAYERS = 78
33
+ MTP_LAYER = 78
34
+
35
+ def log(msg):
36
+ line = f"[{time.strftime('%m-%d %H:%M:%S')}] {msg}"
37
+ print(line, flush=True)
38
+ with open(LOG, "a") as f:
39
+ f.write(line + "\n")
40
+
41
+ def free_gb(path=ROOT):
42
+ st = os.statvfs(path)
43
+ return st.f_bavail * st.f_frsize / 1e9
44
+
45
+ # ---------------------------------------------------------------- index / shard maps
46
+ index = json.load(open(os.path.join(STUB, "model.safetensors.index.json")))["weight_map"]
47
+ layer_shards = {} # layer -> set(shard)
48
+ shard_layers = {} # shard -> set(layer)
49
+ special = set() # shards holding embed/lm_head/norm
50
+ for k, s in index.items():
51
+ m = re.match(r"model\.layers\.(\d+)\.", k)
52
+ if m:
53
+ L = int(m.group(1))
54
+ layer_shards.setdefault(L, set()).add(s)
55
+ shard_layers.setdefault(s, set()).add(L)
56
+ else:
57
+ special.add(s)
58
+ shard_layers.setdefault(s, set())
59
+ ALL_SHARDS = sorted(shard_layers)
60
+ PERMANENT = set(special) | layer_shards[MTP_LAYER] # eh_proj must exist at every resume
61
+
62
+ def load_state():
63
+ if os.path.exists(STATE):
64
+ return json.load(open(STATE))
65
+ return {"done_layers": [], "dropped": {}, "started": False, "finished": False}
66
+
67
+ def save_state(st):
68
+ tmp = STATE + ".tmp"
69
+ json.dump(st, open(tmp, "w"), indent=1)
70
+ os.replace(tmp, STATE)
71
+
72
+ # ---------------------------------------------------------------- HF shard metadata
73
+ def shard_meta():
74
+ p = os.path.join(ROOT, "shards.json")
75
+ if os.path.exists(p):
76
+ return json.load(open(p))
77
+ with urllib.request.urlopen(f"https://huggingface.co/api/models/{REPO}?blobs=true", timeout=60) as r:
78
+ d = json.load(r)
79
+ meta = {}
80
+ for s in d["siblings"]:
81
+ fn = s["rfilename"]
82
+ if fn.endswith(".safetensors"):
83
+ lfs = s.get("lfs") or {}
84
+ meta[fn] = {"size": s.get("size") or lfs.get("size"), "sha256": lfs.get("oid")}
85
+ json.dump(meta, open(p, "w"), indent=1)
86
+ return meta
87
+
88
+ META = shard_meta()
89
+
90
+ # ---------------------------------------------------------------- download (thread)
91
+ dl_lock = threading.Lock()
92
+ dl_inflight = set()
93
+
94
+ def sha256_file(path):
95
+ h = hashlib.sha256()
96
+ with open(path, "rb") as f:
97
+ for chunk in iter(lambda: f.read(64 << 20), b""):
98
+ h.update(chunk)
99
+ return h.hexdigest()
100
+
101
+ def download(shard):
102
+ dst = os.path.join(SRC, shard)
103
+ part = dst + ".part"
104
+ if os.path.exists(dst):
105
+ return True
106
+ # one curl per shard: the prefetch thread and the main thread may want the same file
107
+ with dl_lock:
108
+ if shard in dl_inflight:
109
+ busy = True
110
+ else:
111
+ dl_inflight.add(shard)
112
+ busy = False
113
+ if busy:
114
+ while shard in dl_inflight:
115
+ time.sleep(5)
116
+ return os.path.exists(dst)
117
+ try:
118
+ return _download(shard, dst, part)
119
+ finally:
120
+ with dl_lock:
121
+ dl_inflight.discard(shard)
122
+
123
+ def _download(shard, dst, part):
124
+ for attempt in range(5):
125
+ rc = subprocess.call(["curl", "-sSL", "-C", "-", "--retry", "8", "--retry-delay", "5",
126
+ "-o", part, HF + shard])
127
+ if rc == 0 and os.path.exists(part):
128
+ size = os.path.getsize(part)
129
+ want = META.get(shard, {}).get("size")
130
+ if want and size != want:
131
+ log(f" size mismatch {shard}: {size} != {want}, retrying")
132
+ os.remove(part)
133
+ continue
134
+ want_sha = META.get(shard, {}).get("sha256")
135
+ if want_sha:
136
+ got = sha256_file(part)
137
+ if got != want_sha:
138
+ log(f" sha256 mismatch {shard}, retrying")
139
+ os.remove(part)
140
+ continue
141
+ os.replace(part, dst)
142
+ log(f" downloaded {shard} ({size/1e9:.1f} GB) free={free_gb():.0f} GB")
143
+ return True
144
+ time.sleep(10)
145
+ log(f"!! download failed for {shard}")
146
+ return False
147
+
148
+ def needed_shards(upto_layer):
149
+ """Shards required for layers <= upto_layer that are not done, plus permanent ones."""
150
+ st = load_state()
151
+ done = set(st["done_layers"])
152
+ need = set(PERMANENT)
153
+ for L in range(0, min(upto_layer, NUM_LAYERS - 1) + 1):
154
+ if L not in done:
155
+ need |= layer_shards[L]
156
+ return need
157
+
158
+ def prefetch_loop(get_current):
159
+ while True:
160
+ try:
161
+ st = load_state()
162
+ if st.get("finished"):
163
+ return
164
+ cur = get_current()
165
+ for shard in sorted(needed_shards(cur + WINDOW)):
166
+ dst = os.path.join(SRC, shard)
167
+ if os.path.exists(dst):
168
+ continue
169
+ if free_gb() < MIN_FREE_GB:
170
+ break
171
+ download(shard)
172
+ except Exception as e:
173
+ log(f"prefetch error: {e!r}")
174
+ time.sleep(15)
175
+
176
+ # ---------------------------------------------------------------- shard rewrite (pure python)
177
+ def rewrite_shard(path, drop_prefixes):
178
+ """Rewrite a safetensors file keeping only tensors not starting with drop_prefixes."""
179
+ with open(path, "rb") as f:
180
+ n = struct.unpack("<Q", f.read(8))[0]
181
+ hdr = json.loads(f.read(n))
182
+ base = 8 + n
183
+ meta = hdr.pop("__metadata__", None)
184
+ keep = {k: v for k, v in hdr.items() if not any(k.startswith(p) for p in drop_prefixes)}
185
+ if len(keep) == len(hdr):
186
+ return False
187
+ items = sorted(keep.items(), key=lambda kv: kv[1]["data_offsets"][0])
188
+ new = {}
189
+ off = 0
190
+ for k, v in items:
191
+ s, e = v["data_offsets"]
192
+ new[k] = {"dtype": v["dtype"], "shape": v["shape"], "data_offsets": [off, off + (e - s)]}
193
+ off += e - s
194
+ if meta is not None:
195
+ new["__metadata__"] = meta
196
+ hb = json.dumps(new, separators=(",", ":")).encode()
197
+ hb += b" " * ((8 - len(hb) % 8) % 8)
198
+ tmp = path + ".rewrite"
199
+ with open(tmp, "wb") as o:
200
+ o.write(struct.pack("<Q", len(hb)))
201
+ o.write(hb)
202
+ for k, v in items:
203
+ s, e = v["data_offsets"]
204
+ f.seek(base + s)
205
+ remaining = e - s
206
+ while remaining:
207
+ chunk = f.read(min(64 << 20, remaining))
208
+ o.write(chunk)
209
+ remaining -= len(chunk)
210
+ before = os.path.getsize(path)
211
+ os.replace(tmp, path)
212
+ log(f" rewrote {os.path.basename(path)}: {before/1e9:.1f} -> {os.path.getsize(path)/1e9:.1f} GB")
213
+ return True
214
+
215
+ def drop_done_experts(st):
216
+ done = set(st["done_layers"])
217
+ for shard in ALL_SHARDS:
218
+ path = os.path.join(SRC, shard)
219
+ if not os.path.exists(path):
220
+ continue
221
+ already = set(st["dropped"].get(shard, []))
222
+ todo = sorted((shard_layers[shard] & done) - already)
223
+ todo = [L for L in todo if L != MTP_LAYER or st.get("mtp_done")]
224
+ if not todo:
225
+ continue
226
+ prefixes = [f"model.layers.{L}.mlp.experts." for L in todo]
227
+ rewrite_shard(path, prefixes)
228
+ st["dropped"][shard] = sorted(already | set(todo))
229
+ save_state(st)
230
+
231
+ # ---------------------------------------------------------------- convert runs
232
+ def image_ready():
233
+ return subprocess.call(["docker", "image", "inspect", IMAGE], stdout=subprocess.DEVNULL,
234
+ stderr=subprocess.DEVNULL) == 0
235
+
236
+ def run_convert(max_module, first, tag):
237
+ os.makedirs(RUNS, exist_ok=True)
238
+ logf = os.path.join(RUNS, f"run-{tag}.log")
239
+ cmd = ["docker", "run", "--rm", "--name", CONTAINER, "--gpus", "all", "--ipc=host", "--network", "host",
240
+ "--memory", MEM_CAP, "--memory-swap", MEM_CAP,
241
+ "-v", f"{ROOT}:/work", "-e", "TORCH_CUDA_ARCH_LIST=12.1a", "-e", "PYTHONUNBUFFERED=1",
242
+ "-e", "PYTHONPATH=/work/exllamav3:/work", "-e", "PYTHONDONTWRITEBYTECODE=1"]
243
+ # encode farm: remote worker GPUs, re-read from farm.env before every run (edit without restart)
244
+ fenv = os.path.join(ROOT, "farm.env")
245
+ if os.path.exists(fenv):
246
+ for line in open(fenv):
247
+ line = line.strip()
248
+ if line and not line.startswith("#") and "=" in line:
249
+ cmd += ["-e", line]
250
+ log(f" farm env: {line}")
251
+ cmd += ["--entrypoint", "python3", IMAGE, "/work/exllamav3/convert.py", "-w", "/work/work"]
252
+ if first:
253
+ cmd += ["-i", "/work/src", "-o", "/work/out", "-rcp", "/work/recipe.yaml",
254
+ "-b", str(EXPERT_BITS), "-hb", "16", "-mb", "16", "-cb", "mcg", "-cpi", "0", "-d", "0"]
255
+ else:
256
+ cmd += ["-r"]
257
+ if max_module is not None:
258
+ cmd += ["--max_module", str(max_module)]
259
+ log(f"RUN {tag}: {' '.join(cmd[-8:])} -> {logf}")
260
+ with open(logf, "w") as lf:
261
+ lf.write(" ".join(cmd) + "\n")
262
+ lf.flush()
263
+ rc = subprocess.call(cmd, stdout=lf, stderr=subprocess.STDOUT)
264
+ log(f"RUN {tag} exit={rc}")
265
+ return rc
266
+
267
+ def ensure_present(shards):
268
+ while True:
269
+ missing = [s for s in sorted(shards) if not os.path.exists(os.path.join(SRC, s))]
270
+ if not missing:
271
+ return
272
+ log(f" waiting for {len(missing)} shard(s): {missing[:3]}... free={free_gb():.0f} GB")
273
+ for s in missing:
274
+ if free_gb() < MIN_FREE_GB - 20:
275
+ break
276
+ download(s)
277
+ time.sleep(20)
278
+
279
+
280
+ def container_running():
281
+ out = subprocess.run(["docker", "ps", "-q", "-f", f"name=^{CONTAINER}$"], capture_output=True, text=True).stdout
282
+ return bool(out.strip())
283
+
284
+ def adopt_running(st):
285
+ """If a previous orchestrator left a convert run going, wait for it and credit its layers from
286
+ the converter's own checkpoint (work/ckpt/job.json: next_module_idx = layer + 2)."""
287
+ if not container_running():
288
+ return
289
+ log("adopting a running convert container; waiting for it to exit ...")
290
+ while container_running():
291
+ time.sleep(30)
292
+ try:
293
+ job = json.load(open(os.path.join(WORK, "ckpt", "job.json")))
294
+ nmi = int(job["next_module_idx"])
295
+ except Exception as e:
296
+ log(f"adopt: cannot read job.json ({e!r}); layers will be re-derived by the next run")
297
+ return
298
+ st = load_state()
299
+ done_before = len(st["done_layers"])
300
+ for L in range(0, min(nmi - 1, NUM_LAYERS)):
301
+ if L not in st["done_layers"]:
302
+ st["done_layers"].append(L)
303
+ st["started"] = True
304
+ save_state(st)
305
+ log(f"adopted: next_module_idx={nmi} -> done layers {done_before} -> {len(st['done_layers'])}")
306
+ drop_done_experts(st)
307
+
308
+ # ---------------------------------------------------------------- main
309
+ def main():
310
+ os.makedirs(SRC, exist_ok=True)
311
+ for fn in os.listdir(STUB):
312
+ dst = os.path.join(SRC, fn)
313
+ if not os.path.exists(dst):
314
+ shutil.copy(os.path.join(STUB, fn), dst)
315
+ st = load_state()
316
+ current = {"layer": max(st["done_layers"], default=-1) + 1}
317
+ threading.Thread(target=prefetch_loop, args=(lambda: current["layer"],), daemon=True).start()
318
+ log(f"orchestrator start: done={len(st['done_layers'])} layers, free={free_gb():.0f} GB, "
319
+ f"permanent shards={len(PERMANENT)}, window={WINDOW}, layers/run={LAYERS_PER_RUN}")
320
+
321
+ while not image_ready():
322
+ log(f"waiting for docker image {IMAGE} ...")
323
+ time.sleep(60)
324
+
325
+ adopt_running(st)
326
+
327
+ while True:
328
+ st = load_state()
329
+ done = set(st["done_layers"])
330
+ if st.get("finished"):
331
+ log("already finished")
332
+ return
333
+ next_layer = 0
334
+ while next_layer in done:
335
+ next_layer += 1
336
+ if next_layer >= NUM_LAYERS:
337
+ # final run: norm, head, MTP, compile
338
+ ensure_present(PERMANENT)
339
+ rc = run_convert(None, not st["started"], "final")
340
+ if rc != 0:
341
+ log("!! final run failed; stopping (fix and restart)")
342
+ return
343
+ st["finished"] = True
344
+ st["mtp_done"] = True
345
+ save_state(st)
346
+ log(f"FINISHED. output at {OUT}; free={free_gb():.0f} GB")
347
+ return
348
+ # first run: embed + dense layers 0..2 + first MoE layer in one go
349
+ if not st["started"]:
350
+ last = 3
351
+ else:
352
+ last = min(next_layer + LAYERS_PER_RUN - 1, NUM_LAYERS - 1)
353
+ current["layer"] = next_layer
354
+ need = set(PERMANENT)
355
+ for L in range(next_layer, last + 1):
356
+ need |= layer_shards[L]
357
+ ensure_present(need)
358
+ tag = f"L{next_layer:02d}-{last:02d}"
359
+ rc = run_convert(last + 1, not st["started"], tag) # module idx = layer + 1 (embedding is 0)
360
+ if rc != 0:
361
+ log(f"!! run {tag} failed (exit {rc}); retrying once after 60 s")
362
+ time.sleep(60)
363
+ rc = run_convert(last + 1, False, tag + "-retry")
364
+ if rc != 0:
365
+ log("!! run failed twice; stopping. Inspect runs/ logs, then restart orchestrator.")
366
+ return
367
+ st = load_state()
368
+ st["started"] = True
369
+ for L in range(next_layer, last + 1):
370
+ if L not in st["done_layers"]:
371
+ st["done_layers"].append(L)
372
+ save_state(st)
373
+ current["layer"] = last + 1
374
+ drop_done_experts(st)
375
+ log(f"progress: {len(st['done_layers'])}/{NUM_LAYERS} layers, free={free_gb():.0f} GB")
376
+
377
+ if __name__ == "__main__":
378
+ main()
quant/patch_aarch64.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Make exllamav3 1.4.5's C++/CUDA extension build on aarch64 (GB10 / DGX Spark).
3
+
4
+ x86-only pieces (AVX all-reduce, AVX CPU MoE offload for the mul1 codebook, x86 pause
5
+ intrinsics) are wrapped in architecture guards with throwing stubs. None of these paths are used
6
+ for GPU quantization or GPU inference. Idempotent.
7
+ """
8
+ import re, sys, os
9
+
10
+ root = sys.argv[1] if len(sys.argv) > 1 else "exllamav3/exllamav3_ext"
11
+ X86 = "defined(__x86_64__) || defined(__i386__)"
12
+ PAUSE_MACRO = """
13
+ #if defined(__x86_64__) || defined(__i386__)
14
+ #define EXL3_CPU_PAUSE() __builtin_ia32_pause()
15
+ #else
16
+ #define EXL3_CPU_PAUSE() __asm__ __volatile__("yield" ::: "memory")
17
+ #endif
18
+ """
19
+
20
+ def wrap(path, stub_body):
21
+ s = open(path).read()
22
+ if "EXL3_AARCH64_STUB" in s:
23
+ print("already:", path); return
24
+ s = f"#if {X86}\n" + s + f"\n#else /* EXL3_AARCH64_STUB */\n{stub_body}\n#endif\n"
25
+ open(path, "w").write(s); print("wrapped:", path)
26
+
27
+ def decls_from_header(path):
28
+ """Extract non-inline, non-template free-function declarations `ret name(args);`."""
29
+ s = open(path).read()
30
+ s = re.sub(r"//.*", "", s)
31
+ s = re.sub(r"/\*.*?\*/", "", s, flags=re.S)
32
+ out = []
33
+ for m in re.finditer(r"^\s*((?:[A-Za-z_][\w:<>\*&\s]*?)\s+\**\s*([A-Za-z_]\w*)\s*\(([^;{}]*?)\)\s*;)", s, flags=re.M):
34
+ full, name, args = m.group(1), m.group(2), m.group(3)
35
+ head = full[: full.index(name)].strip()
36
+ if head.startswith(("inline", "template", "static", "typedef", "return", "struct", "class")):
37
+ continue
38
+ if not head:
39
+ continue
40
+ out.append((head, name, " ".join(args.split())))
41
+ return out
42
+
43
+ def stubs(header, exclude=()):
44
+ lines = [f'#include "{os.path.basename(header)}"', "#include <stdexcept>"]
45
+ for ret, name, args in decls_from_header(header):
46
+ if name in exclude:
47
+ continue
48
+ body = 'throw std::runtime_error("' + name + ' unavailable on aarch64");'
49
+ lines.append(f"{ret} {name}({args}) {{ {body} }}")
50
+ return "\n".join(lines)
51
+
52
+ # 1. AVX all-reduce implementations
53
+ h512 = os.path.join(root, "parallel/all_reduce_cpu_avx512.h")
54
+ h2 = os.path.join(root, "parallel/all_reduce_cpu_avx2.h")
55
+ wrap(os.path.join(root, "parallel/all_reduce_cpu_avx512.cpp"),
56
+ stubs(h512, exclude=("is_avx512_supported",)))
57
+ wrap(os.path.join(root, "parallel/all_reduce_cpu_avx2.cpp"),
58
+ '#include "all_reduce_cpu_avx512.h"\n' + stubs(h2))
59
+
60
+ # 2. CPU MoE (mul1) offload kernels
61
+ hm = os.path.join(root, "cpu/moe_mul1.h")
62
+ wrap(os.path.join(root, "cpu/moe_mul1.cpp"), stubs(hm))
63
+
64
+ # 3. x86 pause intrinsic in host code of .cu files
65
+ for f in ("parallel/all_reduce_cpu.cu", "cpu/moe_handoff.cu"):
66
+ p = os.path.join(root, f)
67
+ s = open(p).read()
68
+ if "EXL3_CPU_PAUSE" in s:
69
+ print("already:", p); continue
70
+ s = s.replace("__builtin_ia32_pause()", "EXL3_CPU_PAUSE()")
71
+ # insert macro after the last #include at file top
72
+ idx = 0
73
+ for m in re.finditer(r"^#include.*$", s, flags=re.M):
74
+ idx = m.end()
75
+ s = s[:idx] + "\n" + PAUSE_MACRO + s[idx:]
76
+ open(p, "w").write(s); print("pause-patched:", p)
77
+ print("stub decls:", [n for _, n, _ in decls_from_header(hm)])
quant/patch_farm.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Patch exllamav3/conversion/convert_model.py so expert encoding can be farmed to remote GPUs.
3
+
4
+ EXL3_REMOTE_WORKERS="host:port,host:port" adds one work thread per remote worker to the
5
+ per-layer parallel quantization; each thread ships its groups (weights + finalized Hessians)
6
+ via exl3_farm.quantize_remote and rebuilds LinearEXL3 from the returned tensors. Falls back to
7
+ local encoding for a group if the remote call fails. Idempotent.
8
+ """
9
+ import sys, re
10
+
11
+ p = sys.argv[1]
12
+ s = open(p).read()
13
+ if "EXL3_REMOTE_WORKERS" in s:
14
+ print("already patched")
15
+ sys.exit(0)
16
+
17
+ # 1. remote work path inside quantize_linears_parallel
18
+ old_thread = " def work_thread_(device_idx, dev_groups):\n global curr_progress\n\n with torch.inference_mode():\n"
19
+ assert old_thread in s
20
+ new_thread = ''' def work_thread_(device_idx, dev_groups):
21
+ global curr_progress
22
+
23
+ if isinstance(device_idx, str) and device_idx.startswith("remote:"):
24
+ _remote_work_thread(device_idx, dev_groups)
25
+ return
26
+
27
+ with torch.inference_mode():
28
+ '''
29
+ s = s.replace(old_thread, new_thread, 1)
30
+
31
+ remote_fn = '''
32
+ def _remote_work_thread(device_idx, dev_groups):
33
+ global curr_progress
34
+ from exl3_farm import quantize_remote
35
+ from exllamav3.modules.quant.exl3_lib.quantize import finalize_capture_H
36
+ host, port = device_idx[len("remote:"):].rsplit(":", 1)
37
+ port = int(port)
38
+ local_dev = torch.device([d for d in devices if not isinstance(d, str)][0])
39
+ t0 = time.time()
40
+ with torch.inference_mode():
41
+ for group in dev_groups:
42
+ quant_args_list = [make_quant_args(args, idx, strategy[l.key], [0]) for l in group]
43
+ H_datas = [capture_H[l.qmap] if state else l.init_H_data(False) for l in group]
44
+ for hd, qa in zip(H_datas, quant_args_list):
45
+ torch.manual_seed(qa["seed"])
46
+ finalize_capture_H(hd, qa, False)
47
+ weights = [l.inner.get_weight_tensor() for l in group]
48
+ biases = [l.inner.get_bias_tensor() for l in group]
49
+ try:
50
+ tq = time.time()
51
+ results, quant_args_list = quantize_remote(host, port, weights, H_datas, quant_args_list)
52
+ tstr = f" [remote {host} {(time.time() - tq) / len(group):4.2f} s]"
53
+ except Exception as e:
54
+ print(f" !! remote {host}:{port} failed ({e!r}); encoding group locally", flush = True)
55
+ for l in group:
56
+ l.inner.swap_cpu()
57
+ if len(group) > 1:
58
+ quant_args_list = [make_quant_args(args, idx, strategy[l.key], [local_dev.index or 0]) for l in group]
59
+ proxy_errs = convert_exl3_group(group, H_datas, quant_args_list)
60
+ for l, qa, pe in zip(group, quant_args_list, proxy_errs):
61
+ l.inner.swap_cpu()
62
+ print_quantized_linear(config, l, qa, pe, " [local fallback]")
63
+ with progress_lock:
64
+ curr_progress += 1
65
+ else:
66
+ l = group[0]
67
+ qa = make_quant_args(args, idx, strategy[l.key], [local_dev.index or 0])
68
+ pe = l.convert_exl3(H_datas[0], quant_args = qa, verbose = False, save_reg = False,
69
+ override_swap_device = local_dev)
70
+ l.inner.swap_cpu()
71
+ print_quantized_linear(config, l, qa, pe, " [local fallback]")
72
+ with progress_lock:
73
+ curr_progress += 1
74
+ continue
75
+ for l, bias, qa, (pe, out) in zip(group, biases, quant_args_list, results):
76
+ out = {k: v.to(local_dev) for k, v in out.items()}
77
+ l.inner = LinearEXL3(
78
+ l.config, l.in_features, l.out_features,
79
+ out.get("scale"), out.get("su"), out.get("sv"), out.get("suh"), out.get("svh"),
80
+ out.get("trellis"), out.get("mcg"), out.get("mul1"),
81
+ bias, l.out_dtype, key = l.key
82
+ )
83
+ l.inner.swap_cpu()
84
+ print_quantized_linear(config, l, qa, pe, tstr)
85
+ with progress_lock:
86
+ curr_progress += 1
87
+ print(f" -- remote {host}: {sum(len(g) for g in dev_groups)} tensors in {time.time() - t0:.0f} s", flush = True)
88
+
89
+ '''
90
+ anchor = " # Launch\n threads = []\n"
91
+ assert anchor in s
92
+ s = s.replace(anchor, remote_fn + anchor, 1)
93
+
94
+ # 2. dispatch: farm the <=8-bit linears, keep 16-bit ones on the single path
95
+ old_dispatch = ''' if (
96
+ len(linears) >= len(devices) and
97
+ all(b <= 8 for _, b in strategy.items())
98
+ ):
99
+ quantize_linears_parallel(args, linears, config, strategy, idx, devices, eff_ratios("quant_thread"), capture_H, state)
100
+ else:
101
+ quantize_linears_single(args, linears, config, strategy, idx, devices, eff_ratios("quant_tiles"), capture_H, state)
102
+ '''
103
+ assert old_dispatch in s
104
+ new_dispatch = ''' _remotes = [f"remote:{w.strip()}" for w in os.environ.get("EXL3_REMOTE_WORKERS", "").split(",") if w.strip()]
105
+ _q_linears = [l for l in linears if strategy[l.key] <= 8]
106
+ _u_linears = [l for l in linears if strategy[l.key] > 8]
107
+ if _remotes and len(_q_linears) >= 2 * (len(devices) + len(_remotes)):
108
+ if _u_linears:
109
+ quantize_linears_single(args, _u_linears, config, strategy, idx, devices, eff_ratios("quant_tiles"), capture_H, state)
110
+ print(f" -- Farming {len(_q_linears)} tensors over {devices + _remotes}", flush = True)
111
+ quantize_linears_parallel(args, _q_linears, config, strategy, idx, devices + _remotes, None, capture_H, state)
112
+ elif (
113
+ len(linears) >= len(devices) and
114
+ all(b <= 8 for _, b in strategy.items())
115
+ ):
116
+ quantize_linears_parallel(args, linears, config, strategy, idx, devices, eff_ratios("quant_thread"), capture_H, state)
117
+ else:
118
+ quantize_linears_single(args, linears, config, strategy, idx, devices, eff_ratios("quant_tiles"), capture_H, state)
119
+ '''
120
+ s = s.replace(old_dispatch, new_dispatch, 1)
121
+ open(p, "w").write(s)
122
+ print("patched", p)
quant/recipe.yaml ADDED
The diff for this file is too large to render. See raw diff
 
quant/start-orch.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ #!/bin/bash
2
+ cd ~/glm53-full-exl3 && nohup python3 orchestrate.py >> orchestrate.out 2>&1 &
serving/Dockerfile.dcp ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # keys GLM-5.3 FULL EXL3 (3-bit mcg routed experts) serving image = Mia base + vertical integration:
2
+ # * keys-DCP port of the SM120 sparse-MLA backend (KV sharded across TP ranks -> 1M ctx on 4 GB10)
3
+ # * MLA layer DCP combine fix for sparse all-MQA backends (no MHA prefill path)
4
+ # * ABLIT load-time o_proj transplant hook for the DeepseekV2 model classes (derisk donors from
5
+ # make_derisk_donor.py mounted at /opt/glm53/ablit/transplant; ABLIT=1 to enable)
6
+ # Build on any GB10 (context = this directory): docker build -f Dockerfile.dcp -t glm53-exl3-tp4:dcp .
7
+ ARG BASE=ghcr.io/miaai-lab/glm-5.3-flash-2x-dgx-sparks:exl3
8
+ FROM ${BASE}
9
+ ARG VLLM=/usr/local/lib/python3.12/dist-packages/vllm
10
+ COPY sm120_dcp.py ${VLLM}/v1/attention/backends/mla/flashinfer_mla_sparse_sm120.py
11
+ COPY mla_attn_dcp.py ${VLLM}/model_executor/layers/attention/mla_attention.py
12
+ COPY patch_ablit_dsv2.py /opt/glm53/patch_ablit_dsv2.py
13
+ COPY ablit_runtime.py /opt/glm53/ablit_runtime.py
14
+ RUN python3 /opt/glm53/patch_ablit_dsv2.py /opt/glm53/ablit_runtime.py && python3 - <<'EOF'
15
+ from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm120 import FlashInferMLASparseSM120Impl as I
16
+ assert I.can_return_lse_for_decode is True
17
+ import vllm.model_executor.layers.attention.mla_attention as m
18
+ assert "keys-DCP" in open(m.__file__).read()
19
+ print("keys image OK: DCP backend + layer fix + ablit hook; lse_base_on_e =", I.lse_base_on_e)
20
+ EOF
21
+ LABEL keys.glm53.full.exl3="DCP sm120 + mla combine fix + ablit dsv2 hook, 2026-09-01"
serving/Dockerfile.keys ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ FROM glm53-exl3-tp4:dcp-mul1-v145
2
+ RUN f=/usr/local/lib/python3.12/dist-packages/vllm/model_executor/layers/sparse_attn_indexer.py && sed -i "s/use_persistent_topk = current_platform.is_cuda() and topk_tokens in (/use_persistent_topk = False and current_platform.is_cuda() and topk_tokens in ( # keys: GB10 48-SM oversubscribe/" $f && grep -c "keys: GB10 48-SM" $f && rm -rf /usr/local/lib/python3.12/dist-packages/vllm/model_executor/layers/__pycache__/sparse_attn_indexer*
serving/Dockerfile.keys3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ FROM glm53-exl3-tp4:keys
2
+ COPY exl3.py /usr/local/lib/python3.12/dist-packages/vllm/model_executor/layers/quantization/exl3.py
3
+ RUN rm -rf /usr/local/lib/python3.12/dist-packages/vllm/model_executor/layers/quantization/__pycache__
serving/Dockerfile.mul1 ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ FROM glm53-exl3-tp4:dcp
2
+ COPY exl3.py /usr/local/lib/python3.12/dist-packages/vllm/model_executor/layers/quantization/exl3.py
serving/Dockerfile.v145 ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM glm53-exl3-tp4:dcp-mul1
2
+ ARG DP=/usr/local/lib/python3.12/dist-packages
3
+ RUN rm -rf $DP/exllamav3 $DP/exllamav3-0.0.43.dist-info $DP/exllamav3_ext*.so
4
+ COPY --from=glm53-exl3-quant:1.4.5 $DP/exllamav3 $DP/exllamav3
5
+ COPY --from=glm53-exl3-quant:1.4.5 $DP/exllamav3-1.4.5.dist-info $DP/exllamav3-1.4.5.dist-info
6
+ COPY --from=glm53-exl3-quant:1.4.5 $DP/exllamav3_ext.cpython-312-aarch64-linux-gnu.so $DP/exllamav3_ext.cpython-312-aarch64-linux-gnu.so
serving/ablit_runtime.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: MIT
2
+ """GLM-5.3-Flash o_proj abliteration (ABLIT) — load-time refusal-direction edit.
3
+
4
+ Applies the classic weight orthogonalization to every ``self_attn.o_proj`` in
5
+ the configured layer range:
6
+
7
+ W' = (I - alpha * r r^T / ||r||^2) W
8
+
9
+ ``r`` is the refusal direction in the residual/output space of o_proj
10
+ (hidden_size=4096). Components of the attention output orthogonal to ``r``
11
+ are preserved exactly; the component along ``r`` is scaled by ``1 - alpha``
12
+ (alpha > 1 over-projects, which is what the published recipe uses).
13
+
14
+ METHOD (ABLIT_METHOD=auto | transplant | proj):
15
+ transplant — replace the stock o_proj weights L15-45 (incl. the checkpoint
16
+ MTP block's, layer 45) with the donor tensors fetched by
17
+ ablit/fetch_transplant.py into ablit/transplant/ (raw bf16 + MANIFEST.json).
18
+ This is the published "dealign-oproj-transplant" edit — a byte-copy from
19
+ dealignai/GLM-5.3-Flash-UNCENSORED-NVFP4 into the stock body. Direction
20
+ orthogonalization does NOT work on this model (the published directions
21
+ are statistically random vs stock o_proj — measured), so transplant is
22
+ the only effective mode; "auto" prefers it when the tensors are present.
23
+ proj — the classic orthogonalization below. Kept for custom directions.
24
+
25
+ TP notes: o_proj is a RowParallelLinear — vLLM shards the *input* dim, the
26
+ 4096 output rows are replicated on every rank. The edit only touches rows,
27
+ so each rank applies the identical formula to its own shard and the
28
+ post-allreduce result stays consistent. No collective is needed.
29
+
30
+ The edit runs at the end of ``Glm5NextModel.load_weights`` / ``Glm5NextMTP.
31
+ load_weights`` (installed by overlay/patch_ablit.py). o_proj stays native
32
+ BF16 in this serve (attn is unquantized), so the parameter is final at that
33
+ point — quantized-expert post-processing never touches it, and CUDA graph
34
+ capture happens later.
35
+
36
+ Artifacts (shipped in ``ablit/`` and mounted at /opt/glm53/ablit):
37
+ LAYER_MAP.json layer/shard map + published recipe
38
+ refusal_direction_glm53_dealign_late.pt published dealign direction
39
+ refusal_direction_glm53_bf_oproj.pt blackfrost direction (alpha_ref 3.0)
40
+
41
+ Source: drowzeys/keys-GLM-5.3-Flash-NVFP4-ablit-l15-45-anchorstock
42
+ (published method "dealign-oproj-transplant": layers 15-45 edited, 0-14 kept
43
+ as stock safety anchors, MTP block included).
44
+
45
+ Env knobs (see .env.example):
46
+ ABLIT=1 enable (default off — hook is a no-op)
47
+ ABLIT_DIR artifact dir (default /opt/glm53/ablit)
48
+ ABLIT_METHOD auto | transplant | proj (default auto: transplant
49
+ when ablit/transplant/ is populated, else proj)
50
+ ABLIT_DIRECTION dealign | bf_oproj | /path/to/dir.pt (default dealign)
51
+ ABLIT_LAYERS inclusive ranges, e.g. "15-45" or "15,17-19"
52
+ ABLIT_ALPHA projection scale (default 3.0)
53
+ ABLIT_INCLUDE_MTP also edit the checkpoint MTP block when it exists
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import json
59
+ import math
60
+ import os
61
+ import re
62
+ from pathlib import Path
63
+ from typing import Any
64
+
65
+ import torch
66
+
67
+ def _tp_world() -> int:
68
+ try:
69
+ from vllm.distributed import get_tensor_model_parallel_world_size
70
+ return get_tensor_model_parallel_world_size()
71
+ except Exception: # standalone / tests
72
+ return 1
73
+
74
+
75
+ def _tp_rank() -> int:
76
+ try:
77
+ from vllm.distributed import get_tensor_model_parallel_rank
78
+ return get_tensor_model_parallel_rank()
79
+ except Exception: # standalone / tests
80
+ return 0
81
+
82
+
83
+ try: # inside the vLLM image
84
+ from vllm.logger import init_logger
85
+
86
+ logger = init_logger(__name__)
87
+ except Exception: # standalone (tests)
88
+ import logging
89
+
90
+ logger = logging.getLogger("glm53_ablit")
91
+
92
+ DEFAULT_ABLIT_DIR = "/opt/glm53/ablit"
93
+ TRANSPLANT_SUBDIR = "transplant"
94
+
95
+ DIRECTION_FILES = {
96
+ "dealign": "refusal_direction_glm53_dealign_late.pt",
97
+ "bf_oproj": "refusal_direction_glm53_bf_oproj.pt",
98
+ }
99
+
100
+ # paths inside the hooked model that carry an o_proj to edit
101
+ _LAYER_O_PROJ_RE = re.compile(r"(?:^|\.)layers\.(\d+)\.self_attn\.o_proj$")
102
+ _MTP_O_PROJ_RE = re.compile(r"(?:^|\.)layers\.(\d+)\.mtp_block\.self_attn\.o_proj$")
103
+
104
+
105
+ class AblitError(RuntimeError):
106
+ """ABLIT was explicitly enabled but the edit cannot be applied."""
107
+
108
+
109
+ def env_flag(name: str, default: bool = False) -> bool:
110
+ val = os.environ.get(name)
111
+ if val is None or val == "":
112
+ return default
113
+ return val.strip().lower() in ("1", "true", "yes", "on")
114
+
115
+
116
+ def parse_layers(spec: str) -> list[int]:
117
+ """Parse "15-45" / "15,17-19" into a sorted list of layer indices."""
118
+ layers: set[int] = set()
119
+ for part in spec.split(","):
120
+ part = part.strip()
121
+ if not part:
122
+ continue
123
+ if "-" in part:
124
+ lo_s, hi_s = part.split("-", 1)
125
+ try:
126
+ lo, hi = int(lo_s), int(hi_s)
127
+ except ValueError as exc:
128
+ raise AblitError(f"ABLIT_LAYERS: bad range {part!r}") from exc
129
+ if hi < lo:
130
+ raise AblitError(f"ABLIT_LAYERS: inverted range {part!r}")
131
+ layers.update(range(lo, hi + 1))
132
+ else:
133
+ try:
134
+ layers.add(int(part))
135
+ except ValueError as exc:
136
+ raise AblitError(f"ABLIT_LAYERS: bad index {part!r}") from exc
137
+ if not layers:
138
+ raise AblitError("ABLIT_LAYERS resolved to an empty set")
139
+ return sorted(layers)
140
+
141
+
142
+ def resolve_direction_path(ablit_dir: str, direction: str) -> Path:
143
+ if "/" in direction: # explicit path to a .pt
144
+ return Path(direction)
145
+ fname = DIRECTION_FILES.get(direction)
146
+ if fname is None:
147
+ raise AblitError(
148
+ f"ABLIT_DIRECTION={direction!r} not one of "
149
+ f"{sorted(DIRECTION_FILES)} (or a .pt path)"
150
+ )
151
+ return Path(ablit_dir) / fname
152
+
153
+
154
+ def load_direction(path: Path) -> torch.Tensor:
155
+ try:
156
+ obj = torch.load(str(path), map_location="cpu", weights_only=True)
157
+ except Exception as exc:
158
+ raise AblitError(f"cannot load ablit direction {path}: {exc}") from exc
159
+ if not isinstance(obj, dict) or "directions" not in obj:
160
+ raise AblitError(f"{path}: expected a dict with 'directions'")
161
+ r = obj["directions"]
162
+ if not torch.is_tensor(r):
163
+ raise AblitError(f"{path}: 'directions' must be a tensor, got {type(r)}")
164
+ if r.dim() == 2 and r.shape[0] == 1: # some exports store a [1, N] row
165
+ r = r.squeeze(0)
166
+ if r.dim() != 1:
167
+ raise AblitError(f"{path}: 'directions' must be 1-D (or [1, N]), got {tuple(r.shape)}")
168
+ r = r.to(torch.float32)
169
+ norm = r.norm()
170
+ if not torch.isfinite(norm) or float(norm) <= 0:
171
+ raise AblitError(f"{path}: direction has non-positive/invalid norm {norm}")
172
+ return r
173
+
174
+
175
+ def apply_to_o_proj(mod: Any, r: torch.Tensor, alpha: float) -> dict[str, Any]:
176
+ """Orthogonalize one o_proj module in place. Returns a report dict."""
177
+ weight = getattr(mod, "weight", None)
178
+ if weight is None or not torch.is_tensor(weight) or weight.dim() != 2:
179
+ return {"edited": False, "reason": "no 2-D .weight"}
180
+ if weight.shape[0] != r.numel():
181
+ return {
182
+ "edited": False,
183
+ "reason": f"out_features={weight.shape[0]} != direction dim {r.numel()}",
184
+ }
185
+ r_dev = r.to(device=weight.device, dtype=torch.float32)
186
+ with torch.no_grad():
187
+ w32 = weight.data.to(torch.float32)
188
+ # W' = W - alpha * outer(r, r @ W) / ||r||^2
189
+ rw = r_dev @ w32 # [in_local]
190
+ delta = alpha * torch.outer(r_dev, rw) / float(r_dev @ r_dev)
191
+ w32.sub_(delta)
192
+ weight.data.copy_(w32.to(weight.dtype))
193
+ residual = float((r_dev @ weight.data.to(torch.float32)).abs().max())
194
+ return {
195
+ "edited": True,
196
+ "shape": tuple(weight.shape),
197
+ "residual_max": residual,
198
+ }
199
+
200
+
201
+ def walk_o_proj(model: Any) -> list[tuple[str, int | None, Any]]:
202
+ """Collect (name, layer_idx or None-if-pure-mtp, module) o_proj candidates."""
203
+ found: list[tuple[str, int | None, Any]] = []
204
+ seen: set[int] = set()
205
+ for name, mod in model.named_modules():
206
+ if id(mod) in seen:
207
+ continue
208
+ m = _LAYER_O_PROJ_RE.search(name)
209
+ if m is not None:
210
+ seen.add(id(mod))
211
+ found.append((name, int(m.group(1)), mod))
212
+ continue
213
+ m = _MTP_O_PROJ_RE.search(name)
214
+ if m is not None:
215
+ seen.add(id(mod))
216
+ found.append((name, int(m.group(1)), mod))
217
+ return found
218
+
219
+
220
+ def unwrap_text_model(model: Any) -> Any:
221
+ """Accept the multimodal wrapper and hand back the text model."""
222
+ lm = getattr(model, "language_model", None)
223
+ if lm is not None:
224
+ inner = getattr(lm, "model", None)
225
+ if inner is not None:
226
+ return inner
227
+ return lm
228
+ return model
229
+
230
+
231
+ def apply_ablit(
232
+ model: Any,
233
+ r: torch.Tensor,
234
+ layers: list[int],
235
+ alpha: float,
236
+ include_mtp: bool,
237
+ ) -> dict[str, Any]:
238
+ """Edit every configured o_proj under ``model``. Fails hard on surprises."""
239
+ text_model = unwrap_text_model(model)
240
+ candidates = walk_o_proj(text_model)
241
+ want = set(layers)
242
+ report: dict[str, Any] = {
243
+ "edited_layers": [],
244
+ "skipped": [],
245
+ "mtp_edited": False,
246
+ }
247
+ seen_ids: set[int] = set()
248
+ for name, idx, mod in candidates:
249
+ if id(mod) in seen_ids:
250
+ continue
251
+ seen_ids.add(id(mod))
252
+ is_mtp = _MTP_O_PROJ_RE.search(name) is not None
253
+ if is_mtp:
254
+ if not include_mtp:
255
+ report["skipped"].append({"name": name, "reason": "include_mtp=0"})
256
+ continue
257
+ if idx not in want:
258
+ report["skipped"].append(
259
+ {"name": name, "reason": f"layer {idx} not in ABLIT_LAYERS"}
260
+ )
261
+ continue
262
+ else:
263
+ if idx not in want:
264
+ continue
265
+ rep = apply_to_o_proj(mod, r, alpha)
266
+ if rep.get("edited"):
267
+ if is_mtp:
268
+ report["mtp_edited"] = True
269
+ else:
270
+ report["edited_layers"].append(idx)
271
+ logger.info(
272
+ "ablit: orthogonalized %s shape=%s residual_max=%.3e",
273
+ name,
274
+ rep.get("shape"),
275
+ rep.get("residual_max", float("nan")),
276
+ )
277
+ else:
278
+ report["skipped"].append({"name": name, "reason": rep.get("reason")})
279
+ logger.warning("ablit: skipped %s: %s", name, rep.get("reason"))
280
+ report["edited_layers"].sort()
281
+ return report
282
+
283
+
284
+ def load_transplant_tensors(
285
+ ablit_dir: str, layers: list[int]
286
+ ) -> dict[int, torch.Tensor]:
287
+ """Load donor o_proj tensors (full, unsharded) from ablit/transplant/."""
288
+ tdir = Path(ablit_dir) / TRANSPLANT_SUBDIR
289
+ manifest_path = tdir / "MANIFEST.json"
290
+ if not manifest_path.is_file():
291
+ raise AblitError(f"ABLIT_METHOD=transplant but {manifest_path} is missing "
292
+ "— run ablit/fetch_transplant.py first")
293
+ manifest = json.loads(manifest_path.read_text())
294
+ meta = {int(k): v for k, v in (manifest.get("layers") or manifest.get("tensors") or {}).items()}
295
+ out: dict[int, torch.Tensor] = {}
296
+ for L in layers:
297
+ if L not in meta:
298
+ continue
299
+ info = meta[L]
300
+ path = tdir / f"L{L}.bin"
301
+ if not path.is_file():
302
+ raise AblitError(f"transplant manifest lists L{L} but {path} is missing")
303
+ raw = path.read_bytes()
304
+ if len(raw) != int(info["nbytes"]):
305
+ raise AblitError(
306
+ f"transplant L{L}: expected {info['nbytes']} bytes, got {len(raw)} "
307
+ "— re-run ablit/fetch_transplant.py")
308
+ if info["dtype"] != "BF16":
309
+ raise AblitError(f"transplant L{L}: unsupported dtype {info['dtype']}")
310
+ t = torch.frombuffer(bytearray(raw), dtype=torch.bfloat16).reshape(
311
+ tuple(info["shape"]))
312
+ out[L] = t
313
+ if not out:
314
+ raise AblitError(
315
+ f"no transplant tensors under {tdir} cover ABLIT_LAYERS — "
316
+ "run ablit/fetch_transplant.py first")
317
+ return out
318
+
319
+
320
+ def apply_transplant(
321
+ model: Any,
322
+ donors: dict[int, torch.Tensor],
323
+ layers: list[int],
324
+ include_mtp: bool,
325
+ ) -> dict[str, Any]:
326
+ """Replace stock o_proj weights with the donor tensors (per-TP shard)."""
327
+ text_model = unwrap_text_model(model)
328
+ candidates = walk_o_proj(text_model)
329
+ want = set(layers)
330
+ world, rank = _tp_world(), _tp_rank()
331
+ report: dict[str, Any] = {"edited_layers": [], "skipped": [], "mtp_edited": False,
332
+ "deltas": {}}
333
+ seen_ids: set[int] = set()
334
+ for name, idx, mod in candidates:
335
+ if id(mod) in seen_ids:
336
+ continue
337
+ seen_ids.add(id(mod))
338
+ is_mtp = _MTP_O_PROJ_RE.search(name) is not None
339
+ if is_mtp and not include_mtp:
340
+ report["skipped"].append({"name": name, "reason": "include_mtp=0"})
341
+ continue
342
+ if idx not in want:
343
+ continue
344
+ if idx not in donors:
345
+ raise AblitError(
346
+ f"ABLIT_METHOD=transplant has no donor tensor for layer {idx} "
347
+ "— fetch it with ablit/fetch_transplant.py")
348
+ donor = donors[idx]
349
+ weight = getattr(mod, "weight", None)
350
+ if weight is None or not torch.is_tensor(weight) or weight.dim() != 2:
351
+ raise AblitError(f"ablit transplant: {name} has no 2-D .weight")
352
+ full_in = donor.shape[1]
353
+ local_in = weight.shape[1]
354
+ if donor.shape[0] != weight.shape[0] or full_in != local_in * world:
355
+ raise AblitError(
356
+ f"ablit transplant: {name} shape {tuple(weight.shape)} does not "
357
+ f"match donor {tuple(donor.shape)} at TP={world}")
358
+ shard = donor if world == 1 else donor[:, rank * local_in:(rank + 1) * local_in]
359
+ # Donors come from torch.frombuffer on CPU; o_proj is already on the
360
+ # worker GPU. Compare and copy on weight's device (proj path already
361
+ # does r.to(weight.device); transplant forgot).
362
+ shard = shard.to(device=weight.device, dtype=weight.dtype)
363
+ with torch.no_grad():
364
+ w32 = weight.data.float()
365
+ d32 = shard.float()
366
+ rel_l2 = float((d32 - w32).norm() / w32.norm().clamp_min(1e-9))
367
+ mean_rel = float((d32 - w32).abs().mean() / w32.abs().mean().clamp_min(1e-9))
368
+ weight.data.copy_(shard)
369
+ report["edited_layers"].append(idx)
370
+ report["deltas"][idx] = {"rel_l2": rel_l2, "mean_rel": mean_rel}
371
+ if is_mtp:
372
+ report["mtp_edited"] = True
373
+ logger.info(
374
+ "ablit: transplanted %s (donor L%d) shape=%s rel_l2=%.4f mean_rel=%.4f",
375
+ name, idx, tuple(weight.shape), rel_l2, mean_rel)
376
+ report["edited_layers"].sort()
377
+ return report
378
+
379
+
380
+ def maybe_apply(model: Any) -> dict[str, Any] | None:
381
+ """Hook entrypoint. No-op unless ABLIT=1; raises AblitError if enabled
382
+ but the recipe cannot be honored (fail loud beats silent stock weights)."""
383
+ if not env_flag("ABLIT", False):
384
+ return None
385
+
386
+ ablit_dir = os.environ.get("ABLIT_DIR") or DEFAULT_ABLIT_DIR
387
+ layers_spec = os.environ.get("ABLIT_LAYERS") or "15-45"
388
+ include_mtp = env_flag("ABLIT_INCLUDE_MTP", True)
389
+ method = (os.environ.get("ABLIT_METHOD") or "auto").strip().lower()
390
+ layers = parse_layers(layers_spec)
391
+
392
+ if method in ("auto", "transplant"):
393
+ tdir = Path(ablit_dir) / TRANSPLANT_SUBDIR
394
+ have_transplant = (tdir / "MANIFEST.json").is_file() and (
395
+ method == "transplant"
396
+ or any((tdir / f"L{L}.bin").is_file() for L in layers)
397
+ )
398
+ if have_transplant:
399
+ if method == "auto":
400
+ logger.info("ablit: ABLIT_METHOD=auto -> transplant "
401
+ "(ablit/transplant/ present; direction proj does "
402
+ "not align with stock o_proj on this model)")
403
+ donors = load_transplant_tensors(ablit_dir, layers)
404
+ report = apply_transplant(model, donors, layers, include_mtp)
405
+ if not report["edited_layers"] and not report["mtp_edited"]:
406
+ raise AblitError(
407
+ "ABLIT=1 transplant matched no o_proj — ABLIT_LAYERS="
408
+ f"{layers_spec} matched nothing under this model")
409
+ deltas = list(report["deltas"].values())
410
+ logger.info(
411
+ "ablit: ON method=transplant layers=%s edited=%s mtp=%s "
412
+ "mean rel_l2=%.4f (skipped=%d) — early safety-anchor layers "
413
+ "stay stock",
414
+ layers_spec,
415
+ report["edited_layers"],
416
+ report["mtp_edited"],
417
+ sum(d["rel_l2"] for d in deltas) / max(len(deltas), 1),
418
+ len(report["skipped"]),
419
+ )
420
+ return report
421
+ if method == "transplant":
422
+ raise AblitError(
423
+ "ABLIT_METHOD=transplant but ablit/transplant/ is missing or "
424
+ "empty — run: python3 ablit/fetch_transplant.py")
425
+ logger.info("ablit: ABLIT_METHOD=auto -> proj (no ablit/transplant/ "
426
+ "tensors found — falling back to direction orthogonalization)")
427
+
428
+ # ABLIT_METHOD=proj (or auto fallback): direction orthogonalization
429
+ direction = os.environ.get("ABLIT_DIRECTION") or "dealign"
430
+ try:
431
+ alpha = float(os.environ.get("ABLIT_ALPHA") or "3.0")
432
+ except ValueError as exc:
433
+ raise AblitError(f"ABLIT_ALPHA is not a number: {exc}") from exc
434
+ if not math.isfinite(alpha) or alpha <= 0:
435
+ raise AblitError(f"ABLIT_ALPHA must be a positive finite number, got {alpha}")
436
+
437
+ path = resolve_direction_path(ablit_dir, direction)
438
+ if not path.is_file():
439
+ raise AblitError(f"ABLIT=1 but direction file missing: {path}")
440
+ r = load_direction(path)
441
+
442
+ report = apply_ablit(model, r, layers, alpha, include_mtp)
443
+
444
+ if not report["edited_layers"] and not report["mtp_edited"]:
445
+ raise AblitError(
446
+ "ABLIT=1 but no o_proj was edited — ABLIT_LAYERS="
447
+ f"{layers_spec} matched nothing under this model"
448
+ )
449
+ logger.info(
450
+ "ablit: ON method=proj direction=%s (%s) alpha=%s layers=%s edited=%s "
451
+ "mtp=%s (skipped=%d) — early safety-anchor layers stay stock",
452
+ direction,
453
+ path.name,
454
+ alpha,
455
+ layers_spec,
456
+ report["edited_layers"],
457
+ report["mtp_edited"],
458
+ len(report["skipped"]),
459
+ )
460
+ return report
serving/donor_diff.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, torch, os, sys
2
+ from safetensors import safe_open
3
+ M="/models/GLM-5.3-EXL3-3bpw"; D="/models/glm53-ablit-donor-derisked"
4
+ idx=json.load(open(f"{M}/model.safetensors.index.json"))["weight_map"]
5
+ man=json.load(open(f"{D}/MANIFEST.json"))["layers"]
6
+ print("layer rel_l2 cos donor_bits")
7
+ for L in range(79):
8
+ key=f"model.layers.{L}.self_attn.o_proj.weight"
9
+ if key not in idx: print(L,"no-stock"); continue
10
+ with safe_open(f"{M}/{idx[key]}","pt") as f: w=f.get_tensor(key).float()
11
+ info=man[str(L)]; raw=open(f"{D}/L{L}.bin","rb").read()
12
+ d=torch.frombuffer(bytearray(raw),dtype=torch.bfloat16).view(info["shape"]).float()
13
+ rel=((d-w).norm()/w.norm()).item(); cos=torch.nn.functional.cosine_similarity(d.flatten(),w.flatten(),dim=0).item()
14
+ print(f"{L:2d} {rel:.4f} {cos:.5f} {info.get('bits')}", flush=True)
serving/env.tp4.example ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GLM-5.3 FULL EXL3 3bpw mcg — 4x DGX Spark TP=4 (+DCP=4 for 1M) — DRAFT for ~/glm53-mia/.env.tp4 on .1
2
+ # ranks: .1 head, .2 / .3 / .5 workers (all on the 10.100.10.x CX7 fabric, same NIC names)
3
+ HEAD_IP=10.100.10.1
4
+ WORKER_IP=10.100.10.2
5
+ WORKER2_IP=10.100.10.3
6
+ WORKER3_IP=10.100.10.5
7
+ WORKER_USER=keyspark
8
+ NCCL_CROSS_NIC=0
9
+ HEAD_CX7_IF=enp1s0f1np1
10
+ HEAD_CX7_IB=rocep1s0f1
11
+ WORKER_CX7_IF=enP2p1s0f1np1
12
+ WORKER_CX7_IB=roceP2p1s0f1
13
+ WORKER2_CX7_IF=enp1s0f1np1
14
+ WORKER2_CX7_IB=rocep1s0f1
15
+ WORKER3_CX7_IF=enp1s0f1np1
16
+ WORKER3_CX7_IB=rocep1s0f1
17
+ NCCL_IB_GID_INDEX=3
18
+ NCCL_DEBUG=WARN
19
+
20
+ TP=4
21
+ NNODES=4
22
+ PORT=8888
23
+ MASTER_PORT=29521
24
+ CONTAINER_HEAD=glm53-exl3-tp4-head
25
+ CONTAINER_WORKER=glm53-exl3-tp4-w1
26
+ CONTAINER_WORKER2=glm53-exl3-tp4-w2
27
+ CONTAINER_WORKER3=glm53-exl3-tp4-w3
28
+
29
+ # model: quantized on .3 -> exported over NFS as /mnt/models-node2/<name> on every node
30
+ LOCAL_MODEL_DIR=/mnt/models-node2/GLM-5.3-EXL3-3bpw
31
+ MODEL=keys/GLM-5.3-EXL3-3bpw
32
+ SERVED_MODEL_NAME=GLM-5.3-EXL3
33
+ IMAGE=glm53-exl3-tp4:keys3
34
+ QUANTIZATION=exl3
35
+ KV_CACHE_DTYPE=fp8
36
+ MAX_MODEL_LEN=1000000
37
+ GPU_MEM_UTIL=0.82
38
+ MAX_NUM_SEQS=8
39
+ MAX_NUM_BATCHED_TOKENS=4096
40
+ SPEC_METHOD=mtp
41
+ LANGUAGE_MODEL_ONLY=1
42
+ ENFORCE_EAGER=0
43
+ EXTRA_ARGS="--decode-context-parallel-size 4 --kv-cache-memory-bytes 14092861440"
44
+ LOCAL_DFLASH_DIR=/mnt/models-node2/GLM-5.3-DFlash2
45
+ DFLASH_TOKENS=7
46
+ GLM53_MIXED_PREFILL_CHUNK=2048
47
+ MTP_TOKENS=3
serving/exl3.py ADDED
@@ -0,0 +1,862 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """EXL3/MCG trellis quantization for GLM-5.3-Flash routed experts.
3
+
4
+ Checkpoint ABI (brandonmusic/GLM-5.3-Flash-tr3-4bpw):
5
+ quant_method=exl3, codebook=mcg, scope=glm53_routed_experts_only
6
+ per expert matrix: trellis (int16) + suh/svh (fp16) + mcg (int32 marker)
7
+
8
+ Non-routed tensors stay native (UnquantizedLinearMethod). Experts never
9
+ expand to a persistent BF16 weight; LinearEXL3 / exllamav3_ext runs the
10
+ trellis GEMM. TP=2 shards gate/up column-wise and down row-wise; the MoE
11
+ runner all-reduces the combined output.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import importlib
17
+ import importlib.util
18
+ import os
19
+ import sys
20
+ import types
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ import torch
25
+ import torch.nn.functional as F
26
+ from torch.nn.parameter import Parameter
27
+
28
+ from vllm.logger import init_logger
29
+ from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
30
+ from vllm.model_executor.layers.fused_moe.fused_moe_method_base import (
31
+ FusedMoEMethodBase,
32
+ )
33
+ from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
34
+ from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
35
+ from vllm.model_executor.layers.quantization import register_quantization_config
36
+ from vllm.model_executor.utils import set_weight_attrs
37
+
38
+ if TYPE_CHECKING:
39
+ from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
40
+ from vllm.model_executor.layers.fused_moe.runner.shared_experts import (
41
+ SharedExperts,
42
+ )
43
+
44
+ logger = init_logger(__name__)
45
+
46
+ EXLLAMAV3_COMMIT = "c5d9c657966ffeeaa9353f0cc899f18629da4a13"
47
+ EXLLAMAV3_VERSION = "0.0.43"
48
+ MCG_MULTIPLIER = 0xCBAC1FED
49
+ MCG_MARKER_SIGNED_INT32 = -877912083
50
+ # KEYS PATCH: exllamav3 "mul1" codebook (converter default; 0x83DCD12D marker). LinearEXL3 and
51
+ # exllamav3_ext.exl3_moe in this image already take (mcg, mul1) flag pairs per matrix.
52
+ MUL1_MULTIPLIER = 0x83DCD12D
53
+ MUL1_MARKER_SIGNED_INT32 = -2082680531
54
+ EXL3_SUFFIXES = ("trellis", "suh", "svh", "mcg", "mul1")
55
+ SWIGLU_LIMIT_DEFAULT = 10.0
56
+ TEMP_ROWS_FUSED = 128
57
+ MOE_ACT_SILU = 0
58
+ # Shared fused scratch: decode is sequential across layers.
59
+ _FUSED_TEMP_CACHE: dict[tuple, tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]] = {}
60
+
61
+
62
+ def _narrow_tp(tensor: torch.Tensor, dim: int, tp_rank: int, tp_size: int) -> torch.Tensor:
63
+ if tp_size <= 1:
64
+ return tensor
65
+ size = int(tensor.shape[dim])
66
+ if size % tp_size:
67
+ raise ValueError(
68
+ f"EXL3 TP shard: dim {dim} size {size} is not divisible by tp={tp_size}"
69
+ )
70
+ chunk = size // tp_size
71
+ return tensor.narrow(dim, chunk * tp_rank, chunk).contiguous()
72
+
73
+
74
+ def shard_exl3_col(loaded: torch.Tensor, suffix: str, tp_rank: int, tp_size: int) -> torch.Tensor:
75
+ """Gate/up: trellis dim 1 and svh dim 0 are column-parallel."""
76
+ if suffix == "trellis":
77
+ return _narrow_tp(loaded, 1, tp_rank, tp_size)
78
+ if suffix == "svh":
79
+ return _narrow_tp(loaded, 0, tp_rank, tp_size)
80
+ return loaded.contiguous()
81
+
82
+
83
+ def shard_exl3_row(loaded: torch.Tensor, suffix: str, tp_rank: int, tp_size: int) -> torch.Tensor:
84
+ """Down: trellis dim 0 and suh dim 0 are row-parallel."""
85
+ if suffix == "trellis":
86
+ return _narrow_tp(loaded, 0, tp_rank, tp_size)
87
+ if suffix == "suh":
88
+ return _narrow_tp(loaded, 0, tp_rank, tp_size)
89
+ return loaded.contiguous()
90
+
91
+
92
+ def _install_exllamav3_namespace() -> None:
93
+ """Load LinearEXL3 without running exllamav3/__init__.py (FlashAttention)."""
94
+ if "exllamav3.modules.quant.exl3" in sys.modules:
95
+ return
96
+ import exllamav3_ext # noqa: F401 — compiled extension must exist
97
+
98
+ spec = importlib.util.find_spec("exllamav3")
99
+ if spec is None or not spec.submodule_search_locations:
100
+ raise RuntimeError("exllamav3 package is not installed in this image")
101
+ package_root = Path(list(spec.submodule_search_locations)[0])
102
+
103
+ # Stub only packages whose __init__.py pulls FlashAttention / serving extras.
104
+ # Leave .ext, .util, and .modules.quant as real modules so LinearEXL3 loads.
105
+ for name, path in (
106
+ ("exllamav3", package_root),
107
+ ("exllamav3.modules", package_root / "modules"),
108
+ ("exllamav3.model", package_root / "model"),
109
+ ):
110
+ if name in sys.modules:
111
+ continue
112
+ module = types.ModuleType(name)
113
+ module.__file__ = str(path / "__init__.py")
114
+ module.__package__ = name
115
+ module.__path__ = [str(path)]
116
+ sys.modules[name] = module
117
+
118
+ if "exllamav3.model.config" not in sys.modules:
119
+ config = types.ModuleType("exllamav3.model.config")
120
+ config.__file__ = str(package_root / "model/config.py")
121
+ config.__package__ = "exllamav3.model"
122
+ config.Config = type("Config", (), {})
123
+ # KEYS PATCH: exllamav3 >= 1.x LinearEXL3(config=None) does
124
+ # `from ...model.config import NullConfig` and reads
125
+ # config.infer_params.no_reconstruct in forward().
126
+ class _InferParams:
127
+ no_reconstruct = False
128
+ mgemm_K_threshold = 0
129
+ mgemm_n_threshold = 0
130
+
131
+ class _NullConfig:
132
+ def __init__(self):
133
+ self.infer_params = _InferParams()
134
+
135
+ config.InferParams = _InferParams
136
+ config.NullConfig = _NullConfig
137
+ sys.modules[config.__name__] = config
138
+
139
+
140
+ def load_linear_exl3_cls():
141
+ _install_exllamav3_namespace()
142
+ return importlib.import_module("exllamav3.modules.quant.exl3").LinearEXL3
143
+
144
+
145
+ def make_linear_exl3(
146
+ trellis: torch.Tensor,
147
+ suh: torch.Tensor,
148
+ svh: torch.Tensor,
149
+ mcg: torch.Tensor,
150
+ *,
151
+ mul1: bool = False,
152
+ out_dtype: torch.dtype = torch.float16,
153
+ ):
154
+ """Build a LinearEXL3 over already-sharded packed tensors. No BF16 expand.
155
+ KEYS PATCH: `mcg` is the codebook marker tensor; with mul1=True it is passed as the
156
+ mul1 marker instead (exllamav3 LinearEXL3 accepts either)."""
157
+ cls = load_linear_exl3_cls()
158
+ marker = mcg.contiguous()
159
+ return cls(
160
+ config=None,
161
+ in_features=int(suh.numel()),
162
+ out_features=int(svh.numel()),
163
+ trellis=trellis.contiguous(),
164
+ suh=suh.contiguous(),
165
+ svh=svh.contiguous(),
166
+ mcg=None if mul1 else marker,
167
+ mul1=marker if mul1 else None,
168
+ out_dtype=out_dtype,
169
+ transformers_fix=True,
170
+ )
171
+
172
+
173
+ def execute_exl3_linear(
174
+ x: torch.Tensor,
175
+ trellis: torch.Tensor,
176
+ suh: torch.Tensor,
177
+ svh: torch.Tensor,
178
+ mcg: torch.Tensor,
179
+ *,
180
+ out_dtype: torch.dtype = torch.float32,
181
+ ) -> torch.Tensor:
182
+ """Real EXL3 expert GEMM entry (LinearEXL3 / exllamav3_ext)."""
183
+ inner = make_linear_exl3(trellis, suh, svh, mcg, out_dtype=torch.float16)
184
+ return inner.forward(x.contiguous().half(), {}, out_dtype=out_dtype)
185
+
186
+
187
+ def fused_moe_enabled() -> bool:
188
+ return os.environ.get("EXL3_FUSED_MOE", "1") != "0"
189
+
190
+
191
+ def load_exllamav3_ext():
192
+ import exllamav3_ext
193
+
194
+ return exllamav3_ext
195
+
196
+
197
+ def _exl3_moe_accepts_num_active(fn) -> bool:
198
+ try:
199
+ import inspect
200
+
201
+ if "num_active" in inspect.signature(fn).parameters:
202
+ return True
203
+ except (TypeError, ValueError):
204
+ pass
205
+ doc = getattr(fn, "__doc__", None) or ""
206
+ return "num_active" in doc or "arg29" in doc or doc.count("arg") >= 30
207
+
208
+
209
+ def pin_exl3_expert_map(
210
+ layer: torch.nn.Module, device: torch.device
211
+ ) -> torch.Tensor | None:
212
+ """Move expert_map onto `device` once. CUDA graph capture forbids a CPU→GPU copy."""
213
+ emap = getattr(layer, "expert_map", None)
214
+ if emap is None:
215
+ return None
216
+ if emap.device != device or emap.dtype != torch.long:
217
+ layer.expert_map = emap.to(device=device, dtype=torch.long)
218
+ return layer.expert_map
219
+
220
+
221
+ def map_topk_to_local(
222
+ ids: torch.Tensor,
223
+ n_local: int,
224
+ expert_map: torch.Tensor | None,
225
+ ) -> torch.Tensor:
226
+ """ids (T, K) global expert ids → local ids, invalid/non-local → n_local sentinel.
227
+
228
+ `expert_map` must already live on `ids.device` (see pin_exl3_expert_map).
229
+ """
230
+ flat = ids.reshape(-1)
231
+ if expert_map is None:
232
+ invalid = (flat < 0) | (flat >= n_local)
233
+ return torch.where(invalid, flat.new_full(flat.shape, n_local), flat)
234
+ if expert_map.device != flat.device or expert_map.dtype != torch.long:
235
+ raise RuntimeError(
236
+ "EXL3 expert_map is not pinned to the hidden-state device; "
237
+ "call pin_exl3_expert_map before fused apply (CUDA graphs forbid the copy)"
238
+ )
239
+ n_global = int(expert_map.numel())
240
+ safe = flat.clamp(min=0, max=max(n_global - 1, 0))
241
+ mapped = expert_map[safe] if n_global else flat.new_full(flat.shape, n_local)
242
+ invalid = (flat < 0) | (flat >= n_global) | (mapped < 0) | (mapped >= n_local)
243
+ return torch.where(invalid, flat.new_full(flat.shape, n_local), mapped)
244
+
245
+
246
+ def apply_exl3_python_loop(
247
+ x2d: torch.Tensor,
248
+ ids: torch.Tensor,
249
+ weights: torch.Tensor,
250
+ inners: list[dict[str, Any]],
251
+ expert_map: torch.Tensor | None,
252
+ limit: float,
253
+ *,
254
+ only_experts: set[int] | None = None,
255
+ out: torch.Tensor | None = None,
256
+ ) -> torch.Tensor:
257
+ """Unique-expert LinearEXL3 loop. `only_experts` is local ids (fat-expert fallback)."""
258
+ tokens, hidden = x2d.shape
259
+ if out is None:
260
+ out = torch.zeros(tokens, hidden, dtype=torch.float32, device=x2d.device)
261
+ unique = torch.unique(ids)
262
+ for raw in unique.tolist():
263
+ e_raw = int(raw)
264
+ if e_raw < 0:
265
+ continue
266
+ e = e_raw
267
+ if expert_map is not None:
268
+ mapped = int(expert_map[e].item()) if expert_map.numel() > e else e
269
+ if mapped < 0:
270
+ continue
271
+ e = mapped
272
+ if e >= len(inners):
273
+ continue
274
+ if only_experts is not None and e not in only_experts:
275
+ continue
276
+ token_idx, k_pos = (ids == int(raw)).nonzero(as_tuple=True)
277
+ h = x2d.index_select(0, token_idx)
278
+ pack = inners[e]
279
+ gate = pack["gate"].forward(h.contiguous().half(), {}, out_dtype=torch.float32)
280
+ up = pack["up"].forward(h.contiguous().half(), {}, out_dtype=torch.float32)
281
+ act = F.silu(gate.clamp(max=limit)) * up.clamp(min=-limit, max=limit)
282
+ down = pack["down"].forward(act.contiguous().half(), {}, out_dtype=torch.float32)
283
+ scale = weights[token_idx, k_pos].unsqueeze(-1).to(dtype=torch.float32)
284
+ out.index_add_(0, token_idx, down * scale)
285
+ return out
286
+
287
+
288
+ def build_exl3_fused_state(layer: torch.nn.Module, inners: list[dict[str, Any]]) -> None:
289
+ """Pointer tables + fused temps, once after load. No per-token alloc."""
290
+ import exllamav3_ext
291
+
292
+ device = layer.w13_trellis.device
293
+ n_exp = len(inners)
294
+ hidden = int(layer._exl3_hidden_size)
295
+ intermediate = int(layer._exl3_intermediate_local)
296
+
297
+ def _ptrs(which: str, attr: str) -> torch.Tensor:
298
+ return torch.tensor(
299
+ [int(getattr(pack[which], attr).data_ptr()) for pack in inners],
300
+ dtype=torch.int64,
301
+ device=device,
302
+ )
303
+
304
+ layer._exl3_ptrs = {
305
+ "gate_trellis": _ptrs("gate", "trellis"),
306
+ "gate_suh": _ptrs("gate", "suh"),
307
+ "gate_svh": _ptrs("gate", "svh"),
308
+ "up_trellis": _ptrs("up", "trellis"),
309
+ "up_suh": _ptrs("up", "suh"),
310
+ "up_svh": _ptrs("up", "svh"),
311
+ "down_trellis": _ptrs("down", "trellis"),
312
+ "down_suh": _ptrs("down", "suh"),
313
+ "down_svh": _ptrs("down", "svh"),
314
+ }
315
+ idx = int(device.index) if device.index is not None else 0
316
+ concurrency = int(exllamav3_ext.exl3_moe_max_concurrency(idx))
317
+ if concurrency < 1:
318
+ concurrency = 1
319
+ key = (str(device), hidden, intermediate, concurrency)
320
+ temps = _FUSED_TEMP_CACHE.get(key)
321
+ if temps is None:
322
+ temps = (
323
+ torch.empty((concurrency, TEMP_ROWS_FUSED, hidden), dtype=torch.float16, device=device),
324
+ torch.empty((concurrency, TEMP_ROWS_FUSED, hidden), dtype=torch.float16, device=device),
325
+ torch.empty((concurrency, TEMP_ROWS_FUSED, intermediate), dtype=torch.float16, device=device),
326
+ torch.empty((concurrency, TEMP_ROWS_FUSED, intermediate), dtype=torch.float16, device=device),
327
+ )
328
+ _FUSED_TEMP_CACHE[key] = temps
329
+ layer._exl3_fused_temps = temps
330
+ layer._exl3_fused_concurrency = concurrency
331
+ layer._exl3_k = int(layer._exl3_bits)
332
+
333
+
334
+ def apply_exl3_fused_moe(
335
+ x2d: torch.Tensor,
336
+ ids: torch.Tensor,
337
+ weights: torch.Tensor,
338
+ layer: torch.nn.Module,
339
+ inners: list[dict[str, Any]],
340
+ expert_map: torch.Tensor | None,
341
+ limit: float,
342
+ ) -> torch.Tensor:
343
+ """One exl3_moe launch per layer. Experts with count > 128 fall back to LinearEXL3."""
344
+ import exllamav3_ext
345
+
346
+ tokens, hidden = x2d.shape
347
+ n_exp = len(inners)
348
+ ptrs = getattr(layer, "_exl3_ptrs", None)
349
+ temps = getattr(layer, "_exl3_fused_temps", None)
350
+ if not ptrs or temps is None:
351
+ raise RuntimeError("EXL3 fused pointer tables were not built after weight load")
352
+
353
+ local = map_topk_to_local(ids, n_exp, expert_map)
354
+ topk = int(ids.shape[-1])
355
+ flat_token = torch.arange(tokens, device=x2d.device, dtype=torch.long).repeat_interleave(topk)
356
+ flat_weight = weights.reshape(-1).to(dtype=torch.float16)
357
+ order = local.argsort()
358
+ token_sorted = flat_token[order]
359
+ weight_sorted = flat_weight[order]
360
+ # scatter_add stays on GPU. torch.bincount can host-stage and break CUDA graphs.
361
+ expert_count = torch.zeros(n_exp + 1, dtype=torch.long, device=local.device)
362
+ expert_count.scatter_add_(
363
+ 0, local.long(), torch.ones(local.shape, dtype=torch.long, device=local.device)
364
+ )
365
+ out = torch.zeros(tokens, hidden, dtype=torch.float32, device=x2d.device)
366
+ xh = x2d.contiguous().half()
367
+
368
+ counts = expert_count[:n_exp]
369
+ fn = exllamav3_ext.exl3_moe
370
+ # -1 = unknown active count: max-concurrency grid, no .item() host sync.
371
+ n_active_host = -1 if _exl3_moe_accepts_num_active(fn) else None
372
+
373
+ k = int(getattr(layer, "_exl3_k", 4))
374
+ _m1 = bool(getattr(layer, "_exl3_mul1", False))
375
+ args = (
376
+ xh,
377
+ out,
378
+ expert_count,
379
+ token_sorted,
380
+ weight_sorted,
381
+ temps[0],
382
+ temps[1],
383
+ temps[2],
384
+ temps[3],
385
+ MOE_ACT_SILU,
386
+ k,
387
+ k,
388
+ k,
389
+ ptrs["gate_trellis"],
390
+ ptrs["gate_suh"],
391
+ ptrs["gate_svh"],
392
+ ptrs["up_trellis"],
393
+ ptrs["up_suh"],
394
+ ptrs["up_svh"],
395
+ ptrs["down_trellis"],
396
+ ptrs["down_suh"],
397
+ ptrs["down_svh"],
398
+ # KEYS PATCH: (mcg, mul1) flag pairs for gate / up / down, per-layer codebook.
399
+ not _m1,
400
+ _m1,
401
+ not _m1,
402
+ _m1,
403
+ not _m1,
404
+ _m1,
405
+ float(limit),
406
+ )
407
+ if n_active_host is not None:
408
+ fn(*args, n_active_host)
409
+ else:
410
+ fn(*args)
411
+
412
+ if tokens > TEMP_ROWS_FUSED:
413
+ fat = (counts > TEMP_ROWS_FUSED).nonzero(as_tuple=False).view(-1)
414
+ if fat.numel():
415
+ apply_exl3_python_loop(
416
+ x2d,
417
+ ids,
418
+ weights,
419
+ inners,
420
+ expert_map,
421
+ limit,
422
+ only_experts=set(int(i) for i in fat.tolist()),
423
+ out=out,
424
+ )
425
+ return out
426
+
427
+
428
+ def apply_exl3_experts(
429
+ x: torch.Tensor,
430
+ topk_ids: torch.Tensor,
431
+ topk_weights: torch.Tensor,
432
+ layer: torch.nn.Module,
433
+ *,
434
+ limit: float = SWIGLU_LIMIT_DEFAULT,
435
+ fused: bool | None = None,
436
+ ) -> torch.Tensor:
437
+ """Shipped routed-expert apply. `fused=None` honors EXL3_FUSED_MOE."""
438
+ inners = getattr(layer, "_exl3_inners", None)
439
+ if not inners:
440
+ raise RuntimeError("EXL3 experts were not built after weight load")
441
+ tokens, hidden = x.shape[-2], x.shape[-1]
442
+ x2d = x.reshape(tokens, hidden)
443
+ ids = topk_ids.reshape(tokens, -1).to(torch.long)
444
+ weights = topk_weights.reshape(tokens, -1)
445
+ expert_map = pin_exl3_expert_map(layer, x2d.device)
446
+ have_ptrs = bool(getattr(layer, "_exl3_ptrs", None))
447
+ if fused is True and not have_ptrs:
448
+ raise RuntimeError("EXL3 fused apply requested but pointer tables are missing")
449
+ use_fused = (fused_moe_enabled() if fused is None else bool(fused)) and have_ptrs
450
+ if use_fused:
451
+ try:
452
+ import exllamav3_ext
453
+
454
+ use_fused = hasattr(exllamav3_ext, "exl3_moe")
455
+ except Exception:
456
+ use_fused = False
457
+ if use_fused:
458
+ out = apply_exl3_fused_moe(x2d, ids, weights, layer, inners, expert_map, limit)
459
+ layer._exl3_last_apply = "fused"
460
+ else:
461
+ out = apply_exl3_python_loop(x2d, ids, weights, inners, expert_map, limit)
462
+ layer._exl3_last_apply = "loop"
463
+ return out.to(dtype=x.dtype)
464
+
465
+
466
+ def _suffix_from_mapped_name(weight_name: str) -> str:
467
+ tail = weight_name.rsplit(".", 1)[-1]
468
+ for suffix in EXL3_SUFFIXES:
469
+ if tail == suffix or tail.endswith("_" + suffix):
470
+ return suffix
471
+ raise ValueError(f"not an EXL3 packed name: {weight_name}")
472
+
473
+
474
+
475
+ def _is_mtp_prefix(prefix: str) -> bool:
476
+ """True for layers at index >= num_hidden_layers (DeepSeek/GLM MTP blocks)."""
477
+ import re as _re
478
+ m = _re.search(r"\.layers\.(\d+)\.", prefix + ".")
479
+ if not m:
480
+ return "mtp" in prefix.lower()
481
+ idx = int(m.group(1))
482
+ n = None
483
+ try:
484
+ from vllm.config import get_current_vllm_config
485
+ n = int(get_current_vllm_config().model_config.hf_config.num_hidden_layers)
486
+ except Exception: # noqa: BLE001
487
+ n = None
488
+ if n is None:
489
+ n = int(os.environ.get("EXL3_MTP_LAYER_START", "78"))
490
+ return idx >= n
491
+
492
+ @register_quantization_config("exl3")
493
+ class Exl3Config(QuantizationConfig):
494
+ """Routed-experts-only EXL3/MCG. Dense / shared / attention stay native."""
495
+
496
+ def __init__(
497
+ self,
498
+ bits: int = 4,
499
+ codebook: str = "mcg",
500
+ scope: str = "glm53_routed_experts_only",
501
+ **kwargs: Any,
502
+ ) -> None:
503
+ super().__init__()
504
+ self.bits = int(bits)
505
+ self.codebook = str(codebook)
506
+ self.scope = str(scope)
507
+ self.raw_config = dict(kwargs)
508
+ if self.codebook not in ("mcg", "mul1", "mixed", "auto"):
509
+ raise ValueError(
510
+ f"this overlay implements codebook mcg/mul1 (per layer); got {self.codebook!r}"
511
+ )
512
+ if self.bits not in (3, 4, 5, 6):
513
+ raise ValueError(f"unsupported EXL3 bits={self.bits}")
514
+
515
+ def get_name(self) -> str:
516
+ return "exl3"
517
+
518
+ def get_supported_act_dtypes(self) -> list[torch.dtype]:
519
+ return [torch.bfloat16, torch.float16, torch.float32]
520
+
521
+ @classmethod
522
+ def get_min_capability(cls) -> int:
523
+ # LinearEXL3 uses CUDA >= Ampere; GB10 is SM121.
524
+ return 80
525
+
526
+ @staticmethod
527
+ def get_config_filenames() -> list[str]:
528
+ return ["quantization_config.json"]
529
+
530
+ @classmethod
531
+ def from_config(cls, config: dict[str, Any]) -> "Exl3Config":
532
+ skip = {
533
+ "bits",
534
+ "codebook",
535
+ "scope",
536
+ "quant_method",
537
+ # tr3 ships a 37 MiB per-tensor ledger; keep it off the config object.
538
+ "tensor_storage",
539
+ }
540
+ return cls(
541
+ bits=int(config.get("bits", 4)),
542
+ codebook=str(config.get("codebook", "mcg")),
543
+ scope=str(config.get("scope", "glm53_routed_experts_only")),
544
+ **{k: v for k, v in config.items() if k not in skip},
545
+ )
546
+
547
+ @classmethod
548
+ def override_quantization_method(
549
+ cls,
550
+ hf_quant_cfg: dict[str, Any],
551
+ user_quant: str | None,
552
+ hf_config: Any = None,
553
+ ) -> str | None:
554
+ method = str((hf_quant_cfg or {}).get("quant_method", "")).lower()
555
+ if method == "exl3":
556
+ return "exl3"
557
+ return None
558
+
559
+ def get_quant_method(self, layer: torch.nn.Module, prefix: str):
560
+ from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
561
+
562
+ # KEYS PATCH: the MTP block (model.layers.<num_hidden_layers>+) is stored bf16
563
+ # (-mb 16) -> leave it to the stock unquantized MoE path so SPEC_METHOD=mtp works.
564
+ if isinstance(layer, RoutedExperts) and _is_mtp_prefix(prefix):
565
+ logger.info_once("EXL3: MTP block %s left unquantized (bf16 experts)", prefix)
566
+ return None
567
+ if isinstance(layer, RoutedExperts):
568
+ return Exl3MoEMethod(layer.moe_config, self)
569
+ if isinstance(layer, LinearBase):
570
+ return UnquantizedLinearMethod()
571
+ return None
572
+
573
+
574
+ class Exl3MoEMethod(FusedMoEMethodBase):
575
+ """Packed MCG trellis experts: create/load packed tensors, LinearEXL3 apply."""
576
+
577
+ def __init__(self, moe, quant_config: Exl3Config) -> None:
578
+ super().__init__(moe)
579
+ self.quant_config = quant_config
580
+ self.bits = quant_config.bits
581
+ self._logged = False
582
+
583
+ def get_fused_moe_quant_config(self, layer: "RoutedExperts") -> FusedMoEQuantConfig | None:
584
+ return None
585
+
586
+ def create_weights(
587
+ self,
588
+ layer: "RoutedExperts",
589
+ num_experts: int,
590
+ hidden_size: int,
591
+ intermediate_size_per_partition: int,
592
+ params_dtype: torch.dtype,
593
+ **extra_weight_attrs,
594
+ ) -> None:
595
+ del params_dtype
596
+ if hidden_size % 16 or intermediate_size_per_partition % 16:
597
+ raise ValueError(
598
+ "EXL3 trellis tiles are 16-wide; "
599
+ f"hidden={hidden_size} intermediate_local={intermediate_size_per_partition}"
600
+ )
601
+ k_words = self.bits * 16
602
+ in_tiles = hidden_size // 16
603
+ out_tiles = intermediate_size_per_partition // 16
604
+
605
+ extra = {k: v for k, v in extra_weight_attrs.items() if k != "weight_loader"}
606
+
607
+ # w13_* : stacked [expert, {gate=0, up=1}, ...] so the stock
608
+ # expert_params_mapping (experts.w13_ + suffix) hits these names.
609
+ w13_trellis = Parameter(
610
+ torch.empty(
611
+ num_experts, 2, in_tiles, out_tiles, k_words, dtype=torch.int16
612
+ ),
613
+ requires_grad=False,
614
+ )
615
+ w13_suh = Parameter(
616
+ torch.empty(num_experts, 2, hidden_size, dtype=torch.float16),
617
+ requires_grad=False,
618
+ )
619
+ w13_svh = Parameter(
620
+ torch.empty(
621
+ num_experts, 2, intermediate_size_per_partition, dtype=torch.float16
622
+ ),
623
+ requires_grad=False,
624
+ )
625
+ w13_mcg = Parameter(
626
+ torch.zeros(num_experts, 2, 1, dtype=torch.int32),
627
+ requires_grad=False,
628
+ )
629
+ w13_mul1 = Parameter(
630
+ torch.zeros(num_experts, 2, 1, dtype=torch.int32),
631
+ requires_grad=False,
632
+ )
633
+ w2_trellis = Parameter(
634
+ torch.empty(
635
+ num_experts, out_tiles, in_tiles, k_words, dtype=torch.int16
636
+ ),
637
+ requires_grad=False,
638
+ )
639
+ w2_suh = Parameter(
640
+ torch.empty(
641
+ num_experts, intermediate_size_per_partition, dtype=torch.float16
642
+ ),
643
+ requires_grad=False,
644
+ )
645
+ w2_svh = Parameter(
646
+ torch.empty(num_experts, hidden_size, dtype=torch.float16),
647
+ requires_grad=False,
648
+ )
649
+ w2_mcg = Parameter(
650
+ torch.zeros(num_experts, 1, dtype=torch.int32),
651
+ requires_grad=False,
652
+ )
653
+ w2_mul1 = Parameter(
654
+ torch.zeros(num_experts, 1, dtype=torch.int32),
655
+ requires_grad=False,
656
+ )
657
+
658
+ packed = {
659
+ "w13_trellis": w13_trellis,
660
+ "w13_suh": w13_suh,
661
+ "w13_svh": w13_svh,
662
+ "w13_mcg": w13_mcg,
663
+ "w13_mul1": w13_mul1,
664
+ "w2_trellis": w2_trellis,
665
+ "w2_suh": w2_suh,
666
+ "w2_svh": w2_svh,
667
+ "w2_mcg": w2_mcg,
668
+ "w2_mul1": w2_mul1,
669
+ }
670
+ for name, param in packed.items():
671
+ layer.register_parameter(name, param)
672
+ set_weight_attrs(param, extra)
673
+ param.weight_loader = self._load_exl3
674
+ param._exl3_owner = layer
675
+ if hasattr(layer, "w13_weight") or hasattr(layer, "w2_weight"):
676
+ raise RuntimeError("EXL3 create_weights must not allocate dense expert weights")
677
+
678
+ layer._exl3_hidden_size = hidden_size
679
+ layer._exl3_intermediate_local = intermediate_size_per_partition
680
+ layer._exl3_k_words = k_words
681
+ layer._exl3_bits = self.bits
682
+
683
+ def _load_exl3(
684
+ self,
685
+ param: Parameter,
686
+ loaded_weight: torch.Tensor,
687
+ weight_name: str,
688
+ shard_id: str = "w1",
689
+ expert_id: int = 0,
690
+ return_success: bool = False,
691
+ ) -> bool | None:
692
+ from vllm.distributed import (
693
+ get_tensor_model_parallel_rank,
694
+ get_tensor_model_parallel_world_size,
695
+ )
696
+
697
+ layer = param
698
+ # param is the Parameter; expert_id is already physical. Map to local
699
+ # via the owning module if present on the weight_loader closure... we
700
+ # look up from param's __dict__ after register. RoutedExperts.weight_loader
701
+ # maps global→local; glm5next calls *our* loader, so map here.
702
+ owner = getattr(param, "_exl3_owner", None)
703
+ if owner is not None:
704
+ local_id = owner._map_global_expert_id_to_local_expert_id(expert_id)
705
+ if local_id == -1:
706
+ return False if return_success else None
707
+ expert_id = local_id
708
+
709
+ tp_rank = get_tensor_model_parallel_rank()
710
+ tp_size = get_tensor_model_parallel_world_size()
711
+ suffix = _suffix_from_mapped_name(weight_name)
712
+ loaded = loaded_weight.detach().contiguous()
713
+
714
+ if shard_id in ("w1", "w3"):
715
+ shard_idx = 0 if shard_id == "w1" else 1
716
+ sharded = shard_exl3_col(loaded, suffix, tp_rank, tp_size)
717
+ dest = param.data[expert_id, shard_idx]
718
+ elif shard_id == "w2":
719
+ sharded = shard_exl3_row(loaded, suffix, tp_rank, tp_size)
720
+ dest = param.data[expert_id]
721
+ else:
722
+ raise ValueError(f"unknown EXL3 shard_id={shard_id}")
723
+
724
+ # KEYS PATCH: codebook markers (mcg/mul1) may be stored as 0-d scalars; accept any
725
+ # shape with the same element count.
726
+ if suffix in ("mcg", "mul1") and sharded.numel() == dest.numel():
727
+ sharded = sharded.reshape(dest.shape)
728
+ if tuple(dest.shape) != tuple(sharded.shape):
729
+ raise RuntimeError(
730
+ f"EXL3 load shape mismatch {weight_name} shard={shard_id} "
731
+ f"expert={expert_id}: dest {tuple(dest.shape)} != "
732
+ f"loaded {tuple(sharded.shape)}"
733
+ )
734
+ dest.copy_(sharded)
735
+ return True if return_success else None
736
+
737
+ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
738
+ if not hasattr(layer, "w13_trellis"):
739
+ return
740
+ # Bind owner for any late loads; stitch LinearEXL3 handles.
741
+ for name in (
742
+ "w13_trellis",
743
+ "w13_suh",
744
+ "w13_svh",
745
+ "w13_mcg",
746
+ "w13_mul1",
747
+ "w2_trellis",
748
+ "w2_suh",
749
+ "w2_svh",
750
+ "w2_mcg",
751
+ "w2_mul1",
752
+ ):
753
+ getattr(layer, name)._exl3_owner = layer
754
+
755
+ # KEYS PATCH: per-layer codebook detection from the loaded marker tensors.
756
+ mcg13 = layer.w13_mcg.reshape(-1)
757
+ mcg2 = layer.w2_mcg.reshape(-1)
758
+ mul13 = layer.w13_mul1.reshape(-1)
759
+ mul2 = layer.w2_mul1.reshape(-1)
760
+ is_mcg = bool(torch.all(mcg13 == MCG_MARKER_SIGNED_INT32)) and bool(
761
+ torch.all(mcg2 == MCG_MARKER_SIGNED_INT32)
762
+ )
763
+ is_mul1 = bool(torch.all(mul13 == MUL1_MARKER_SIGNED_INT32)) and bool(
764
+ torch.all(mul2 == MUL1_MARKER_SIGNED_INT32)
765
+ )
766
+ if is_mcg == is_mul1:
767
+ raise RuntimeError(
768
+ "EXL3 codebook markers inconsistent for this layer: "
769
+ f"mcg={is_mcg} mul1={is_mul1} (expect exactly one; "
770
+ f"MCG {MCG_MARKER_SIGNED_INT32} / MUL1 {MUL1_MARKER_SIGNED_INT32})"
771
+ )
772
+ use_mul1 = is_mul1
773
+ layer._exl3_mul1 = use_mul1
774
+ m13 = layer.w13_mul1 if use_mul1 else layer.w13_mcg
775
+ m2 = layer.w2_mul1 if use_mul1 else layer.w2_mcg
776
+ if not getattr(Exl3MoEMethod, "_logged_codebooks", None):
777
+ Exl3MoEMethod._logged_codebooks = set()
778
+ if use_mul1 not in Exl3MoEMethod._logged_codebooks:
779
+ Exl3MoEMethod._logged_codebooks.add(use_mul1)
780
+ logger.info("EXL3 experts codebook detected: %s", "mul1" if use_mul1 else "mcg")
781
+
782
+ n_exp = int(layer.w13_trellis.shape[0])
783
+ inners: list[dict[str, Any]] = []
784
+ for e in range(n_exp):
785
+ gate = make_linear_exl3(
786
+ layer.w13_trellis[e, 0],
787
+ layer.w13_suh[e, 0],
788
+ layer.w13_svh[e, 0],
789
+ m13[e, 0],
790
+ mul1=use_mul1,
791
+ )
792
+ up = make_linear_exl3(
793
+ layer.w13_trellis[e, 1],
794
+ layer.w13_suh[e, 1],
795
+ layer.w13_svh[e, 1],
796
+ m13[e, 1],
797
+ mul1=use_mul1,
798
+ )
799
+ down = make_linear_exl3(
800
+ layer.w2_trellis[e],
801
+ layer.w2_suh[e],
802
+ layer.w2_svh[e],
803
+ m2[e],
804
+ mul1=use_mul1,
805
+ )
806
+ inners.append({"gate": gate, "up": up, "down": down})
807
+ layer._exl3_inners = inners
808
+ fused_ok = False
809
+ fused_err = None
810
+ if fused_moe_enabled():
811
+ try:
812
+ import exllamav3_ext
813
+
814
+ if hasattr(exllamav3_ext, "exl3_moe"):
815
+ build_exl3_fused_state(layer, inners)
816
+ fused_ok = True
817
+ else:
818
+ fused_err = "exllamav3_ext.exl3_moe missing"
819
+ except Exception as exc:
820
+ fused_err = repr(exc)
821
+ layer._exl3_ptrs = None
822
+ if not self._logged:
823
+ if fused_ok:
824
+ logger.info(
825
+ "EXL3 MCG trellis engaged for routed experts: bits=%s "
826
+ "experts_local=%s hidden=%s intermediate_local=%s "
827
+ "fused_moe=exl3_moe concurrency=%s "
828
+ "(no BF16 expert reconstruct at load)",
829
+ self.bits,
830
+ n_exp,
831
+ layer._exl3_hidden_size,
832
+ layer._exl3_intermediate_local,
833
+ getattr(layer, "_exl3_fused_concurrency", "?"),
834
+ )
835
+ else:
836
+ logger.info(
837
+ "EXL3 MCG trellis engaged for routed experts: bits=%s "
838
+ "experts_local=%s hidden=%s intermediate_local=%s "
839
+ "fused_moe=python_loop (%s) "
840
+ "(no BF16 expert reconstruct at load)",
841
+ self.bits,
842
+ n_exp,
843
+ layer._exl3_hidden_size,
844
+ layer._exl3_intermediate_local,
845
+ fused_err or "EXL3_FUSED_MOE=0",
846
+ )
847
+ self._logged = True
848
+
849
+ def apply(
850
+ self,
851
+ layer: "RoutedExperts",
852
+ x: torch.Tensor,
853
+ topk_weights: torch.Tensor,
854
+ topk_ids: torch.Tensor,
855
+ shared_experts: "SharedExperts | None",
856
+ shared_experts_input: torch.Tensor | None,
857
+ ) -> torch.Tensor:
858
+ del shared_experts, shared_experts_input
859
+ limit = getattr(self.moe, "swiglu_limit", None) or SWIGLU_LIMIT_DEFAULT
860
+ return apply_exl3_experts(
861
+ x, topk_ids, topk_weights, layer, limit=float(limit)
862
+ )
serving/finalize_config.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Post-compile fixups so the exllamav3 output is a Mia-native vLLM checkpoint.
3
+
4
+ * config.json / quantization_config.json: quant_method=exl3, bits=<int>, codebook=mcg,
5
+ scope=glm53_routed_experts_only, head_bits=16, mtp_bits=16 (Mia's Exl3Config reads bits/codebook/scope;
6
+ everything else is carried as raw config). The FP8 source block is preserved as original_quantization_config.
7
+ * sanity: every routed expert has trellis/suh/svh/mcg; every non-expert linear is a plain .weight; report dtypes.
8
+ Usage: finalize_config.py <out_dir> [expert_bits]
9
+ """
10
+ import json, os, sys, re, struct, collections
11
+
12
+ out = sys.argv[1]
13
+ bits = int(sys.argv[2]) if len(sys.argv) > 2 else 3
14
+ CODEBOOK = sys.argv[3] if len(sys.argv) > 3 else "mcg"
15
+
16
+ def read_header(path):
17
+ with open(path, "rb") as f:
18
+ n = struct.unpack("<Q", f.read(8))[0]
19
+ return json.loads(f.read(n))
20
+
21
+ # --- tensor inventory
22
+ suffix_by_kind = collections.Counter()
23
+ dtypes = collections.Counter()
24
+ experts_ok, experts_bad = 0, []
25
+ nonexp_quant = []
26
+ shards = sorted(p for p in os.listdir(out) if p.endswith(".safetensors"))
27
+ seen = {}
28
+ for sh in shards:
29
+ for k, v in read_header(os.path.join(out, sh)).items():
30
+ if k == "__metadata__":
31
+ continue
32
+ seen[k] = v["dtype"]
33
+ exp = collections.defaultdict(set)
34
+ for k in seen:
35
+ m = re.match(r"model\.layers\.(\d+)\.mlp\.experts\.(\d+)\.(gate|up|down)_proj\.(\w+)$", k)
36
+ if m:
37
+ exp[(int(m.group(1)), int(m.group(2)), m.group(3))].add(m.group(4))
38
+ continue
39
+ m2 = re.match(r"(model\.layers\.\d+\.(self_attn|mlp\.shared_experts|mlp)\.[\w.]+?)\.(trellis|suh|svh|mcg|mul1)$", k)
40
+ if m2:
41
+ nonexp_quant.append(k)
42
+ for key, sfx in exp.items():
43
+ if {"trellis", "suh", "svh"} <= sfx and ({"mcg"} <= sfx or {"mul1"} <= sfx):
44
+ experts_ok += 1
45
+ else:
46
+ experts_bad.append((key, sorted(sfx)))
47
+ for k, d in seen.items():
48
+ dtypes[(k.rsplit(".", 1)[-1], d)] += 1
49
+ print(f"shards={len(shards)} tensors={len(seen)} experts_ok={experts_ok} experts_bad={len(experts_bad)} nonexpert_quantized={len(nonexp_quant)}")
50
+ if experts_bad:
51
+ print(" bad:", experts_bad[:5])
52
+ if nonexp_quant:
53
+ print(" !! non-expert EXL3 tensors (Mia overlay expects native):", nonexp_quant[:5])
54
+ for (sfx, d), n in sorted(dtypes.items(), key=lambda x: -x[1])[:12]:
55
+ print(f" {sfx:14s} {d:8s} {n}")
56
+
57
+ # --- config fixups
58
+ def fix(path, is_hf_config):
59
+ d = json.load(open(path))
60
+ q = d.get("quantization_config", d if not is_hf_config else {}) if is_hf_config else d
61
+ if is_hf_config:
62
+ q = d.setdefault("quantization_config", {})
63
+ orig = None
64
+ if q.get("quant_method") not in (None, "exl3"):
65
+ orig = dict(q)
66
+ q.clear()
67
+ if orig is not None:
68
+ q["original_quantization_config"] = orig
69
+ q.update({"quant_method": "exl3", "bits": bits, "codebook": CODEBOOK, "scope": "glm53_routed_experts_only",
70
+ "head_bits": 16, "mtp_bits": 16, "keys_profile": f"routed-experts-{bits}bpw-{CODEBOOK}; attn/dense/shared/head bf16",
71
+ "keys_source": "zai-org/GLM-5.3 (FP8) via exllamav3 1.4.5 + keys encode farm"})
72
+ q.pop("tensor_storage", None) if is_hf_config else None
73
+ json.dump(d, open(path, "w"), indent=2)
74
+ print(f"wrote {path}: quant_method={q['quant_method']} bits={q['bits']} codebook={q['codebook']}")
75
+
76
+ fix(os.path.join(out, "config.json"), True)
77
+ qc = os.path.join(out, "quantization_config.json")
78
+ if os.path.exists(qc):
79
+ fix(qc, False)
80
+ for fn in ("tokenizer.json", "tokenizer_config.json", "chat_template.jinja", "generation_config.json", "model.safetensors.index.json"):
81
+ print(f" {fn}: {'ok' if os.path.exists(os.path.join(out, fn)) else 'MISSING'}")
serving/glm53_probe.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json,time,urllib.request,sys
2
+ BASE="http://10.100.10.1:8888"
3
+ def chat(prompt,max_tokens,think=False):
4
+ body={"model":"GLM-5.3-EXL3","messages":[{"role":"user","content":prompt}],"max_tokens":max_tokens,"temperature":0,"stream":True,"stream_options":{"include_usage":True},"chat_template_kwargs":{"enable_thinking":think}}
5
+ req=urllib.request.Request(BASE+"/v1/chat/completions",data=json.dumps(body).encode(),headers={"Content-Type":"application/json"})
6
+ t0=time.time(); ttft=None; n=0; text=""
7
+ with urllib.request.urlopen(req,timeout=3600) as r:
8
+ for line in r:
9
+ line=line.decode().strip()
10
+ if not line.startswith("data:") or line=="data: [DONE]": continue
11
+ ev=json.loads(line[5:])
12
+ if ev.get("usage"): n=ev["usage"]["completion_tokens"]; pt=ev["usage"]["prompt_tokens"]
13
+ ch=ev.get("choices")
14
+ if ch:
15
+ d=ch[0].get("delta") or {}
16
+ c=d.get("content") or d.get("reasoning_content") or ""
17
+ if c and ttft is None: ttft=time.time()-t0
18
+ text+=c
19
+ wall=time.time()-t0
20
+ return dict(prompt_tokens=pt,tokens=n,ttft=ttft,wall=wall,decode_tps=n/(wall-ttft) if ttft and wall>ttft else None,text=text)
21
+ mode=sys.argv[1]
22
+ if mode=="decode":
23
+ r=chat("Write a detailed 300-word explanation of how a transformer language model generates text, covering tokenization, attention, and sampling.",256)
24
+ print(f"decode: prompt {r['prompt_tokens']} tok, gen {r['tokens']} tok, TTFT {r['ttft']:.2f}s, decode {r['decode_tps']:.2f} tok/s"); print("head:",repr(r["text"][:200]))
25
+ elif mode=="needle":
26
+ k=int(sys.argv[2])
27
+ filler="The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs. "*(k*1000//19)
28
+ secret="The secret passphrase is VIOLET-KANGAROO-7731."
29
+ pos=len(filler)//2; doc=filler[:pos]+" "+secret+" "+filler[pos:]
30
+ r=chat(doc+"\n\nWhat is the secret passphrase mentioned in the text above? Reply with just the passphrase.",40)
31
+ ok="VIOLET-KANGAROO-7731" in r["text"]
32
+ print(f"needle {k}K: prompt {r['prompt_tokens']} tok, TTFT {r['ttft']:.1f}s ({r['prompt_tokens']/r['ttft']:.0f} tok/s prefill), decode {r['decode_tps']:.2f} tok/s, PASS={ok}, reply={r['text'].strip()[:80]!r}")
serving/keys-launch.sh ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # keys wrapper: drop page caches on all 4 ranks (NFS weight reads inflate "used" and trip vLLM's
3
+ # startup free-memory guard on unified-memory GB10), then launch with the standard skip flags.
4
+ # Usage: [ENFORCE_EAGER=0] [EXL3_FUSED_MOE=1] [GPU_MEM_UTIL=0.86] ./keys-launch.sh [logname]
5
+ set -u
6
+ cd ~/glm53-mia
7
+ LOG=${1:-launch-tp4-keys.log}
8
+ ./start-tp4.sh stop 2>&1 | tail -1
9
+ for ip in 10.100.10.1 10.100.10.2 10.100.10.3 10.100.10.5; do
10
+ ssh -o BatchMode=yes -o ConnectTimeout=10 "$ip" "sudo sync; echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null; free -g | awk '/Mem:/{print \"\" \" free=\" \$4 \"G avail=\" \$7 \"G\"}'" 2>/dev/null || echo "$ip: drop_caches failed"
11
+ done
12
+ SKIP_PULL=1 SKIP_SHIP=1 SKIP_BUILD=1 SKIP_DOWNLOAD=1 SKIP_SYNC=1 SKIP_OVERLAY_VERIFY=1 \
13
+ ENFORCE_EAGER=${ENFORCE_EAGER:-0} EXL3_FUSED_MOE=${EXL3_FUSED_MOE:-1} \
14
+ setsid nohup ./start-tp4.sh > "$LOG" 2>&1 < /dev/null &
15
+ echo "launched -> $LOG"
serving/mla_attn_dcp.py ADDED
The diff for this file is too large to render. See raw diff
 
serving/patch_ablit_dsv2.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Install Mia's ABLIT load-time hook for FULL GLM-5.3 (GlmMoeDsaForCausalLM -> deepseek_v2.py).
3
+
4
+ Mia's overlay/patch_ablit.py splices the hook into the GLM-5.3-Flash classes (glm5next). The full
5
+ model is served by DeepseekV2Model / DeepSeekMTP, so this splices the same call at the end of their
6
+ load_weights. ablit_runtime.py is reused unchanged: its o_proj regexes already match
7
+ ``model.layers.N.self_attn.o_proj`` and ``layers.N.mtp_block.self_attn.o_proj``. Idempotent.
8
+ Donor set: <ABLIT_DIR>/transplant/L{N}.bin + MANIFEST.json (make_derisk_donor.py output).
9
+ Runtime env: ABLIT=1 ABLIT_METHOD=transplant ABLIT_LAYERS=<ranges> ABLIT_INCLUDE_MTP=1 ABLIT_DIR=/opt/glm53/ablit
10
+ """
11
+ from pathlib import Path
12
+ import shutil, sys
13
+
14
+ SITE = Path("/usr/local/lib/python3.12/dist-packages/vllm")
15
+ MODELS = SITE / "model_executor/models"
16
+ NVIDIA_DIR = SITE / "models/glm5next/nvidia"
17
+ RUNTIME_SRC = Path(sys.argv[1] if len(sys.argv) > 1 else "/opt/glm53/ablit_runtime.py")
18
+ RUNTIME_DST = NVIDIA_DIR / "glm53_ablit.py"
19
+ MARKER = "ABLIT-HOOK-DSV2"
20
+ HOOK = (
21
+ " from vllm.models.glm5next.nvidia.glm53_ablit import maybe_apply "
22
+ "as _glm53_ablit_maybe_apply # ABLIT-HOOK-DSV2\n"
23
+ " _glm53_ablit_maybe_apply(self) # ABLIT-HOOK-DSV2\n"
24
+ )
25
+ MODEL_TAIL = """ if name is not None and not is_fusion_moe_shared_experts_layer:
26
+ loaded_params.add(name)
27
+
28
+ return loaded_params"""
29
+ MTP_TAIL = """ f"or disable speculative decoding."
30
+ )
31
+
32
+ return loaded_params"""
33
+
34
+
35
+ def splice(path: Path, tail: str, label: str) -> None:
36
+ text = path.read_text()
37
+ if MARKER in text:
38
+ print(f"{path.name}: {MARKER} already present")
39
+ return
40
+ n = text.count(tail)
41
+ if n != 1:
42
+ raise SystemExit(f"{path}: expected one patch target for {label}, found {n}")
43
+ new_tail = tail.replace(" return loaded_params", HOOK + " return loaded_params")
44
+ path.write_text(text.replace(tail, new_tail))
45
+ print(f"patched {path.name} ({label})")
46
+
47
+
48
+ NVIDIA_DIR.mkdir(parents=True, exist_ok=True)
49
+ if RUNTIME_SRC.is_file():
50
+ shutil.copyfile(RUNTIME_SRC, RUNTIME_DST)
51
+ print(f"installed {RUNTIME_DST}")
52
+ elif not RUNTIME_DST.is_file():
53
+ raise SystemExit(f"ablit runtime not found at {RUNTIME_SRC}")
54
+ splice(MODELS / "deepseek_v2.py", MODEL_TAIL, "DeepseekV2Model.load_weights")
55
+ splice(MODELS / "deepseek_mtp.py", MTP_TAIL, "DeepSeekMTP.load_weights")
serving/patch_overlay_mtp.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ p=sys.argv[1]; s=open(p).read()
3
+ old=""" def get_quant_method(self, layer: torch.nn.Module, prefix: str):
4
+ from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
5
+
6
+ if isinstance(layer, RoutedExperts):
7
+ return Exl3MoEMethod(layer.moe_config, self)"""
8
+ new=""" def get_quant_method(self, layer: torch.nn.Module, prefix: str):
9
+ from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
10
+
11
+ # KEYS PATCH: the MTP block (model.layers.<num_hidden_layers>+) is stored bf16
12
+ # (-mb 16) -> leave it to the stock unquantized MoE path so SPEC_METHOD=mtp works.
13
+ if isinstance(layer, RoutedExperts) and _is_mtp_prefix(prefix):
14
+ logger.info_once("EXL3: MTP block %s left unquantized (bf16 experts)", prefix)
15
+ return None
16
+ if isinstance(layer, RoutedExperts):
17
+ return Exl3MoEMethod(layer.moe_config, self)"""
18
+ assert s.count(old)==1, s.count(old); s=s.replace(old,new)
19
+ helper='''
20
+
21
+ def _is_mtp_prefix(prefix: str) -> bool:
22
+ """True for layers at index >= num_hidden_layers (DeepSeek/GLM MTP blocks)."""
23
+ import re as _re
24
+ m = _re.search(r"\\.layers\\.(\\d+)\\.", prefix + ".")
25
+ if not m:
26
+ return "mtp" in prefix.lower()
27
+ idx = int(m.group(1))
28
+ n = None
29
+ try:
30
+ from vllm.config import get_current_vllm_config
31
+ n = int(get_current_vllm_config().model_config.hf_config.num_hidden_layers)
32
+ except Exception: # noqa: BLE001
33
+ n = None
34
+ if n is None:
35
+ n = int(os.environ.get("EXL3_MTP_LAYER_START", "78"))
36
+ return idx >= n
37
+ '''
38
+ anchor='\n@register_quantization_config("exl3")'
39
+ assert s.count(anchor)==1, s.count(anchor); s=s.replace(anchor, helper+anchor)
40
+ open(p,"w").write(s); print("mtp exemption patched")
serving/patch_overlay_mul1.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys,re
2
+ src=sys.argv[1]; dst=sys.argv[2]; s=open(src).read()
3
+ def rep(old,new,count=1):
4
+ global s
5
+ assert old in s, f"missing: {old[:70]!r}"
6
+ s=s.replace(old,new,count)
7
+ rep('MCG_MARKER_SIGNED_INT32 = -877912083\nEXL3_SUFFIXES = ("trellis", "suh", "svh", "mcg")',
8
+ '''MCG_MARKER_SIGNED_INT32 = -877912083
9
+ # KEYS PATCH: exllamav3 "mul1" codebook (converter default; 0x83DCD12D marker). LinearEXL3 and
10
+ # exllamav3_ext.exl3_moe in this image already take (mcg, mul1) flag pairs per matrix.
11
+ MUL1_MULTIPLIER = 0x83DCD12D
12
+ MUL1_MARKER_SIGNED_INT32 = -2082680531
13
+ EXL3_SUFFIXES = ("trellis", "suh", "svh", "mcg", "mul1")''')
14
+ rep(''' mcg: torch.Tensor,
15
+ *,
16
+ out_dtype: torch.dtype = torch.float16,
17
+ ):
18
+ """Build a LinearEXL3 over already-sharded packed tensors. No BF16 expand."""
19
+ cls = load_linear_exl3_cls()
20
+ return cls(
21
+ config=None,
22
+ in_features=int(suh.numel()),
23
+ out_features=int(svh.numel()),
24
+ trellis=trellis.contiguous(),
25
+ suh=suh.contiguous(),
26
+ svh=svh.contiguous(),
27
+ mcg=mcg.contiguous(),
28
+ out_dtype=out_dtype,
29
+ transformers_fix=True,
30
+ )''',''' mcg: torch.Tensor,
31
+ *,
32
+ mul1: bool = False,
33
+ out_dtype: torch.dtype = torch.float16,
34
+ ):
35
+ """Build a LinearEXL3 over already-sharded packed tensors. No BF16 expand.
36
+ KEYS PATCH: `mcg` is the codebook marker tensor; with mul1=True it is passed as the
37
+ mul1 marker instead (exllamav3 LinearEXL3 accepts either)."""
38
+ cls = load_linear_exl3_cls()
39
+ marker = mcg.contiguous()
40
+ return cls(
41
+ config=None,
42
+ in_features=int(suh.numel()),
43
+ out_features=int(svh.numel()),
44
+ trellis=trellis.contiguous(),
45
+ suh=suh.contiguous(),
46
+ svh=svh.contiguous(),
47
+ mcg=None if mul1 else marker,
48
+ mul1=marker if mul1 else None,
49
+ out_dtype=out_dtype,
50
+ transformers_fix=True,
51
+ )''')
52
+ rep(''' if self.codebook != "mcg":
53
+ raise ValueError(
54
+ f"this overlay only implements codebook=mcg; got {self.codebook!r}"
55
+ )''',''' if self.codebook not in ("mcg", "mul1", "mixed", "auto"):
56
+ raise ValueError(
57
+ f"this overlay implements codebook mcg/mul1 (per layer); got {self.codebook!r}"
58
+ )''')
59
+ rep(''' w13_mcg = Parameter(
60
+ torch.empty(num_experts, 2, 1, dtype=torch.int32),
61
+ requires_grad=False,
62
+ )''',''' w13_mcg = Parameter(
63
+ torch.zeros(num_experts, 2, 1, dtype=torch.int32),
64
+ requires_grad=False,
65
+ )
66
+ w13_mul1 = Parameter(
67
+ torch.zeros(num_experts, 2, 1, dtype=torch.int32),
68
+ requires_grad=False,
69
+ )''')
70
+ rep(''' w2_mcg = Parameter(
71
+ torch.empty(num_experts, 1, dtype=torch.int32),
72
+ requires_grad=False,
73
+ )''',''' w2_mcg = Parameter(
74
+ torch.zeros(num_experts, 1, dtype=torch.int32),
75
+ requires_grad=False,
76
+ )
77
+ w2_mul1 = Parameter(
78
+ torch.zeros(num_experts, 1, dtype=torch.int32),
79
+ requires_grad=False,
80
+ )''')
81
+ rep(''' "w13_mcg": w13_mcg,
82
+ "w2_trellis": w2_trellis,''',''' "w13_mcg": w13_mcg,
83
+ "w13_mul1": w13_mul1,
84
+ "w2_trellis": w2_trellis,''')
85
+ rep(''' "w2_mcg": w2_mcg,
86
+ }''',''' "w2_mcg": w2_mcg,
87
+ "w2_mul1": w2_mul1,
88
+ }''')
89
+ rep(''' "w13_mcg",
90
+ "w2_trellis",
91
+ "w2_suh",
92
+ "w2_svh",
93
+ "w2_mcg",
94
+ ):
95
+ getattr(layer, name)._exl3_owner = layer
96
+
97
+ mcg13 = layer.w13_mcg.reshape(-1)
98
+ mcg2 = layer.w2_mcg.reshape(-1)
99
+ if not torch.all(mcg13 == MCG_MARKER_SIGNED_INT32) or not torch.all(
100
+ mcg2 == MCG_MARKER_SIGNED_INT32
101
+ ):
102
+ raise RuntimeError(
103
+ "EXL3 mcg marker is not the MCG int32 0xCBAC1FED / "
104
+ f"{MCG_MARKER_SIGNED_INT32}; packed ABI mismatch"
105
+ )
106
+ ''',''' "w13_mcg",
107
+ "w13_mul1",
108
+ "w2_trellis",
109
+ "w2_suh",
110
+ "w2_svh",
111
+ "w2_mcg",
112
+ "w2_mul1",
113
+ ):
114
+ getattr(layer, name)._exl3_owner = layer
115
+
116
+ # KEYS PATCH: per-layer codebook detection from the loaded marker tensors.
117
+ mcg13 = layer.w13_mcg.reshape(-1)
118
+ mcg2 = layer.w2_mcg.reshape(-1)
119
+ mul13 = layer.w13_mul1.reshape(-1)
120
+ mul2 = layer.w2_mul1.reshape(-1)
121
+ is_mcg = bool(torch.all(mcg13 == MCG_MARKER_SIGNED_INT32)) and bool(
122
+ torch.all(mcg2 == MCG_MARKER_SIGNED_INT32)
123
+ )
124
+ is_mul1 = bool(torch.all(mul13 == MUL1_MARKER_SIGNED_INT32)) and bool(
125
+ torch.all(mul2 == MUL1_MARKER_SIGNED_INT32)
126
+ )
127
+ if is_mcg == is_mul1:
128
+ raise RuntimeError(
129
+ "EXL3 codebook markers inconsistent for this layer: "
130
+ f"mcg={is_mcg} mul1={is_mul1} (expect exactly one; "
131
+ f"MCG {MCG_MARKER_SIGNED_INT32} / MUL1 {MUL1_MARKER_SIGNED_INT32})"
132
+ )
133
+ use_mul1 = is_mul1
134
+ layer._exl3_mul1 = use_mul1
135
+ m13 = layer.w13_mul1 if use_mul1 else layer.w13_mcg
136
+ m2 = layer.w2_mul1 if use_mul1 else layer.w2_mcg
137
+ if not getattr(Exl3MoEMethod, "_logged_codebooks", None):
138
+ Exl3MoEMethod._logged_codebooks = set()
139
+ if use_mul1 not in Exl3MoEMethod._logged_codebooks:
140
+ Exl3MoEMethod._logged_codebooks.add(use_mul1)
141
+ logger.info("EXL3 experts codebook detected: %s", "mul1" if use_mul1 else "mcg")
142
+ ''')
143
+ rep(''' gate = make_linear_exl3(
144
+ layer.w13_trellis[e, 0],
145
+ layer.w13_suh[e, 0],
146
+ layer.w13_svh[e, 0],
147
+ layer.w13_mcg[e, 0],
148
+ )
149
+ up = make_linear_exl3(
150
+ layer.w13_trellis[e, 1],
151
+ layer.w13_suh[e, 1],
152
+ layer.w13_svh[e, 1],
153
+ layer.w13_mcg[e, 1],
154
+ )
155
+ down = make_linear_exl3(
156
+ layer.w2_trellis[e],
157
+ layer.w2_suh[e],
158
+ layer.w2_svh[e],
159
+ layer.w2_mcg[e],
160
+ )''',''' gate = make_linear_exl3(
161
+ layer.w13_trellis[e, 0],
162
+ layer.w13_suh[e, 0],
163
+ layer.w13_svh[e, 0],
164
+ m13[e, 0],
165
+ mul1=use_mul1,
166
+ )
167
+ up = make_linear_exl3(
168
+ layer.w13_trellis[e, 1],
169
+ layer.w13_suh[e, 1],
170
+ layer.w13_svh[e, 1],
171
+ m13[e, 1],
172
+ mul1=use_mul1,
173
+ )
174
+ down = make_linear_exl3(
175
+ layer.w2_trellis[e],
176
+ layer.w2_suh[e],
177
+ layer.w2_svh[e],
178
+ m2[e],
179
+ mul1=use_mul1,
180
+ )''')
181
+ rep(''' ptrs["down_svh"],
182
+ True,
183
+ False,
184
+ True,
185
+ False,
186
+ True,
187
+ False,
188
+ float(limit),''',''' ptrs["down_svh"],
189
+ # KEYS PATCH: (mcg, mul1) flag pairs for gate / up / down, per-layer codebook.
190
+ not _m1,
191
+ _m1,
192
+ not _m1,
193
+ _m1,
194
+ not _m1,
195
+ _m1,
196
+ float(limit),''')
197
+ rep(''' k = int(getattr(layer, "_exl3_k", 4))
198
+ args = (''',''' k = int(getattr(layer, "_exl3_k", 4))
199
+ _m1 = bool(getattr(layer, "_exl3_mul1", False))
200
+ args = (''')
201
+ open(dst,"w").write(s); print("overlay patched")
serving/patch_tp4_launcher.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Add LOCAL_MODEL_DIR / LOCAL_DFLASH_DIR support to Mia's start-tp4.sh (serve a local checkpoint dir,
3
+ e.g. an NFS path visible on all four nodes, instead of the HF-cache layout). Idempotent.
4
+ Usage: patch_tp4_launcher.py ~/glm53-mia/start-tp4.sh (then set LOCAL_MODEL_DIR=... in .env.tp4 and run with
5
+ SKIP_DOWNLOAD=1 SKIP_SYNC=1 SKIP_PULL=1)"""
6
+ import sys
7
+ p = sys.argv[1]
8
+ s = open(p).read()
9
+ if "LOCAL_MODEL_DIR" in s:
10
+ print("already patched"); sys.exit(0)
11
+
12
+ # workers: extend the per-rank preload string right before each rank's docker run
13
+ old_w = ''' worker_ssh_n "$r" "docker run -d --name '$cname' \\'''
14
+ new_w = ''' [ -n "${LOCAL_MODEL_DIR:-}" ] && worker_preload="$worker_preload -v '$LOCAL_MODEL_DIR:/model:ro'"
15
+ [ -n "${LOCAL_DFLASH_DIR:-}" ] && worker_preload="$worker_preload -v '$LOCAL_DFLASH_DIR:/draft:ro'"
16
+ worker_ssh_n "$r" "docker run -d --name '$cname' \\'''
17
+ assert s.count(old_w) == 1, s.count(old_w)
18
+ s = s.replace(old_w, new_w, 1)
19
+
20
+ # head
21
+ old_h = ''' docker run -d --name "$CONTAINER_HEAD" \\'''
22
+ new_h = ''' [ -n "${LOCAL_MODEL_DIR:-}" ] && head_preload+=(-v "$LOCAL_MODEL_DIR:/model:ro")
23
+ [ -n "${LOCAL_DFLASH_DIR:-}" ] && head_preload+=(-v "$LOCAL_DFLASH_DIR:/draft:ro")
24
+ docker run -d --name "$CONTAINER_HEAD" \\'''
25
+ assert s.count(old_h) == 1, s.count(old_h)
26
+ s = s.replace(old_h, new_h, 1)
27
+
28
+ # in-container model paths
29
+ old_m = ''' MODEL_DIR="$(resolve_model_dir)"
30
+ DFLASH_MODEL_DIR=""
31
+ if [ "$SPEC_METHOD" = "dflash" ]; then
32
+ DFLASH_MODEL_DIR="$(resolve_dflash_dir)"'''
33
+ new_m = ''' if [ -n "${LOCAL_MODEL_DIR:-}" ]; then
34
+ MODEL_DIR="/model"
35
+ log "LOCAL_MODEL_DIR mode: $LOCAL_MODEL_DIR -> /model on all ranks"
36
+ else
37
+ MODEL_DIR="$(resolve_model_dir)"
38
+ fi
39
+ DFLASH_MODEL_DIR=""
40
+ if [ "$SPEC_METHOD" = "dflash" ]; then
41
+ if [ -n "${LOCAL_DFLASH_DIR:-}" ]; then
42
+ DFLASH_MODEL_DIR="/draft"
43
+ else
44
+ DFLASH_MODEL_DIR="$(resolve_dflash_dir)"
45
+ fi'''
46
+ assert s.count(old_m) == 1, s.count(old_m)
47
+ s = s.replace(old_m, new_m, 1)
48
+ open(p, "w").write(s)
49
+ print("patched", p)
serving/sm120_dcp.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+ """SM120 implementation variant for ``FLASHINFER_MLA_SPARSE_SM120``.
4
+
5
+ keys-DCP port (2026-09-01): adds Decode Context Parallel (KV sharded across the TP ranks)
6
+ support, mirroring the SM100 ``FlashInferMLASparseImpl`` branch:
7
+ * ``can_return_lse_for_decode = True`` so the CP compatibility check passes and the layer
8
+ combines per-rank partial attention with ``MLADCPManager.combine``;
9
+ * under DCP the global top-k token indices are filtered to this rank's local KV slots with
10
+ ``triton_filter_and_convert_dcp_index`` (interleave-aware);
11
+ * the flashinfer SM120 sparse kernel is asked for the softmax LSE (``return_lse``), which is
12
+ normalized to ``(num_tokens, num_heads)``; rows with no local top-k tokens get output 0 and
13
+ LSE ``-inf`` so they drop out of the cross-rank softmax merge;
14
+ * the output is allocated from ``q.shape[1]`` (all heads after the DCP query all-gather),
15
+ not the layer's TP-local head count.
16
+ ``EXL3_SM120_LSE_BASE_E`` (default 1) selects the LSE base reported to the combine kernel; the
17
+ SM120 sparse kernel follows the FlashMLA (natural-log) convention. Set 0 for base-2.
18
+ """
19
+
20
+ import os
21
+ from typing import TYPE_CHECKING, cast
22
+
23
+ import torch
24
+
25
+ from vllm.v1.attention.backend import (
26
+ AttentionLayer,
27
+ AttentionType,
28
+ MLAAttentionImpl,
29
+ )
30
+ from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
31
+ FlashInferMLASparseMetadata,
32
+ _get_workspace_buffer,
33
+ )
34
+ from vllm.v1.attention.backends.mla.sparse_utils import (
35
+ triton_convert_req_index_to_global_index,
36
+ triton_filter_and_convert_dcp_index,
37
+ )
38
+
39
+ if TYPE_CHECKING:
40
+ from vllm.model_executor.models.deepseek_v2 import Indexer
41
+
42
+
43
+ # The layer's DCP combine reads ``attn_metadata.decode``; the sparse metadata has no such field.
44
+ if not hasattr(FlashInferMLASparseMetadata, "decode"):
45
+ FlashInferMLASparseMetadata.decode = None # type: ignore[attr-defined]
46
+
47
+
48
+ def _kv_scale_format_for_model(model_type: str | None) -> str:
49
+ if model_type is not None and model_type.startswith("glm"):
50
+ return "arbitrary_fp32"
51
+ return "pow2_fp32"
52
+
53
+
54
+ class FlashInferMLASparseSM120Impl(MLAAttentionImpl[FlashInferMLASparseMetadata]):
55
+ """SM120 FlashInfer sparse-MLA implementation (+ DCP)."""
56
+
57
+ is_sparse = True
58
+ supports_dense_mha_prefill = False
59
+ # DCP: we can hand the softmax LSE to the cross-rank combine.
60
+ can_return_lse_for_decode = True
61
+ lse_base_on_e = os.environ.get("EXL3_SM120_LSE_BASE_E", "1") == "1"
62
+
63
+ def __init__(
64
+ self,
65
+ num_heads: int,
66
+ head_size: int,
67
+ scale: float,
68
+ num_kv_heads: int,
69
+ alibi_slopes: list[float] | None,
70
+ sliding_window: int | None,
71
+ kv_cache_dtype: str,
72
+ logits_soft_cap: float | None,
73
+ attn_type: str,
74
+ kv_sharing_target_layer_name: str | None,
75
+ indexer: "Indexer | None" = None,
76
+ **mla_args,
77
+ ) -> None:
78
+ if any([alibi_slopes, sliding_window, logits_soft_cap]):
79
+ raise NotImplementedError(
80
+ "FLASHINFER_MLA_SPARSE_SM120 does not support alibi_slopes / "
81
+ "sliding_window / logits_soft_cap"
82
+ )
83
+ if attn_type != AttentionType.DECODER:
84
+ raise NotImplementedError(
85
+ "FLASHINFER_MLA_SPARSE_SM120 only supports decoder self-attention"
86
+ )
87
+
88
+ self.num_heads = num_heads
89
+ self.head_size = head_size
90
+ self.scale = float(scale)
91
+ self.num_kv_heads = num_kv_heads
92
+ self.kv_cache_dtype = kv_cache_dtype
93
+ if self.kv_cache_dtype != "fp8_ds_mla":
94
+ raise NotImplementedError(
95
+ "FLASHINFER_MLA_SPARSE_SM120 requires the packed fp8_ds_mla "
96
+ f"KV cache layout; got kv_cache_dtype={kv_cache_dtype!r}."
97
+ )
98
+
99
+ self.kv_lora_rank: int = mla_args["kv_lora_rank"]
100
+ self.qk_nope_head_dim: int = mla_args["qk_nope_head_dim"]
101
+ self.qk_rope_head_dim: int = mla_args["qk_rope_head_dim"]
102
+ self.rope_pad = 0
103
+ if self.qk_rope_head_dim == 0:
104
+ if self.kv_lora_rank != 512:
105
+ raise NotImplementedError(
106
+ "FLASHINFER_MLA_SPARSE_SM120 pads NoPE MLA into the "
107
+ "576-wide GLM_NSA geometry, which requires "
108
+ f"kv_lora_rank=512; got {self.kv_lora_rank}."
109
+ )
110
+ self.rope_pad = 64
111
+ self.kernel_qk_rope_head_dim = self.qk_rope_head_dim + self.rope_pad
112
+ from vllm.config import get_current_vllm_config
113
+
114
+ vllm_config = get_current_vllm_config()
115
+ model_type = None
116
+ if vllm_config.model_config is not None:
117
+ model_type = getattr(
118
+ vllm_config.model_config.hf_text_config, "model_type", None
119
+ )
120
+ self.kv_scale_format = _kv_scale_format_for_model(model_type)
121
+
122
+ # Skip-topk layers are built with indexer=None and get the shared
123
+ # buffer via mla_args instead (cf. FLASHMLA_SPARSE).
124
+ self.topk_indices_buffer: torch.Tensor | None = (
125
+ indexer.topk_indices_buffer
126
+ if indexer is not None
127
+ else mla_args.get("topk_indices_buffer")
128
+ )
129
+ from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
130
+
131
+ if not has_flashinfer_sparse_mla_sm120():
132
+ raise RuntimeError(
133
+ "FLASHINFER_MLA_SPARSE_SM120 requires FlashInfer's "
134
+ "sparse MLA decode API."
135
+ )
136
+ assert self.topk_indices_buffer is not None
137
+
138
+ self.supports_quant_query_input = False
139
+ self._workspace_buffer: torch.Tensor | None = None
140
+ self._dcp_logged = False
141
+
142
+ @staticmethod
143
+ def _normalize_lse(lse: torch.Tensor, num_tokens: int, num_heads: int) -> torch.Tensor:
144
+ # (num_tokens, num_heads) is what the shared DCP reducer expects; the kernel may
145
+ # hand back (T, H, 1) / (T, 1, H) / (B, q_len, H).
146
+ if lse.dim() == 3:
147
+ if lse.shape[-1] == 1:
148
+ lse = lse.squeeze(-1)
149
+ elif lse.shape[1] == 1:
150
+ lse = lse.squeeze(1)
151
+ elif lse.shape[0] * lse.shape[1] == num_tokens:
152
+ lse = lse.reshape(num_tokens, lse.shape[-1])
153
+ if lse.shape != (num_tokens, num_heads):
154
+ raise RuntimeError(
155
+ "Unexpected SM120 sparse MLA LSE shape: "
156
+ f"{tuple(lse.shape)}, expected ({num_tokens}, {num_heads})."
157
+ )
158
+ return lse
159
+
160
+ def forward_mqa(
161
+ self,
162
+ q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
163
+ kv_c_and_k_pe_cache: torch.Tensor,
164
+ attn_metadata: FlashInferMLASparseMetadata,
165
+ layer: AttentionLayer,
166
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
167
+ if isinstance(q, tuple):
168
+ q = torch.cat(q, dim=-1)
169
+ if self.rope_pad:
170
+ q = torch.nn.functional.pad(q, (0, self.rope_pad))
171
+
172
+ num_actual_toks = q.shape[0]
173
+ # Under DCP the layer all-gathers queries across the CP ranks first, so q carries
174
+ # every head (not just this rank's TP slice).
175
+ num_heads = q.shape[1]
176
+
177
+ assert self.topk_indices_buffer is not None
178
+ topk_indices = self.topk_indices_buffer[:num_actual_toks]
179
+
180
+ use_dcp = self.dcp_world_size > 1
181
+ if use_dcp:
182
+ if not self._dcp_logged:
183
+ from vllm.logger import init_logger
184
+
185
+ init_logger(__name__).info(
186
+ "[keys-DCP] SM120 sparse MLA: dcp_world=%d rank=%d interleave=%d "
187
+ "heads=%d lse_base_e=%s",
188
+ self.dcp_world_size,
189
+ self.dcp_rank,
190
+ attn_metadata.cp_kv_cache_interleave_size,
191
+ num_heads,
192
+ self.lse_base_on_e,
193
+ )
194
+ self._dcp_logged = True
195
+ topk_indices_physical, topk_lengths = cast(
196
+ tuple[torch.Tensor, torch.Tensor],
197
+ triton_filter_and_convert_dcp_index(
198
+ attn_metadata.req_id_per_token[:num_actual_toks],
199
+ attn_metadata.block_table,
200
+ topk_indices,
201
+ dcp_size=self.dcp_world_size,
202
+ dcp_rank=self.dcp_rank,
203
+ cp_kv_cache_interleave_size=attn_metadata.cp_kv_cache_interleave_size,
204
+ BLOCK_SIZE=attn_metadata.block_size,
205
+ NUM_TOPK_TOKENS=topk_indices.shape[1],
206
+ return_valid_counts=True,
207
+ ),
208
+ )
209
+ else:
210
+ topk_indices_physical, topk_lengths = cast(
211
+ tuple[torch.Tensor, torch.Tensor],
212
+ triton_convert_req_index_to_global_index(
213
+ attn_metadata.req_id_per_token[:num_actual_toks],
214
+ attn_metadata.block_table,
215
+ topk_indices,
216
+ BLOCK_SIZE=attn_metadata.block_size,
217
+ NUM_TOPK_TOKENS=topk_indices.shape[1],
218
+ return_valid_counts=True,
219
+ ),
220
+ )
221
+ sparse_topk_capacity = topk_indices_physical.shape[1]
222
+ empty_rows = topk_lengths == 0
223
+ topk_indices_physical[:, 0] = topk_indices_physical[:, 0].masked_fill(
224
+ empty_rows, 0
225
+ )
226
+ topk_lengths = topk_lengths.clamp(min=1)
227
+
228
+ output = q.new_empty(
229
+ (num_actual_toks, num_heads, self.kv_lora_rank),
230
+ dtype=q.dtype,
231
+ )
232
+
233
+ if self._workspace_buffer is None:
234
+ self._workspace_buffer = _get_workspace_buffer(q.device)
235
+
236
+ from vllm.utils.flashinfer import (
237
+ flashinfer_trtllm_batch_decode_with_kv_cache_mla,
238
+ )
239
+
240
+ want_lse = bool(self.need_to_return_lse_for_decode)
241
+ kernel_out = flashinfer_trtllm_batch_decode_with_kv_cache_mla(
242
+ query=q.unsqueeze(1),
243
+ kv_cache=kv_c_and_k_pe_cache.view(torch.uint8).unsqueeze(1),
244
+ workspace_buffer=self._workspace_buffer,
245
+ qk_nope_head_dim=self.qk_nope_head_dim,
246
+ kv_lora_rank=self.kv_lora_rank,
247
+ qk_rope_head_dim=self.kernel_qk_rope_head_dim,
248
+ block_tables=topk_indices_physical.unsqueeze(1),
249
+ seq_lens=topk_lengths,
250
+ max_seq_len=sparse_topk_capacity,
251
+ out=output.unsqueeze(1),
252
+ bmm1_scale=self.scale,
253
+ bmm2_scale=1.0,
254
+ sparse_mla_top_k=sparse_topk_capacity,
255
+ kv_scale_format=self.kv_scale_format,
256
+ return_lse=want_lse,
257
+ )
258
+ lse: torch.Tensor | None = None
259
+ if want_lse:
260
+ assert isinstance(kernel_out, tuple), "SM120 sparse MLA did not return LSE"
261
+ out, lse = kernel_out
262
+ else:
263
+ out = kernel_out
264
+ out = out.view(-1, out.shape[-2], out.shape[-1])
265
+ out.masked_fill_(empty_rows.view(-1, 1, 1), 0.0)
266
+ if lse is not None:
267
+ lse = self._normalize_lse(lse, out.shape[0], out.shape[1])
268
+ lse = lse.masked_fill(empty_rows.view(-1, 1), float("-inf"))
269
+ return out, lse
270
+
271
+ def do_kv_cache_update(
272
+ self,
273
+ kv_c_normed: torch.Tensor,
274
+ k_pe: torch.Tensor,
275
+ kv_cache: torch.Tensor,
276
+ slot_mapping: torch.Tensor,
277
+ kv_cache_dtype: str,
278
+ k_scale: torch.Tensor,
279
+ ) -> None:
280
+ if self.rope_pad:
281
+ k_pe = k_pe.new_zeros((k_pe.shape[0], 1, self.rope_pad))
282
+ super().do_kv_cache_update(
283
+ kv_c_normed, k_pe, kv_cache, slot_mapping, kv_cache_dtype, k_scale
284
+ )
serving/start-tp4.sh ADDED
@@ -0,0 +1,1776 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # start-tp4.sh — EXPERIMENTAL 4× DGX Spark TP=4 launcher for GLM-5.3-Flash EXL3
4
+ # ============================================================================
5
+ #
6
+ # Optional sibling of start.sh. Does not change the supported 2× TP=2 path.
7
+ # Untested here (no 4-Spark kit). A user with four GB10s can try:
8
+ #
9
+ # ./start-tp4.sh
10
+ #
11
+ # Layout (mp executor, not Ray):
12
+ # rank 0 HEAD_IP (default 10.0.0.1) — vLLM API on :8888
13
+ # rank 1 WORKER_IP (default 10.0.0.2) — --headless
14
+ # rank 2 WORKER2_IP (default 10.0.0.3) — --headless
15
+ # rank 3 WORKER3_IP (default 10.0.0.4) — --headless
16
+ # --tensor-parallel-size 4 --nnodes 4
17
+ #
18
+ # Fabric: four Sparks need a RoCE path among all ranks (QSFP ring using both
19
+ # CX7 ports, or a switch). Defaults set NCCL_CROSS_NIC=1. Pin per-rank
20
+ # WORKER2_CX7_IF / WORKER2_CX7_IB / WORKER2_GID (same for 3) in .env.
21
+ # Dual-port example: RANK NCCL_IB_HCA="rocep1s0f0,rocep1s0f1".
22
+ #
23
+ # Same image/weights as start.sh. Container names are glm53-exl3-tp4-* so a
24
+ # TP=2 serve is not accidentally reused. Stop with ./start-tp4.sh stop
25
+ # (./start.sh stop does not know about ranks 2/3).
26
+ #
27
+ # Usage:
28
+ # ./start-tp4.sh start TP=4
29
+ # ./start-tp4.sh download head HF cache only
30
+ # ./start-tp4.sh stop|restart|status
31
+ # ./start-tp4.sh logs follow head
32
+ # ./start-tp4.sh logs 1|2|3 follow that worker rank
33
+ #
34
+ # Extra knobs live in .env.tp4 (copied from .env.tp4.example). start.sh
35
+ # never reads that file. Shared tokens/IPs can stay in .env.
36
+ # ============================================================================
37
+ set -euo pipefail
38
+
39
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
40
+
41
+ if [ ! -f "$SCRIPT_DIR/.env" ]; then
42
+ [ -f "$SCRIPT_DIR/.env.example" ] || {
43
+ echo "ERROR: missing .env.example" >&2
44
+ exit 1
45
+ }
46
+ cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env"
47
+ printf '\033[1;36m[glm53-exl3-tp4]\033[0m wrote .env from .env.example\n'
48
+ fi
49
+ if [ ! -f "$SCRIPT_DIR/.env.tp4" ]; then
50
+ [ -f "$SCRIPT_DIR/.env.tp4.example" ] || {
51
+ echo "ERROR: missing .env.tp4.example" >&2
52
+ exit 1
53
+ }
54
+ cp "$SCRIPT_DIR/.env.tp4.example" "$SCRIPT_DIR/.env.tp4"
55
+ printf '\033[1;36m[glm53-exl3-tp4]\033[0m wrote .env.tp4 from .env.tp4.example — edit WORKER2_IP / WORKER3_IP / CX7 pins\n'
56
+ fi
57
+ # Caller exports (MTP_TOKENS=2 ./start.sh restart) must win over .env.
58
+ _cli_mtp="${MTP_TOKENS-}"
59
+ _cli_spec="${SPEC_METHOD-}"
60
+ _cli_eager="${ENFORCE_EAGER-}"
61
+ _cli_fused="${EXL3_FUSED_MOE-}"
62
+ _cli_row_tile="${EXL3_MOE_ROW_TILE-}"
63
+ _cli_temp_rows="${EXL3_TEMP_ROWS_FUSED-}"
64
+ _cli_fat_sorted="${EXL3_FAT_SORTED-}"
65
+ _cli_fat_batched="${EXL3_FAT_BATCHED-}"
66
+ _cli_fat_kernel="${EXL3_FAT_KERNEL-}"
67
+ _cli_mnbt="${MAX_NUM_BATCHED_TOKENS-}"
68
+ _cli_image="${IMAGE-}"
69
+ _cli_util="${GPU_MEM_UTIL-}"
70
+ _cli_lm="${LANGUAGE_MODEL_ONLY-}"
71
+ _cli_max_num_seqs="${MAX_NUM_SEQS-}"
72
+ _cli_ablit="${ABLIT-}"
73
+ _cli_ablit_method="${ABLIT_METHOD-}"
74
+ _cli_ablit_direction="${ABLIT_DIRECTION-}"
75
+ _cli_ablit_layers="${ABLIT_LAYERS-}"
76
+ _cli_ablit_alpha="${ABLIT_ALPHA-}"
77
+ _cli_ablit_mtp="${ABLIT_INCLUDE_MTP-}"
78
+ # Setness-aware: an explicitly empty caller value is an operator error and
79
+ # must reach validate_numeric_config, not be swallowed by a .env value.
80
+ _cli_indexer_workspace_set="${GLM53_INDEXER_WORKSPACE+1}"
81
+ _cli_indexer_workspace="${GLM53_INDEXER_WORKSPACE-}"
82
+ _cli_spinwait_ms_set="${GLM53_SPINWAIT_MS+1}"
83
+ _cli_spinwait_ms="${GLM53_SPINWAIT_MS-}"
84
+ set -a
85
+ # shellcheck disable=SC1091
86
+ source "$SCRIPT_DIR/.env"
87
+ # TP=4 overlay wins over the 2× knobs in .env.
88
+ # shellcheck disable=SC1091
89
+ source "$SCRIPT_DIR/.env.tp4"
90
+ set +a
91
+ [ -n "${_cli_mtp}" ] && MTP_TOKENS="$_cli_mtp"
92
+ [ -n "${_cli_spec}" ] && SPEC_METHOD="$_cli_spec"
93
+ [ -n "${_cli_eager}" ] && ENFORCE_EAGER="$_cli_eager"
94
+ [ -n "${_cli_fused}" ] && EXL3_FUSED_MOE="$_cli_fused"
95
+ [ -n "${_cli_row_tile}" ] && EXL3_MOE_ROW_TILE="$_cli_row_tile"
96
+ [ -n "${_cli_temp_rows}" ] && EXL3_TEMP_ROWS_FUSED="$_cli_temp_rows"
97
+ [ -n "${_cli_fat_sorted}" ] && EXL3_FAT_SORTED="$_cli_fat_sorted"
98
+ [ -n "${_cli_fat_batched}" ] && EXL3_FAT_BATCHED="$_cli_fat_batched"
99
+ [ -n "${_cli_fat_kernel}" ] && EXL3_FAT_KERNEL="$_cli_fat_kernel"
100
+ [ -n "${_cli_mnbt}" ] && MAX_NUM_BATCHED_TOKENS="$_cli_mnbt"
101
+ [ -n "${_cli_image}" ] && IMAGE="$_cli_image"
102
+ [ -n "${_cli_util}" ] && GPU_MEM_UTIL="$_cli_util"
103
+ [ -n "${_cli_lm}" ] && LANGUAGE_MODEL_ONLY="$_cli_lm"
104
+ [ -n "${_cli_max_num_seqs}" ] && MAX_NUM_SEQS="$_cli_max_num_seqs"
105
+ [ -n "${_cli_ablit}" ] && ABLIT="$_cli_ablit"
106
+ [ -n "${_cli_ablit_method}" ] && ABLIT_METHOD="$_cli_ablit_method"
107
+ [ -n "${_cli_ablit_direction}" ] && ABLIT_DIRECTION="$_cli_ablit_direction"
108
+ [ -n "${_cli_ablit_layers}" ] && ABLIT_LAYERS="$_cli_ablit_layers"
109
+ [ -n "${_cli_ablit_alpha}" ] && ABLIT_ALPHA="$_cli_ablit_alpha"
110
+ [ -n "${_cli_ablit_mtp}" ] && ABLIT_INCLUDE_MTP="$_cli_ablit_mtp"
111
+ [ -n "${_cli_indexer_workspace_set}" ] && GLM53_INDEXER_WORKSPACE="$_cli_indexer_workspace"
112
+ [ -n "${_cli_spinwait_ms_set}" ] && GLM53_SPINWAIT_MS="$_cli_spinwait_ms"
113
+
114
+ # ----------------------------- configuration -------------------------------
115
+ MODEL="${MODEL:-Mia-AiLab/GLM-5.3-Flash-EXL3-TR3-4bpw}"
116
+ # If the durable mirror is empty/moved, download.sh falls back to this id.
117
+ MODEL_FALLBACK="${MODEL_FALLBACK:-brandonmusic/GLM-5.3-Flash-tr3-4bpw}"
118
+ MODEL_CACHE_NAME="${MODEL_CACHE_NAME:-models--${MODEL//\//--}}"
119
+ MODEL_FALLBACK_CACHE_NAME="${MODEL_FALLBACK_CACHE_NAME:-models--${MODEL_FALLBACK//\//--}}"
120
+ # Hub commit on the Mia-AiLab mirror (the 5ab363a8-byte-identical upload).
121
+ MODEL_REVISION="${MODEL_REVISION:-25a44fdbf16862a46b7cc9921142c6c81350af2f}"
122
+ IMAGE="${IMAGE:-ghcr.io/miaai-lab/glm-5.3-flash-2x-dgx-sparks:exl3}"
123
+ SERVED_MODEL_NAME="${SERVED_MODEL_NAME:-GLM-5.3-Flash-EXL3}"
124
+ GHCR_USER="${GHCR_USER:-MiaAI-Lab}"
125
+
126
+ HEAD_IP="${HEAD_IP:-10.0.0.1}"
127
+ WORKER_IP="${WORKER_IP:-10.0.0.2}"
128
+ # Same OS user on both Sparks unless .env sets WORKER_USER (mixed-account kits).
129
+ WORKER_USER="${WORKER_USER:-$USER}"
130
+ if [ "$WORKER_USER" = "$USER" ]; then
131
+ WORKER_HOME="${WORKER_HOME:-$HOME}"
132
+ else
133
+ WORKER_HOME="${WORKER_HOME:-/home/${WORKER_USER}}"
134
+ fi
135
+ WORKER_SSH="${WORKER_SSH:-${WORKER_USER}@${WORKER_IP}}"
136
+
137
+ # Ranks 2 and 3 (experimental TP=4). Same OS user as WORKER_USER unless set.
138
+ WORKER2_IP="${WORKER2_IP:-10.0.0.3}"
139
+ WORKER3_IP="${WORKER3_IP:-10.0.0.4}"
140
+ WORKER2_USER="${WORKER2_USER:-$WORKER_USER}"
141
+ WORKER3_USER="${WORKER3_USER:-$WORKER_USER}"
142
+ if [ "$WORKER2_USER" = "$USER" ]; then
143
+ WORKER2_HOME="${WORKER2_HOME:-$HOME}"
144
+ else
145
+ WORKER2_HOME="${WORKER2_HOME:-/home/${WORKER2_USER}}"
146
+ fi
147
+ if [ "$WORKER3_USER" = "$USER" ]; then
148
+ WORKER3_HOME="${WORKER3_HOME:-$HOME}"
149
+ else
150
+ WORKER3_HOME="${WORKER3_HOME:-/home/${WORKER3_USER}}"
151
+ fi
152
+ WORKER2_SSH="${WORKER2_SSH:-${WORKER2_USER}@${WORKER2_IP}}"
153
+ WORKER3_SSH="${WORKER3_SSH:-${WORKER3_USER}@${WORKER3_IP}}"
154
+
155
+ HEAD_CX7_IF="${HEAD_CX7_IF:-enp1s0f1np1}"
156
+ WORKER_CX7_IF="${WORKER_CX7_IF:-enp1s0f0np0}"
157
+ HEAD_CX7_IB="${HEAD_CX7_IB:-rocep1s0f1}"
158
+ WORKER_CX7_IB="${WORKER_CX7_IB:-rocep1s0f0}"
159
+ WORKER2_CX7_IF="${WORKER2_CX7_IF:-$WORKER_CX7_IF}"
160
+ WORKER2_CX7_IB="${WORKER2_CX7_IB:-$WORKER_CX7_IB}"
161
+ WORKER3_CX7_IF="${WORKER3_CX7_IF:-$WORKER_CX7_IF}"
162
+ WORKER3_CX7_IB="${WORKER3_CX7_IB:-$WORKER_CX7_IB}"
163
+ NCCL_DEBUG="${NCCL_DEBUG:-WARN}"
164
+ NCCL_IB_GID_INDEX="${NCCL_IB_GID_INDEX:-3}"
165
+ # The RoCEv2 GID index is per-NIC: the usable entry is the one whose GID matches
166
+ # that node's own fabric IP. Most pairs share a good index; some do not (this kit
167
+ # needs head=4, worker=3). Unset, both inherit NCCL_IB_GID_INDEX -> unchanged.
168
+ HEAD_GID="${HEAD_GID:-$NCCL_IB_GID_INDEX}"
169
+ WORKER_GID="${WORKER_GID:-$NCCL_IB_GID_INDEX}"
170
+ WORKER2_GID="${WORKER2_GID:-$NCCL_IB_GID_INDEX}"
171
+ WORKER3_GID="${WORKER3_GID:-$NCCL_IB_GID_INDEX}"
172
+ # vLLM subtracts a CUDA-graph memory ESTIMATE from the KV pool. On this kit the
173
+ # estimate is 2.43 GiB while the captured graphs actually consume -0.19 GiB, so
174
+ # ~2.6 GiB of KV is reserved and never used. 0 keeps CUDA graphs ON and drops only
175
+ # the deduction. 1 = upstream default.
176
+ CG_ESTIMATE="${CG_ESTIMATE:-1}"
177
+ NCCL_CROSS_NIC="${NCCL_CROSS_NIC:-1}"
178
+ NCCL_HOST_DIR="${NCCL_HOST_DIR:-$HOME/nccl-2.30.7}"
179
+ WORKER_NCCL_HOST_DIR="${WORKER_NCCL_HOST_DIR:-$WORKER_HOME/nccl-2.30.7}"
180
+ WORKER2_NCCL_HOST_DIR="${WORKER2_NCCL_HOST_DIR:-$WORKER2_HOME/nccl-2.30.7}"
181
+ WORKER3_NCCL_HOST_DIR="${WORKER3_NCCL_HOST_DIR:-$WORKER3_HOME/nccl-2.30.7}"
182
+ NCCL_SO_NAME="${NCCL_SO_NAME:-libnccl.so.2.30.7}"
183
+ # glm53-flash already ships nvidia-nccl. LD_PRELOAD of the host 2.30.7 SO
184
+ # makes DeepEP assert duplicate NCCL (/nccl/... vs nvidia/nccl/lib/...).
185
+ # Set USE_HOST_NCCL=1 only if image NCCL cannot talk CX7.
186
+ USE_HOST_NCCL="${USE_HOST_NCCL:-0}"
187
+
188
+ TP="${TP:-4}"
189
+ NNODES="${NNODES:-4}"
190
+ PORT="${PORT:-8888}"
191
+ MASTER_PORT="${MASTER_PORT:-29521}"
192
+
193
+ MTP_TOKENS="${MTP_TOKENS:-2}"
194
+ # dflash (default, incoai/GLM-5.3-Flash-DFlash2, k=7) | mtp | none
195
+ SPEC_METHOD="${SPEC_METHOD:-dflash}"
196
+ DFLASH_MODEL="${DFLASH_MODEL:-incoai/GLM-5.3-Flash-DFlash2}"
197
+ DFLASH_CACHE_NAME="${DFLASH_CACHE_NAME:-models--${DFLASH_MODEL//\//--}}"
198
+ DFLASH_TOKENS="${DFLASH_TOKENS:-7}"
199
+ # 2 = shard the ~2.3 GiB DFlash2 drafter across TP (C4 keep, 2026-08-30:
200
+ # idle 8k 938 / 16k 972 / 100k 997; decode structured 65.1 / prose 27.1).
201
+ # 1 = rank 0 only (no CX7 on every draft step). Empty = inherit target TP.
202
+ # Do not pin attention_backend: SM121 already prefers FLASH_ATTN for
203
+ # non-causal dense SWA. TRITON_ATTN was an SM120 mask-fix this image lacks.
204
+ DFLASH_DRAFT_TP="${DFLASH_DRAFT_TP-4}"
205
+ MAX_MODEL_LEN="${MAX_MODEL_LEN:-1000000}"
206
+ GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.87}"
207
+ MAX_NUM_SEQS="${MAX_NUM_SEQS:-4}"
208
+ # 8192 chunk × long history oversubscribes GB10 persistent_topk smem (300k crash).
209
+ # E2 one-shot 2026-09-01: 7168 keep (100k ~1148 / 300k ~1107); 2048/3548 similar or slower.
210
+ MAX_NUM_BATCHED_TOKENS="${MAX_NUM_BATCHED_TOKENS:-7168}"
211
+ CHAT_TEMPLATE_HOST="${CHAT_TEMPLATE_HOST:-$SCRIPT_DIR/files/chat_template.jinja}"
212
+ CHAT_TEMPLATE="${CHAT_TEMPLATE:-/opt/glm53/chat_template.jinja}"
213
+ VIDEO_PATCH_HOST="${VIDEO_PATCH_HOST:-$SCRIPT_DIR/overlay/patch_glm_video_placeholders.py}"
214
+ STOP_PATCH_HOST="${STOP_PATCH_HOST:-$SCRIPT_DIR/overlay/patch_suppress_stops_in_reasoning.py}"
215
+ SCHED_PATCH_HOST="${SCHED_PATCH_HOST:-$SCRIPT_DIR/overlay/patch_scheduler_decode_floor.py}"
216
+ DRAFTER_PATCH_HOST="${DRAFTER_PATCH_HOST:-$SCRIPT_DIR/overlay/patch_glm5_drafter_group.py}"
217
+ APC_PATCH_HOST="${APC_PATCH_HOST:-$SCRIPT_DIR/overlay/patch_hybrid_prefix_hit.py}"
218
+ XGRAMMAR_PATCH_HOST="${XGRAMMAR_PATCH_HOST:-$SCRIPT_DIR/overlay/patch_xgrammar_termination.py}"
219
+ KPOOL_TAIL_PATCH_HOST="${KPOOL_TAIL_PATCH_HOST:-$SCRIPT_DIR/overlay/patch_kpool_tail_slotmap.py}"
220
+ SPINWAIT_PATCH_HOST="${SPINWAIT_PATCH_HOST:-$SCRIPT_DIR/overlay/patch_spinwait.py}"
221
+ KV_CACHE_DTYPE="${KV_CACHE_DTYPE:-fp8}"
222
+ QUANTIZATION="${QUANTIZATION:-exl3}"
223
+ LANGUAGE_MODEL_ONLY="${LANGUAGE_MODEL_ONLY:-0}"
224
+ SKIP_MM_PROFILING="${SKIP_MM_PROFILING:-1}"
225
+ # JSON default cannot sit in ${LIMIT_MM:-{...}} — } ends the expansion.
226
+ if [ -z "${LIMIT_MM:-}" ]; then
227
+ LIMIT_MM='{"image":4,"video":1}'
228
+ fi
229
+ TORCH_CUDA_ARCH_LIST="${TORCH_CUDA_ARCH_LIST:-12.1a}"
230
+ FLASHINFER_CUDA_ARCH_LIST="${FLASHINFER_CUDA_ARCH_LIST:-12.1a}"
231
+ # Graph-safe fused apply (device-side expert grouping). MTP k=2 decode is
232
+ # 1..4 seqs × 3 tokens (must include 3). DFlash2 k=7 is 1..4 seqs × 8 tokens
233
+ # (must include 8, 16, 24, 32).
234
+ ENFORCE_EAGER="${ENFORCE_EAGER:-0}"
235
+ if [ "${ENFORCE_EAGER}" != "1" ]; then
236
+ case " ${EXTRA_ARGS:-} " in
237
+ *" --cudagraph-capture-sizes "*|*" cudagraph-capture-sizes "*) ;;
238
+ *)
239
+ if [ "$SPEC_METHOD" = "dflash" ]; then
240
+ EXTRA_ARGS="${EXTRA_ARGS:+$EXTRA_ARGS }--cudagraph-capture-sizes 1 2 4 8 16 24 32"
241
+ else
242
+ EXTRA_ARGS="${EXTRA_ARGS:+$EXTRA_ARGS }--cudagraph-capture-sizes 1 2 3 4 6 8 12"
243
+ fi
244
+ ;;
245
+ esac
246
+ fi
247
+ # 1 = fused exl3_moe (decode). 0 restores the unique-expert LinearEXL3 loop.
248
+ EXL3_FUSED_MOE="${EXL3_FUSED_MOE:-1}"
249
+ # 1 = GPU row tiles for fat experts (prefill). 0 = LinearEXL3 fallback.
250
+ # Tile (P2a) and TEMP_ROWS=1024 (P2b) both lost at MNBT=1024 — leave 128.
251
+ EXL3_MOE_ROW_TILE="${EXL3_MOE_ROW_TILE:-0}"
252
+ # Fused exl3_moe temp rows/expert. 1024 was slower than 128+fallback (P2b).
253
+ EXL3_TEMP_ROWS_FUSED="${EXL3_TEMP_ROWS_FUSED:-128}"
254
+ # Sorted routing tier; higher tiers imply it even when this is 0.
255
+ EXL3_FAT_SORTED="${EXL3_FAT_SORTED:-0}"
256
+ # E1 batched tier: persistent scratch + combined gate/up; implies SORTED=1.
257
+ EXL3_FAT_BATCHED="${EXL3_FAT_BATCHED:-0}"
258
+ # E2 direct trellis kernel (default on). Implies BATCHED=1 and SORTED=1.
259
+ # Needs the patched extension — start.sh rebuilds when the recipe stamp drifts.
260
+ # Set all three flags to 0 for the legacy fat-expert path.
261
+ EXL3_FAT_KERNEL="${EXL3_FAT_KERNEL:-1}"
262
+
263
+ # --- abliteration (ablit/) --------------------------------------------------
264
+ # Load-time o_proj orthogonalization (overlay/ablit_runtime.py). Published
265
+ # recipe: layers 15-45 edited with the dealign direction, 0-14 stay stock
266
+ # safety anchors, MTP block included. 0 = stock weights. Applied identically
267
+ # on both TP ranks; the DFlash2 drafter is never touched.
268
+ ABLIT="${ABLIT:-0}"
269
+ ABLIT_METHOD="${ABLIT_METHOD:-auto}" # auto | transplant | proj
270
+ ABLIT_DIRECTION="${ABLIT_DIRECTION:-dealign}" # dealign | bf_oproj | /path/dir.pt
271
+ ABLIT_LAYERS="${ABLIT_LAYERS:-15-45}" # inclusive; 45 = checkpoint MTP block
272
+ ABLIT_ALPHA="${ABLIT_ALPHA:-3.0}" # 1.0 = plain projection, >1 over-projects
273
+ ABLIT_INCLUDE_MTP="${ABLIT_INCLUDE_MTP:-1}"
274
+
275
+ READY_TIMEOUT="${READY_TIMEOUT:-3600}"
276
+ # 1 = suppress client stop strings until </think> (DSpark #42 class).
277
+ GLM53_SUPPRESS_STOPS_IN_REASONING="${GLM53_SUPPRESS_STOPS_IN_REASONING:-1}"
278
+ # Mixed-step prefill policy when a peer is already decoding (issue #6).
279
+ # skip = do not mix; N>0 = cap tokens; 0 = off.
280
+ GLM53_MIXED_PREFILL_CHUNK="${GLM53_MIXED_PREFILL_CHUNK:-skip}"
281
+ # Sparse-indexer prefill gather workspace (overlay/patch_indexer_workspace.py).
282
+ # stock = max_model_len * 40 entries (5036.40 MB locked at 1M, measured);
283
+ # rightsize = the legal per-step maximum, ~+26% KV. Default applies only
284
+ # when UNSET: an explicitly empty value is an operator error and
285
+ # validate_numeric_config rejects it rather than guessing a serving mode.
286
+ GLM53_INDEXER_WORKSPACE="${GLM53_INDEXER_WORKSPACE-stock}"
287
+ # SpinCondition reader busy-loop window. "stock" preserves vLLM's 1 s default;
288
+ # 1..1000 selects milliseconds. The frozen TP=2 sweep selected 16 ms.
289
+ GLM53_SPINWAIT_MS="${GLM53_SPINWAIT_MS-stock}"
290
+ # EngineCore stock timeout is 300s; mid-serve Triton/TileLang JIT on TP=2 can
291
+ # exceed that without being a true hang. NCCL watchdog is still 600s.
292
+ VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS="${VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS:-1800}"
293
+ # 1 = after /health, burn DFlash2 BLOCK / sampler / kpool shapes. Nonfatal.
294
+ GLM53_BOOT_SHAPE_WARMUP="${GLM53_BOOT_SHAPE_WARMUP:-1}"
295
+ GLM53_WARMUP_REQ_TIMEOUT="${GLM53_WARMUP_REQ_TIMEOUT:-240}"
296
+
297
+ # OpenAI-compatible API bearer token. Read the native VLLM_API_KEY env var
298
+ # (vLLM falls back to it when --api-key is absent on the CLI), so the key
299
+ # never lands in argv / `non-default args` startup log. Empty = no auth.
300
+ # Same single-key semantics as the DeepSeek V4 Flash DSpark deployment.
301
+ VLLM_API_KEY="${VLLM_API_KEY:-}"
302
+
303
+ CONTAINER_HEAD="${CONTAINER_HEAD:-glm53-exl3-tp4-head}"
304
+ CONTAINER_WORKER="${CONTAINER_WORKER:-glm53-exl3-tp4-w1}"
305
+ CONTAINER_WORKER2="${CONTAINER_WORKER2:-glm53-exl3-tp4-w2}"
306
+ CONTAINER_WORKER3="${CONTAINER_WORKER3:-glm53-exl3-tp4-w3}"
307
+
308
+ HF_CACHE_DIR="${HF_HOME:-$HOME/.cache/huggingface}"
309
+ MODEL_PATH="$HF_CACHE_DIR/hub/$MODEL_CACHE_NAME"
310
+ FALLBACK_MODEL_PATH="$HF_CACHE_DIR/hub/$MODEL_FALLBACK_CACHE_NAME"
311
+ DFLASH_PATH="$HF_CACHE_DIR/hub/$DFLASH_CACHE_NAME"
312
+ WORKER_CACHE_DIR="$WORKER_HOME/.cache/huggingface"
313
+ WORKER2_CACHE_DIR="${WORKER2_CACHE_DIR:-$WORKER2_HOME/.cache/huggingface}"
314
+ WORKER3_CACHE_DIR="${WORKER3_CACHE_DIR:-$WORKER3_HOME/.cache/huggingface}"
315
+ CACHE_ROOT="${CACHE_ROOT:-$HOME/.cache/vllm-glm53-flash}"
316
+ WORKER_VLLM_CACHE="${WORKER_VLLM_CACHE:-$WORKER_HOME/.cache/vllm-glm53-flash}"
317
+ WORKER2_VLLM_CACHE="${WORKER2_VLLM_CACHE:-$WORKER2_HOME/.cache/vllm-glm53-flash}"
318
+ WORKER3_VLLM_CACHE="${WORKER3_VLLM_CACHE:-$WORKER3_HOME/.cache/vllm-glm53-flash}"
319
+ # Overlay FS ~/.triton and ~/.tilelang die on container recreate (TP=2 JIT
320
+ # stall → 600s NCCL watchdog). Persist next to the vLLM cache.
321
+ TRITON_HOST_CACHE="${TRITON_HOST_CACHE:-$CACHE_ROOT/triton}"
322
+ TILELANG_HOST_CACHE="${TILELANG_HOST_CACHE:-$CACHE_ROOT/tilelang}"
323
+ WORKER_TRITON_CACHE="${WORKER_TRITON_CACHE:-$WORKER_VLLM_CACHE/triton}"
324
+ WORKER_TILELANG_CACHE="${WORKER_TILELANG_CACHE:-$WORKER_VLLM_CACHE/tilelang}"
325
+ WORKER2_TRITON_CACHE="${WORKER2_TRITON_CACHE:-$WORKER2_VLLM_CACHE/triton}"
326
+ WORKER2_TILELANG_CACHE="${WORKER2_TILELANG_CACHE:-$WORKER2_VLLM_CACHE/tilelang}"
327
+ WORKER3_TRITON_CACHE="${WORKER3_TRITON_CACHE:-$WORKER3_VLLM_CACHE/triton}"
328
+ WORKER3_TILELANG_CACHE="${WORKER3_TILELANG_CACHE:-$WORKER3_VLLM_CACHE/tilelang}"
329
+ TRITON_CACHE_DIR="${TRITON_CACHE_DIR:-/root/.triton/cache}"
330
+ TILELANG_CACHE_DIR="${TILELANG_CACHE_DIR:-/root/.tilelang/cache}"
331
+
332
+ LOGDIR="$SCRIPT_DIR/logs"
333
+ HEAD_SCRIPT="$SCRIPT_DIR/.glm53-exl3-tp4-head.inner.sh"
334
+ WORKER_SCRIPT="$SCRIPT_DIR/.glm53-exl3-tp4-worker.inner.sh"
335
+ EXPECTED_SHARDS="${EXPECTED_SHARDS:-120}"
336
+
337
+ # ------------------------------- helpers -----------------------------------
338
+ log() { printf '\033[1;36m[glm53-exl3-tp4]\033[0m %s\n' "$*"; }
339
+ warn() { printf '\033[1;33m[glm53-exl3-tp4]\033[0m %s\n' "$*" >&2; }
340
+ die() { printf '\033[1;31m[glm53-exl3-tp4]\033[0m ERROR: %s\n' "$*" >&2; exit 1; }
341
+
342
+ # GLM53 numeric config guard (begin)
343
+ _glm53_canonical_positive_int() {
344
+ local name="$1" value="$2" maximum="$3" canonical
345
+ if ! [[ "$value" =~ ^[0-9]+$ ]]; then
346
+ echo "$name must be a positive base-10 integer (got: $value)" >&2
347
+ return 2
348
+ fi
349
+ canonical="$value"
350
+ while [ "${canonical#0}" != "$canonical" ]; do canonical="${canonical#0}"; done
351
+ [ -n "$canonical" ] || canonical=0
352
+ if [ "$canonical" = 0 ] \
353
+ || [ "${#canonical}" -gt "${#maximum}" ] \
354
+ || [ "$canonical" -gt "$maximum" ]; then
355
+ echo "$name must be between 1 and $maximum (got: $value)" >&2
356
+ return 2
357
+ fi
358
+ printf -v "$name" '%s' "$canonical"
359
+ # $name is a validated integer configuration variable.
360
+ # shellcheck disable=SC2163
361
+ export "$name"
362
+ }
363
+
364
+ # Enum knobs are exactly one of a fixed set. Not "non-empty means on": a
365
+ # typo'd knob must not silently pick a serving mode. GLM53_INDEXER_WORKSPACE
366
+ # sizes the sparse-indexer prefill workspace, and the patched
367
+ # get_max_prefill_buffer_size itself raises on anything but stock/rightsize
368
+ # (overlay/patch_indexer_workspace.py, _glm53_workspace_mode), so catching it
369
+ # here turns a container boot failure into a launcher error. The match is
370
+ # literal on both sides -- the "-stock" default applies only to an UNSET var,
371
+ # so "", " rightsize " and "RIGHTSIZE" all fail here and would fail there.
372
+ _glm53_validate_enum() {
373
+ local name="$1" value="$2" allowed
374
+ shift 2
375
+ for allowed in "$@"; do
376
+ [ "$value" = "$allowed" ] && return 0
377
+ done
378
+ echo "$name must be one of: $* (got: $value)" >&2
379
+ return 2
380
+ }
381
+
382
+ _glm53_validate_spinwait_ms() {
383
+ if [ "$GLM53_SPINWAIT_MS" = "stock" ]; then
384
+ export GLM53_SPINWAIT_MS
385
+ return 0
386
+ fi
387
+ _glm53_canonical_positive_int \
388
+ GLM53_SPINWAIT_MS "$GLM53_SPINWAIT_MS" 1000
389
+ }
390
+
391
+ validate_numeric_config() {
392
+ if ! [[ "$GPU_MEM_UTIL" =~ ^(0([.][0-9]+)?|[.][0-9]+|1([.]0+)?)$ ]] \
393
+ || ! awk -v u="$GPU_MEM_UTIL" 'BEGIN { exit !(u > 0 && u <= 1) }'; then
394
+ echo "GPU_MEM_UTIL must be greater than 0 and at most 1 (got: $GPU_MEM_UTIL)" >&2
395
+ return 2
396
+ fi
397
+ _glm53_canonical_positive_int MAX_MODEL_LEN "$MAX_MODEL_LEN" 1000000 || return
398
+ _glm53_canonical_positive_int MAX_NUM_SEQS "$MAX_NUM_SEQS" 4096 || return
399
+ _glm53_canonical_positive_int MAX_NUM_BATCHED_TOKENS "$MAX_NUM_BATCHED_TOKENS" 8388608 || return
400
+ _glm53_validate_enum GLM53_INDEXER_WORKSPACE "${GLM53_INDEXER_WORKSPACE-stock}" \
401
+ stock rightsize || return
402
+ _glm53_validate_spinwait_ms || return
403
+ }
404
+ # GLM53 numeric config guard (end)
405
+
406
+ banner() {
407
+ local label="${1:-start-tp4.sh}"
408
+ printf '\n'
409
+ printf ' \033[1;36m┌────────────────────────────────────────────┐\033[0m\n'
410
+ printf ' \033[1;36m│\033[0m \033[1mGLM-5.3 Flash EXL3\033[0m \033[2m· %-11s\033[0m \033[1;36m│\033[0m\n' "$label"
411
+ printf ' \033[1;36m└────────────────────────────────────────────┘\033[0m\n'
412
+ printf '\n'
413
+ }
414
+
415
+ # rank 1..3
416
+ _tp4_ssh_target() {
417
+ case "$1" in
418
+ 1) printf '%s' "$WORKER_SSH" ;;
419
+ 2) printf '%s' "$WORKER2_SSH" ;;
420
+ 3) printf '%s' "$WORKER3_SSH" ;;
421
+ *) die "internal: bad worker rank $1" ;;
422
+ esac
423
+ }
424
+ _tp4_rank_ip() {
425
+ case "$1" in
426
+ 1) printf '%s' "$WORKER_IP" ;;
427
+ 2) printf '%s' "$WORKER2_IP" ;;
428
+ 3) printf '%s' "$WORKER3_IP" ;;
429
+ esac
430
+ }
431
+ _tp4_rank_home() {
432
+ case "$1" in
433
+ 1) printf '%s' "$WORKER_HOME" ;;
434
+ 2) printf '%s' "$WORKER2_HOME" ;;
435
+ 3) printf '%s' "$WORKER3_HOME" ;;
436
+ esac
437
+ }
438
+ _tp4_rank_hf() {
439
+ case "$1" in
440
+ 1) printf '%s' "$WORKER_CACHE_DIR" ;;
441
+ 2) printf '%s' "$WORKER2_CACHE_DIR" ;;
442
+ 3) printf '%s' "$WORKER3_CACHE_DIR" ;;
443
+ esac
444
+ }
445
+ _tp4_rank_vllm() {
446
+ case "$1" in
447
+ 1) printf '%s' "$WORKER_VLLM_CACHE" ;;
448
+ 2) printf '%s' "$WORKER2_VLLM_CACHE" ;;
449
+ 3) printf '%s' "$WORKER3_VLLM_CACHE" ;;
450
+ esac
451
+ }
452
+ _tp4_rank_triton() {
453
+ case "$1" in
454
+ 1) printf '%s' "$WORKER_TRITON_CACHE" ;;
455
+ 2) printf '%s' "$WORKER2_TRITON_CACHE" ;;
456
+ 3) printf '%s' "$WORKER3_TRITON_CACHE" ;;
457
+ esac
458
+ }
459
+ _tp4_rank_tilelang() {
460
+ case "$1" in
461
+ 1) printf '%s' "$WORKER_TILELANG_CACHE" ;;
462
+ 2) printf '%s' "$WORKER2_TILELANG_CACHE" ;;
463
+ 3) printf '%s' "$WORKER3_TILELANG_CACHE" ;;
464
+ esac
465
+ }
466
+ _tp4_rank_cx7_if() {
467
+ case "$1" in
468
+ 1) printf '%s' "$WORKER_CX7_IF" ;;
469
+ 2) printf '%s' "$WORKER2_CX7_IF" ;;
470
+ 3) printf '%s' "$WORKER3_CX7_IF" ;;
471
+ esac
472
+ }
473
+ _tp4_rank_cx7_ib() {
474
+ case "$1" in
475
+ 1) printf '%s' "$WORKER_CX7_IB" ;;
476
+ 2) printf '%s' "$WORKER2_CX7_IB" ;;
477
+ 3) printf '%s' "$WORKER3_CX7_IB" ;;
478
+ esac
479
+ }
480
+ _tp4_rank_gid() {
481
+ case "$1" in
482
+ 1) printf '%s' "$WORKER_GID" ;;
483
+ 2) printf '%s' "$WORKER2_GID" ;;
484
+ 3) printf '%s' "$WORKER3_GID" ;;
485
+ esac
486
+ }
487
+ _tp4_rank_container() {
488
+ case "$1" in
489
+ 1) printf '%s' "$CONTAINER_WORKER" ;;
490
+ 2) printf '%s' "$CONTAINER_WORKER2" ;;
491
+ 3) printf '%s' "$CONTAINER_WORKER3" ;;
492
+ esac
493
+ }
494
+ _tp4_rank_nccl_dir() {
495
+ case "$1" in
496
+ 1) printf '%s' "$WORKER_NCCL_HOST_DIR" ;;
497
+ 2) printf '%s' "$WORKER2_NCCL_HOST_DIR" ;;
498
+ 3) printf '%s' "$WORKER3_NCCL_HOST_DIR" ;;
499
+ esac
500
+ }
501
+ _tp4_first_ib() { printf '%s' "${1%%,*}"; }
502
+
503
+ worker_ssh() { ssh -T -o BatchMode=yes -o ConnectTimeout=15 "$WORKER_SSH" "$@"; }
504
+ worker_ssh_n() {
505
+ local r="$1"; shift
506
+ ssh -T -o BatchMode=yes -o ConnectTimeout=15 "$(_tp4_ssh_target "$r")" "$@"
507
+ }
508
+
509
+ usage() { sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
510
+
511
+ count_shards() {
512
+ find "$1/snapshots" -name '*.safetensors' 2>/dev/null | wc -l | tr -d '[:space:]' || true
513
+ }
514
+
515
+ ensure_refs_main() {
516
+ local ref="$MODEL_PATH/refs/main" snap
517
+ [ -f "$ref" ] && [ -n "$(<"$ref")" ] && return 0
518
+ snap="$(ls -1t "$MODEL_PATH/snapshots" 2>/dev/null | head -n 1 || true)"
519
+ [ -n "$snap" ] || die "no snapshots under $MODEL_PATH — re-run download"
520
+ mkdir -p "$MODEL_PATH/refs"
521
+ printf '%s' "$snap" >"$ref"
522
+ log "wrote refs/main -> $snap (hf download left it empty)"
523
+ }
524
+
525
+ resolve_model_dir() {
526
+ local ref="$MODEL_PATH/refs/main" hash dir
527
+ ensure_refs_main
528
+ hash="$(<"$ref")"
529
+ dir="$MODEL_PATH/snapshots/$hash"
530
+ [ -f "$dir/config.json" ] || die "config.json missing in $dir — re-run with REFRESH_WEIGHTS=1"
531
+ printf '/root/.cache/huggingface/hub/%s/snapshots/%s' "$MODEL_CACHE_NAME" "$hash"
532
+ }
533
+
534
+ ensure_dflash_refs_main() {
535
+ local ref="$DFLASH_PATH/refs/main" snap
536
+ [ -f "$ref" ] && [ -n "$(<"$ref")" ] && return 0
537
+ snap="$(ls -1t "$DFLASH_PATH/snapshots" 2>/dev/null | head -n 1 || true)"
538
+ [ -n "$snap" ] || die "no snapshots under $DFLASH_PATH — re-run download"
539
+ mkdir -p "$DFLASH_PATH/refs"
540
+ printf '%s' "$snap" >"$ref"
541
+ log "wrote DFlash2 refs/main -> $snap"
542
+ }
543
+
544
+ resolve_dflash_dir() {
545
+ local ref="$DFLASH_PATH/refs/main" hash dir
546
+ ensure_dflash_refs_main
547
+ hash="$(<"$ref")"
548
+ dir="$DFLASH_PATH/snapshots/$hash"
549
+ [ -f "$dir/config.json" ] || die "DFlash2 config.json missing in $dir"
550
+ printf '/root/.cache/huggingface/hub/%s/snapshots/%s' "$DFLASH_CACHE_NAME" "$hash"
551
+ }
552
+
553
+ check_port_free() {
554
+ local port="$1" envname="$2"
555
+ command -v ss >/dev/null 2>&1 || return 0
556
+ if ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${port}\$"; then
557
+ if docker inspect -f '{{.State.Running}}' "$CONTAINER_HEAD" 2>/dev/null | grep -q true; then
558
+ die "port ${port} is held by ${CONTAINER_HEAD} — use './start-tp4.sh restart' or './start-tp4.sh stop' first"
559
+ fi
560
+ die "port ${port} is already in use — stop it or rerun with ${envname}=<free-port>"
561
+ fi
562
+ }
563
+
564
+ trap 'warn "interrupted — containers keep running ('"'"'./start-tp4.sh logs'"'"' to watch, '"'"'./start-tp4.sh stop'"'"' to stop)"; exit 130' INT
565
+
566
+ # ------------------------------ preflight ----------------------------------
567
+ preflight() {
568
+ command -v docker >/dev/null 2>&1 || die "docker not found on head"
569
+ command -v curl >/dev/null 2>&1 || die "curl not found on head"
570
+ command -v rsync >/dev/null 2>&1 || die "rsync not found on head"
571
+ docker info >/dev/null 2>&1 || die "cannot talk to docker daemon on head"
572
+
573
+ ip -4 addr show 2>/dev/null | grep -q "inet ${HEAD_IP}/" \
574
+ || die "HEAD_IP=${HEAD_IP} is not assigned on this host — set it in .env"
575
+
576
+ [ "$TP" = "4" ] || warn "TP=${TP} — this script is meant for TP=4"
577
+ [ "$NNODES" = "4" ] || warn "NNODES=${NNODES} — this script is meant for nnodes=4"
578
+ local r ssh_t
579
+ for r in 1 2 3; do
580
+ ssh_t="$(_tp4_ssh_target "$r")"
581
+ log "checking worker rank ${r} ${ssh_t} ..."
582
+ worker_ssh_n "$r" true 2>/dev/null \
583
+ || die "cannot ssh (key-based) to ${ssh_t} — set up passwordless ssh first"
584
+ worker_ssh_n "$r" "docker info >/dev/null 2>&1" \
585
+ || die "rank ${r} (${ssh_t}) cannot talk to its docker daemon"
586
+ worker_ssh_n "$r" "nvidia-smi -L 2>/dev/null | grep -q GB10" \
587
+ || warn "no GB10 GPU visible on rank ${r} (${ssh_t})"
588
+ done
589
+
590
+ # Each rank's GID index must name a populated entry on ITS OWN CX7 device.
591
+ # An empty (all-zero) entry passes every earlier check and then kills that
592
+ # rank ~60 s in with ibv_modify_qp errno 61 "No data available". The index is
593
+ # per-NIC, so validate head and worker separately: some pairs share one good
594
+ # index, others need different ones (HEAD_GID / WORKER_GID).
595
+ local gid_head gid_worker gid_path ib r ssh_t gid_ok=1
596
+ ib="$(_tp4_first_ib "$HEAD_CX7_IB")"
597
+ gid_path="/sys/class/infiniband/${ib}/ports/1/gids/${HEAD_GID}"
598
+ gid_head=$(cat "$gid_path" 2>/dev/null | tr -d ':0' || true)
599
+ [ -n "$gid_head" ] || { warn "head GID index ${HEAD_GID} is EMPTY on ${ib}"; gid_ok=0; }
600
+ for r in 1 2 3; do
601
+ ib="$(_tp4_first_ib "$(_tp4_rank_cx7_ib "$r")")"
602
+ gid_path="/sys/class/infiniband/${ib}/ports/1/gids/$(_tp4_rank_gid "$r")"
603
+ gid_worker=$(worker_ssh_n "$r" "cat '$gid_path' 2>/dev/null" | tr -d ':0' || true)
604
+ if [ -z "$gid_worker" ]; then
605
+ warn "rank ${r} GID index $(_tp4_rank_gid "$r") is EMPTY on ${ib}"
606
+ gid_ok=0
607
+ fi
608
+ done
609
+ if [ "$gid_ok" != "1" ]; then
610
+ if [ -z "$gid_head" ]; then
611
+ warn "head GID index ${HEAD_GID} is EMPTY on $(_tp4_first_ib "$HEAD_CX7_IB")"
612
+ fi
613
+ warn "GID tables — pick each node's ::ffff:<ip> entry whose type is RoCE v2;"
614
+ warn "the two indices need not match, and a v1 entry at the same index will not work:"
615
+ for i in 0 1 2 3 4 5 6 7; do
616
+ printf ' head gid%s: %-40s %s\n' "$i" \
617
+ "$(cat "/sys/class/infiniband/${HEAD_CX7_IB}/ports/1/gids/$i" 2>/dev/null)" \
618
+ "$(cat "/sys/class/infiniband/${HEAD_CX7_IB}/ports/1/gid_attrs/types/$i" 2>/dev/null)" >&2
619
+ done
620
+ worker_ssh "for i in 0 1 2 3 4 5 6 7; do printf ' worker gid%s: %-40s %s\n' \"\$i\" \"\$(cat /sys/class/infiniband/${WORKER_CX7_IB}/ports/1/gids/\$i 2>/dev/null)\" \"\$(cat /sys/class/infiniband/${WORKER_CX7_IB}/ports/1/gid_attrs/types/\$i 2>/dev/null)\"; done" >&2 || true
621
+ die "set NCCL_IB_GID_INDEX (same index both ranks) or HEAD_GID/WORKER_GID (per rank) in .env to populated indices"
622
+ fi
623
+
624
+ [ "$TP" = "4" ] || warn "TP=${TP} — expected TP=4 for start-tp4.sh"
625
+ [ "$NNODES" = "4" ] || warn "NNODES=${NNODES} — expected 4"
626
+
627
+ local others cname
628
+ for r in 1 2 3; do
629
+ cname="$(_tp4_rank_container "$r")"
630
+ others=$(worker_ssh_n "$r" "docker ps --format ' {{.Names}} ({{.Image}})'" 2>/dev/null | grep -v "^ ${cname}" || true)
631
+ if [ -n "$others" ]; then
632
+ warn "other containers are running on rank ${r} ($(_tp4_ssh_target "$r")):"
633
+ echo "$others" >&2
634
+ warn "this model needs most of each GB10 — stop GPU containers on that Spark first"
635
+ fi
636
+ done
637
+
638
+ check_port_free "$PORT" PORT
639
+ check_port_free "$MASTER_PORT" MASTER_PORT
640
+
641
+ [ -f "$STOP_PATCH_HOST" ] || die "$STOP_PATCH_HOST missing"
642
+ [ -f "$SCHED_PATCH_HOST" ] || die "$SCHED_PATCH_HOST missing"
643
+ [ -f "$DRAFTER_PATCH_HOST" ] || die "$DRAFTER_PATCH_HOST missing"
644
+ [ -f "$APC_PATCH_HOST" ] || die "$APC_PATCH_HOST missing"
645
+ [ -f "$XGRAMMAR_PATCH_HOST" ] || die "$XGRAMMAR_PATCH_HOST missing"
646
+ [ -f "$KPOOL_TAIL_PATCH_HOST" ] || die "$KPOOL_TAIL_PATCH_HOST missing"
647
+ [ -f "$SPINWAIT_PATCH_HOST" ] || die "$SPINWAIT_PATCH_HOST missing"
648
+ [ -f "$SCRIPT_DIR/overlay/patch_ablit.py" ] || die "$SCRIPT_DIR/overlay/patch_ablit.py missing"
649
+ [ -f "$SCRIPT_DIR/overlay/ablit_runtime.py" ] || die "$SCRIPT_DIR/overlay/ablit_runtime.py missing"
650
+ [ -f "$SCRIPT_DIR/ablit/LAYER_MAP.json" ] || die "$SCRIPT_DIR/ablit/LAYER_MAP.json missing"
651
+ if [ "$ABLIT" = "1" ]; then
652
+ log "ablit: ON (method=${ABLIT_METHOD} direction=${ABLIT_DIRECTION} layers=${ABLIT_LAYERS} alpha=${ABLIT_ALPHA} mtp=${ABLIT_INCLUDE_MTP})"
653
+ fi
654
+
655
+ local need_kb=$((180 * 1024 * 1024)) avail
656
+ mkdir -p "$HF_CACHE_DIR"
657
+ avail=$(df -Pk "$HF_CACHE_DIR" 2>/dev/null | awk 'NR==2{print $4}' || true)
658
+ [ "${avail:-0}" -ge "$need_kb" ] || warn "only $((avail/1024/1024)) GiB free on head for a ~164 GiB model"
659
+ for r in 1 2 3; do
660
+ avail=$(worker_ssh_n "$r" "df -Pk '$(_tp4_rank_home "$r")' 2>/dev/null" | awk 'NR==2{print $4}' || true)
661
+ [ "${avail:-0}" -ge "$need_kb" ] || warn "only $((avail/1024/1024)) GiB free on rank ${r} for a ~164 GiB model"
662
+ done
663
+
664
+ # The worker HF cache must be writable by the SSH user before the ~164 GiB
665
+ # sync starts. A root-owned ~/.cache/huggingface (prior sudo/docker
666
+ # prepare on the worker) otherwise fails mid-sync with a bare mkdir
667
+ # permission error. mkdir -p is idempotent and is what sync does anyway.
668
+ for r in 1 2 3; do
669
+ if ! worker_ssh_n "$r" "mkdir -p '$(_tp4_rank_hf "$r")/hub' && test -w '$(_tp4_rank_hf "$r")/hub'"; then
670
+ die "rank ${r} cannot write $(_tp4_rank_hf "$r")/hub — fix ownership, e.g. ssh $(_tp4_ssh_target "$r") \"sudo chown -R \$USER: '$(_tp4_rank_hf "$r")'\""
671
+ fi
672
+ done
673
+
674
+ log "preflight OK (head=$(hostname) ${HEAD_IP}, workers=${WORKER_SSH} ${WORKER2_SSH} ${WORKER3_SSH})"
675
+ }
676
+
677
+ # ------------------------------ image --------------------------------------
678
+ image_from_registry() {
679
+ case "$IMAGE" in
680
+ */*) return 0 ;;
681
+ *) return 1 ;;
682
+ esac
683
+ }
684
+
685
+ login_ghcr_if_token() {
686
+ [ -n "${GHCR_TOKEN:-}" ] || return 0
687
+ log "docker login ghcr.io as ${GHCR_USER} (GHCR_TOKEN)"
688
+ echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin >/dev/null
689
+ }
690
+
691
+ login_ghcr_if_token_worker() {
692
+ [ -n "${GHCR_TOKEN:-}" ] || return 0
693
+ local r
694
+ for r in 1 2 3; do
695
+ log "docker login ghcr.io on rank ${r} as ${GHCR_USER} (GHCR_TOKEN)"
696
+ echo "$GHCR_TOKEN" | worker_ssh_n "$r" "docker login ghcr.io -u '$GHCR_USER' --password-stdin" >/dev/null
697
+ done
698
+ }
699
+
700
+ # RepoDigest is stable across overlay2 vs containerd. Those snapshotters
701
+ # disagree on .Id (config digest vs index digest), so start.sh used to
702
+ # ship even after the worker had already pulled the same GHCR tag (issue #8).
703
+ # Local builds have no RepoDigest — use RootFS layer diffs, then .Id.
704
+ _IMAGE_KEY_FMT='{{if .RepoDigests}}{{index .RepoDigests 0}}{{else if .RootFS.Layers}}{{join .RootFS.Layers ","}}{{else}}{{.Id}}{{end}}'
705
+
706
+ parse_image_key() {
707
+ tr -d '\r' | sed -n 's/^GLM53KEY //p' | tail -n 1
708
+ }
709
+
710
+ local_image_key() {
711
+ docker image inspect -f "GLM53KEY ${_IMAGE_KEY_FMT}" "$IMAGE" 2>/dev/null | parse_image_key
712
+ }
713
+
714
+ worker_image_key() {
715
+ worker_ssh_n "${1:-1}" "docker image inspect -f 'GLM53KEY ${_IMAGE_KEY_FMT}' '$IMAGE' 2>/dev/null" | parse_image_key
716
+ }
717
+
718
+ images_match() {
719
+ [ -n "${1:-}" ] && [ -n "${2:-}" ] && [ "$1" = "$2" ]
720
+ }
721
+
722
+ image_platform() {
723
+ if [ -n "${IMAGE_PLATFORM:-}" ]; then
724
+ printf '%s' "$IMAGE_PLATFORM"
725
+ return
726
+ fi
727
+ local p
728
+ p="$(docker image inspect -f '{{.Os}}/{{.Architecture}}' "$IMAGE" 2>/dev/null || true)"
729
+ printf '%s' "${p:-linux/arm64}"
730
+ }
731
+
732
+ # Hash of Dockerfile + overlay/tests/files/ablit inputs that docker COPY.
733
+ # Compared to LABEL glm53.recipe.stamp so a git pull rebuilds once.
734
+ overlay_recipe_hash() {
735
+ {
736
+ printf '%s\n' "$SCRIPT_DIR/Dockerfile"
737
+ find "$SCRIPT_DIR/overlay" "$SCRIPT_DIR/files" "$SCRIPT_DIR/tests" \
738
+ "$SCRIPT_DIR/ablit" \
739
+ -type f \
740
+ ! -path '*/__pycache__/*' \
741
+ ! -path '*/ablit/transplant/*' \
742
+ ! -name '*.pyc' \
743
+ 2>/dev/null
744
+ } | LC_ALL=C sort | xargs -d '\n' -r sha256sum | sha256sum | awk '{print $1}'
745
+ }
746
+
747
+ image_recipe_stamp() {
748
+ local stamp
749
+ stamp="$(docker image inspect -f '{{ index .Config.Labels "glm53.recipe.stamp" }}' "$IMAGE" 2>/dev/null || true)"
750
+ case "$stamp" in
751
+ ""|"<no value>"|"<nil>") printf '' ;;
752
+ *) printf '%s' "$stamp" ;;
753
+ esac
754
+ }
755
+
756
+ build_image() {
757
+ local stamp
758
+ stamp="$(overlay_recipe_hash)"
759
+ log "building ${IMAGE} from Dockerfile stamp=${stamp:0:12} (log: $LOGDIR/build-sm121.log) ..."
760
+ docker build --build-arg "GLM53_RECIPE_STAMP=$stamp" -t "$IMAGE" "$SCRIPT_DIR" \
761
+ >"$LOGDIR/build-sm121.log" 2>&1 \
762
+ || { tail -n 40 "$LOGDIR/build-sm121.log" >&2; die "docker build of $IMAGE failed"; }
763
+ }
764
+
765
+ pull_image() {
766
+ login_ghcr_if_token
767
+ log "pulling ${IMAGE} ..."
768
+ docker pull "$IMAGE" && return 0
769
+ die "docker pull ${IMAGE} failed.
770
+ :exl3 is a public GHCR package — check network / disk.
771
+ If you still get 401/403: echo YOUR_PAT | docker login ghcr.io -u YOUR_GITHUB_USER --password-stdin
772
+ Overlay rebuild: BUILD=1 ./start.sh. Recipe-stamp drift also rebuilds; SKIP_BUILD=1 keeps GHCR."
773
+ }
774
+
775
+ pull_image_on_worker() {
776
+ local r="${1:-1}"
777
+ login_ghcr_if_token_worker
778
+ log "pulling ${IMAGE} on rank ${r} ..."
779
+ worker_ssh_n "$r" "docker pull '$IMAGE'"
780
+ }
781
+
782
+ ship_image_to_worker() {
783
+ local r="${1:-1}" platform
784
+ platform="$(image_platform)"
785
+ log "shipping ${IMAGE} (${platform}) to rank ${r} via docker save | ssh docker load ..."
786
+ if docker save --platform "$platform" "$IMAGE" | worker_ssh_n "$r" docker load; then
787
+ return 0
788
+ fi
789
+ warn "docker save --platform ${platform} failed — retrying without --platform"
790
+ docker save "$IMAGE" | worker_ssh_n "$r" docker load
791
+ }
792
+
793
+ ensure_image() {
794
+ mkdir -p "$LOGDIR"
795
+ local head_ok=0 worker_ok=0 head_key="" worker_key=""
796
+ if docker image inspect "$IMAGE" >/dev/null 2>&1; then
797
+ head_ok=1
798
+ head_key="$(local_image_key)"
799
+ fi
800
+ worker_ok=1
801
+ for r in 1 2 3; do
802
+ if worker_ssh_n "$r" "docker image inspect '$IMAGE' >/dev/null 2>&1"; then
803
+ worker_key="$(worker_image_key "$r")"
804
+ if images_match "$head_key" "$worker_key"; then
805
+ :
806
+ else
807
+ worker_ok=0
808
+ log "rank ${r} image differs (head=${head_key:-none} worker=${worker_key:-none}) — will refresh"
809
+ fi
810
+ else
811
+ worker_ok=0
812
+ fi
813
+ done
814
+ local skip_pull="${SKIP_PULL:-0}"
815
+ [ "${PULL:-0}" = "1" ] && skip_pull=0
816
+ local wanted_stamp have_stamp
817
+ wanted_stamp="$(overlay_recipe_hash)"
818
+ have_stamp=""
819
+ [ "$head_ok" = "1" ] && have_stamp="$(image_recipe_stamp)"
820
+ if [ "${BUILD:-0}" != "1" ] && [ "${SKIP_BUILD:-0}" != "1" ]; then
821
+ if [ "$head_ok" = "0" ] || [ "$have_stamp" != "$wanted_stamp" ]; then
822
+ log "image recipe ${have_stamp:-none} != repo ${wanted_stamp:0:12} — rebuilding (SKIP_BUILD=1 keeps GHCR)"
823
+ BUILD=1
824
+ fi
825
+ elif [ "${SKIP_BUILD:-0}" = "1" ] && [ "$have_stamp" != "$wanted_stamp" ]; then
826
+ warn "SKIP_BUILD=1 — not rebuilding; stamp ${have_stamp:-none} != repo ${wanted_stamp:0:12}"
827
+ fi
828
+ if [ "${BUILD:-0}" = "1" ]; then
829
+ build_image
830
+ head_key="$(local_image_key)"
831
+ head_ok=1
832
+ worker_ok=0
833
+ elif image_from_registry && [ "$skip_pull" != "1" ]; then
834
+ local before_key="$head_key"
835
+ pull_image
836
+ head_key="$(local_image_key)"
837
+ head_ok=1
838
+ if [ "$head_key" != "$before_key" ]; then
839
+ log "pulled ${IMAGE} (${before_key:-missing} -> ${head_key})"
840
+ else
841
+ log "${IMAGE} already current"
842
+ fi
843
+ if images_match "$worker_key" "$head_key"; then
844
+ worker_ok=1
845
+ else
846
+ worker_ok=0
847
+ fi
848
+ elif [ "$head_ok" = "0" ]; then
849
+ if image_from_registry && [ "$skip_pull" = "1" ]; then
850
+ die "SKIP_PULL=1 but ${IMAGE} is not on the head"
851
+ fi
852
+ build_image
853
+ head_key="$(local_image_key)"
854
+ head_ok=1
855
+ worker_ok=0
856
+ fi
857
+ if [ "${SKIP_SHIP:-0}" = "1" ]; then
858
+ [ "$worker_ok" = "1" ] || warn "SKIP_SHIP=1 — not copying ${IMAGE} to workers"
859
+ elif [ "$worker_ok" = "0" ]; then
860
+ for r in 1 2 3; do
861
+ local wok=0
862
+ worker_key="$(worker_image_key "$r")"
863
+ if images_match "$head_key" "$worker_key"; then
864
+ continue
865
+ fi
866
+ if image_from_registry && [ "$skip_pull" != "1" ] && [ "${BUILD:-0}" != "1" ]; then
867
+ if pull_image_on_worker "$r"; then
868
+ worker_key="$(worker_image_key "$r")"
869
+ if images_match "$head_key" "$worker_key"; then
870
+ wok=1
871
+ log "rank ${r} pulled ${IMAGE} — matches head"
872
+ else
873
+ warn "rank ${r} pull left a different image — shipping"
874
+ fi
875
+ else
876
+ warn "rank ${r} docker pull failed — shipping over SSH"
877
+ fi
878
+ fi
879
+ if [ "$wok" = "0" ]; then
880
+ ship_image_to_worker "$r"
881
+ worker_key="$(worker_image_key "$r")"
882
+ if images_match "$head_key" "$worker_key"; then
883
+ :
884
+ elif worker_ssh_n "$r" "docker image inspect '$IMAGE' >/dev/null 2>&1"; then
885
+ warn "rank ${r} has ${IMAGE} after ship but keys still differ — continuing"
886
+ else
887
+ die "rank ${r} still missing ${IMAGE} after ship"
888
+ fi
889
+ fi
890
+ done
891
+ fi
892
+ if [ "${SKIP_OVERLAY_VERIFY:-0}" != "1" ]; then
893
+ log "GPU EXL3 self-check on ${IMAGE} (log: $LOGDIR/overlay-verify.log) ..."
894
+ docker run --rm --gpus all \
895
+ -e EXL3_SELFCHECK_GPU=1 \
896
+ --entrypoint python3 "$IMAGE" /opt/glm53/test_exl3_overlay.py \
897
+ >"$LOGDIR/overlay-verify.log" 2>&1 \
898
+ || { tail -n 80 "$LOGDIR/overlay-verify.log" >&2; die "EXL3 overlay GPU self-check failed"; }
899
+ log "overlay verify OK"
900
+ fi
901
+ log "image ready on all four nodes"
902
+ }
903
+
904
+ # ---------------------------- weight download ------------------------------
905
+ # Use an already-complete local tree (primary or upstream fallback). If the
906
+ # durable Mia-AiLab mirror is still filling / 404s, keep serving from the
907
+ # brandonmusic cache folder without a second 164 GiB pull.
908
+ adopt_complete_weights() {
909
+ local have
910
+ have="$(count_shards "$MODEL_PATH")"
911
+ if [ "${have:-0}" -ge "$EXPECTED_SHARDS" ]; then
912
+ ensure_refs_main
913
+ log "weights already present: $MODEL_PATH ($have shards)"
914
+ return 0
915
+ fi
916
+ have="$(count_shards "$FALLBACK_MODEL_PATH")"
917
+ if [ "${have:-0}" -ge "$EXPECTED_SHARDS" ]; then
918
+ log "primary cache incomplete — using fallback ${MODEL_FALLBACK} at $FALLBACK_MODEL_PATH ($have shards)"
919
+ MODEL_PATH="$FALLBACK_MODEL_PATH"
920
+ MODEL_CACHE_NAME="$MODEL_FALLBACK_CACHE_NAME"
921
+ ensure_refs_main
922
+ return 0
923
+ fi
924
+ return 1
925
+ }
926
+
927
+ # Resolve the HF CLI even when it lives outside PATH (venv installs), with a
928
+ # python huggingface_hub fallback when no binary exists (issue #22, item 1).
929
+ # Sets the global HF_BIN_CMD array. HF_BIN (may contain arguments) wins when
930
+ # its first word resolves. Returns 1 when nothing usable is found.
931
+ resolve_hf_bin() {
932
+ HF_BIN_CMD=()
933
+ if [ -n "${HF_BIN:-}" ]; then
934
+ read -ra HF_BIN_CMD <<< "$HF_BIN"
935
+ if command -v "${HF_BIN_CMD[0]}" >/dev/null 2>&1; then return 0; fi
936
+ HF_BIN_CMD=()
937
+ fi
938
+ local cand
939
+ for cand in hf huggingface-cli "$HOME/.local/bin/hf" "$HOME/.hf-cli/venv/bin/hf" /opt/hf-cli/venv/bin/hf; do
940
+ if command -v "$cand" >/dev/null 2>&1; then HF_BIN_CMD=("$cand"); return 0; fi
941
+ done
942
+ if command -v python3 >/dev/null 2>&1 && python3 -c 'import huggingface_hub' >/dev/null 2>&1; then
943
+ HF_BIN_CMD=(python3 -m huggingface_hub.commands.huggingface_cli)
944
+ return 0
945
+ fi
946
+ return 1
947
+ }
948
+
949
+ hf_download_repo() {
950
+ local repo="$1"
951
+ shift
952
+ local -a args=("$repo")
953
+ if [ -n "${MODEL_REVISION:-}" ] && [ "$repo" = "$MODEL" ]; then
954
+ args+=(--revision "$MODEL_REVISION")
955
+ fi
956
+ args+=("$@")
957
+ HF_HOME="$HF_CACHE_DIR" "${HF_BIN_CMD[@]}" download "${args[@]}"
958
+ }
959
+
960
+ download_weights() {
961
+ [ "${SKIP_DOWNLOAD:-0}" = "1" ] && { log "SKIP_DOWNLOAD=1 — skipping download check"; return; }
962
+ if [ "${REFRESH_WEIGHTS:-0}" != "1" ] && adopt_complete_weights; then
963
+ return
964
+ fi
965
+
966
+ resolve_hf_bin || die "no 'hf' / 'huggingface-cli' on PATH and no python huggingface_hub — pip install --user -U 'huggingface_hub[cli]' (or set HF_BIN=/path/to/hf)"
967
+
968
+ mkdir -p "$HF_CACHE_DIR"
969
+ local -a hf_excl=()
970
+ local pat
971
+ IFS=',' read -ra _excl_pats <<< "${HF_DOWNLOAD_EXCLUDE:-runtime-results/**,src/**,runtime/src/**,scripts/**,docs/**,results/**,.materialization/**,runtime/scripts/**}"
972
+ for pat in "${_excl_pats[@]}"; do
973
+ [ -n "$pat" ] && hf_excl+=(--exclude "$pat")
974
+ done
975
+
976
+ log "downloading ${MODEL} (~164 GiB / ${EXPECTED_SHARDS} shards) into ${HF_CACHE_DIR} ..."
977
+ hf_download_repo "$MODEL" "${hf_excl[@]}" || warn "download of ${MODEL} failed — will try ${MODEL_FALLBACK}"
978
+ if adopt_complete_weights; then
979
+ return
980
+ fi
981
+
982
+ if [ "$MODEL_FALLBACK" != "$MODEL" ]; then
983
+ log "falling back to ${MODEL_FALLBACK} ..."
984
+ hf_download_repo "$MODEL_FALLBACK" "${hf_excl[@]}" \
985
+ || die "download of ${MODEL} and ${MODEL_FALLBACK} both failed"
986
+ fi
987
+ adopt_complete_weights \
988
+ || die "download finished with $(count_shards "$MODEL_PATH") / $EXPECTED_SHARDS shards"
989
+ }
990
+
991
+ download_dflash() {
992
+ [ "$SPEC_METHOD" = "dflash" ] || return 0
993
+ [ "${SKIP_DOWNLOAD:-0}" = "1" ] && { log "SKIP_DOWNLOAD=1 — skipping DFlash2 download check"; return; }
994
+ local have
995
+ have="$(find "$DFLASH_PATH/snapshots" -name 'model.safetensors' 2>/dev/null | wc -l | tr -d '[:space:]' || true)"
996
+ if [ "${have:-0}" -ge 1 ] && [ "${REFRESH_WEIGHTS:-0}" != "1" ]; then
997
+ log "DFlash2 already present: $DFLASH_PATH"
998
+ ensure_dflash_refs_main
999
+ return
1000
+ fi
1001
+ resolve_hf_bin || die "no 'hf' / 'huggingface-cli' on PATH and no python huggingface_hub — pip install --user -U 'huggingface_hub[cli]' (or set HF_BIN=/path/to/hf)"
1002
+ mkdir -p "$HF_CACHE_DIR"
1003
+ log "downloading ${DFLASH_MODEL} (~2.3 GiB) into ${HF_CACHE_DIR} ..."
1004
+ HF_HOME="$HF_CACHE_DIR" "${HF_BIN_CMD[@]}" download "$DFLASH_MODEL"
1005
+ ensure_dflash_refs_main
1006
+ have="$(find "$DFLASH_PATH/snapshots" -name 'model.safetensors' 2>/dev/null | wc -l | tr -d '[:space:]' || true)"
1007
+ [ "${have:-0}" -ge 1 ] || die "DFlash2 download finished without model.safetensors"
1008
+ log "DFlash2 download complete"
1009
+ }
1010
+
1011
+ # Head-only Hub fetch. No docker, no SSH, no worker rsync.
1012
+ download_only() {
1013
+ local have
1014
+ resolve_hf_bin || die "no 'hf' / 'huggingface-cli' on PATH and no python huggingface_hub — pip install --user -U 'huggingface_hub[cli]' (or set HF_BIN=/path/to/hf)"
1015
+ mkdir -p "$HF_CACHE_DIR"
1016
+ local need_kb=$((180 * 1024 * 1024)) avail
1017
+ avail=$(df -Pk "$HF_CACHE_DIR" 2>/dev/null | awk 'NR==2{print $4}' || true)
1018
+ [ "${avail:-0}" -ge "$need_kb" ] || warn "only $((avail/1024/1024)) GiB free on this disk for a ~164 GiB model"
1019
+
1020
+ # Explicit download: do not honor SKIP_DOWNLOAD from .env.
1021
+ SKIP_DOWNLOAD=0
1022
+ download_weights
1023
+ download_dflash
1024
+
1025
+ have="$(count_shards "$MODEL_PATH")"
1026
+ log "======================================================================"
1027
+ log "head HF cache : ${HF_CACHE_DIR}"
1028
+ log " target : ${MODEL} (${have} / ${EXPECTED_SHARDS} shards)"
1029
+ log " snapshot : ${MODEL_PATH}"
1030
+ if [ "$SPEC_METHOD" = "dflash" ]; then
1031
+ log " DFlash2 : ${DFLASH_MODEL}"
1032
+ log " draft cache : ${DFLASH_PATH}"
1033
+ else
1034
+ log " DFlash2 : skipped (SPEC_METHOD=${SPEC_METHOD})"
1035
+ fi
1036
+ log "workers were not touched. ./start-tp4.sh will rsync on launch unless SKIP_SYNC=1."
1037
+ log "======================================================================"
1038
+ }
1039
+
1040
+ # ------------------------------ weight sync --------------------------------
1041
+ # Keyed on the snapshot commit (refs/main, with the same repair fallback as
1042
+ # ensure_refs_main), not on MODEL_REVISION: the marker lives inside each
1043
+ # synced repo folder, so a MODEL / revision switch re-syncs automatically.
1044
+ # Without it, every ./start.sh pays a full size+mtime re-verification walk
1045
+ # over ~164 GiB / 120 shards on both ends for zero bytes of difference
1046
+ # (issue #22, item 2). FORCE_SYNC=1 bypasses the marker; deleting the
1047
+ # marker file on the worker has the same effect.
1048
+ sync_repo_marker_rev() {
1049
+ local src="$1"
1050
+ local rev
1051
+ rev="$(cat "$src/refs/main" 2>/dev/null || true)"
1052
+ [ -n "$rev" ] || rev="$(ls -1t "$src/snapshots" 2>/dev/null | head -n 1 || true)"
1053
+ [ -n "$rev" ] || rev="unknown"
1054
+ printf '%s' "$rev"
1055
+ }
1056
+
1057
+ sync_repo_to_one_worker() {
1058
+ local r="$1" src="$2" cache_name="$3" label="$4"
1059
+ local marker rev hf ssh_t
1060
+ hf="$(_tp4_rank_hf "$r")"
1061
+ ssh_t="$(_tp4_ssh_target "$r")"
1062
+ marker="${hf}/hub/${cache_name}/.glm53-exl3-synced"
1063
+ rev="$(sync_repo_marker_rev "$src")"
1064
+ if [ "${FORCE_SYNC:-0}" != "1" ] \
1065
+ && [ "$(worker_ssh_n "$r" "cat '$marker' 2>/dev/null" || true)" = "$rev" ]; then
1066
+ log "rank ${r} ${cache_name} already at ${rev} — rsync skipped"
1067
+ return 0
1068
+ fi
1069
+ log "syncing ${label} to rank ${r} (${ssh_t}) ..."
1070
+ worker_ssh_n "$r" "mkdir -p '${hf}/hub/${cache_name}'"
1071
+ rsync -a --partial --info=progress2 \
1072
+ "$src/" "${ssh_t}:${hf}/hub/${cache_name}/"
1073
+ worker_ssh_n "$r" "printf '%s' '$rev' > '$marker'"
1074
+ }
1075
+
1076
+ sync_weights() {
1077
+ [ "${SKIP_SYNC:-0}" = "1" ] && { log "SKIP_SYNC=1 — not syncing to workers"; return; }
1078
+ [ -d "$MODEL_PATH" ] || die "weights missing at $MODEL_PATH — run without SKIP_DOWNLOAD first"
1079
+ local r
1080
+ for r in 1 2 3; do
1081
+ sync_repo_to_one_worker "$r" "$MODEL_PATH" "$MODEL_CACHE_NAME" "weights"
1082
+ if [ "$SPEC_METHOD" = "dflash" ]; then
1083
+ [ -d "$DFLASH_PATH" ] || die "DFlash2 weights missing at $DFLASH_PATH"
1084
+ sync_repo_to_one_worker "$r" "$DFLASH_PATH" "$DFLASH_CACHE_NAME" "DFlash2 draft"
1085
+ fi
1086
+ done
1087
+ log "all worker weights in sync"
1088
+ }
1089
+
1090
+ # ------------------------ inner container scripts --------------------------
1091
+ write_inner_scripts() {
1092
+ cat > "$HEAD_SCRIPT" <<'EOF'
1093
+ #!/bin/bash
1094
+ set -euo pipefail
1095
+ say() { echo "[glm53-exl3-head] $*"; }
1096
+
1097
+ ARGS=(
1098
+ --served-model-name "${SERVED_MODEL_NAME}"
1099
+ --host 0.0.0.0
1100
+ --port "${PORT}"
1101
+ --tensor-parallel-size "${TP}"
1102
+ --nnodes "${NNODES}"
1103
+ --node-rank 0
1104
+ --master-addr "${HEAD_IP}"
1105
+ --master-port "${MASTER_PORT}"
1106
+ --distributed-executor-backend mp
1107
+ --tool-call-parser glm47
1108
+ --enable-auto-tool-choice
1109
+ --reasoning-parser glm45
1110
+ --enable-prefix-caching
1111
+ --no-enable-flashinfer-autotune
1112
+ )
1113
+ [ "${ENFORCE_EAGER:-1}" = "1" ] && ARGS+=(--enforce-eager)
1114
+ [ -n "${QUANTIZATION:-}" ] && [ "${QUANTIZATION}" != "none" ] && ARGS+=(--quantization "${QUANTIZATION}")
1115
+ [ -n "${MAX_MODEL_LEN:-}" ] && ARGS+=(--max-model-len "${MAX_MODEL_LEN}")
1116
+ [ -n "${GPU_MEM_UTIL:-}" ] && ARGS+=(--gpu-memory-utilization "${GPU_MEM_UTIL}")
1117
+ [ -n "${MAX_NUM_SEQS:-}" ] && ARGS+=(--max-num-seqs "${MAX_NUM_SEQS}")
1118
+ [ -n "${MAX_NUM_BATCHED_TOKENS:-}" ] && ARGS+=(--max-num-batched-tokens "${MAX_NUM_BATCHED_TOKENS}")
1119
+ [ -n "${KV_CACHE_DTYPE:-}" ] && ARGS+=(--kv-cache-dtype "${KV_CACHE_DTYPE}")
1120
+ if [ "${SPEC_METHOD:-mtp}" = "dflash" ]; then
1121
+ ARGS+=(--speculative-config "$(python3 -S -c 'import json,os
1122
+ spec={"method":"dflash","model":os.environ["DFLASH_MODEL_DIR"],"num_speculative_tokens":int(os.environ.get("DFLASH_TOKENS","7")),"kv_cache_dtype":"auto","draft_sample_method":"probabilistic","rejection_sample_method":"standard"}
1123
+ tp=os.environ.get("DFLASH_DRAFT_TP","").strip()
1124
+ if tp:
1125
+ spec["draft_tensor_parallel_size"]=int(tp)
1126
+ print(json.dumps(spec,separators=(",",":")))')")
1127
+ elif [ "${SPEC_METHOD:-mtp}" = "none" ]; then
1128
+ :
1129
+ elif [ "${MTP_TOKENS:-0}" != "0" ]; then
1130
+ ARGS+=(--speculative-config "{\"method\":\"mtp\",\"num_speculative_tokens\":${MTP_TOKENS}}")
1131
+ fi
1132
+ if [ -n "${CHAT_TEMPLATE:-}" ] && [ -f "${CHAT_TEMPLATE}" ]; then
1133
+ ARGS+=(--chat-template "${CHAT_TEMPLATE}")
1134
+ fi
1135
+ if [ "${LANGUAGE_MODEL_ONLY:-0}" = "1" ]; then
1136
+ ARGS+=(--language-model-only)
1137
+ say "language-model-only: no vision tower"
1138
+ else
1139
+ [ -n "${LIMIT_MM:-}" ] && ARGS+=(--limit-mm-per-prompt "${LIMIT_MM}")
1140
+ [ "${SKIP_MM_PROFILING:-1}" = "1" ] && ARGS+=(--skip-mm-profiling)
1141
+ say "vision on: limit-mm=${LIMIT_MM:-} skip-mm-profiling=${SKIP_MM_PROFILING:-1} chat-template=${CHAT_TEMPLATE:-}"
1142
+ fi
1143
+ if [ -n "${EXTRA_ARGS:-}" ]; then
1144
+ # shellcheck disable=SC2206
1145
+ EXTRA=(${EXTRA_ARGS})
1146
+ ARGS+=("${EXTRA[@]}")
1147
+ fi
1148
+
1149
+ [ -f "${MODEL_DIR}/config.json" ] || { say "FATAL: ${MODEL_DIR}/config.json missing"; ls -la "${MODEL_DIR}" | head; exit 1; }
1150
+ if [ -f /opt/glm53/patch_glm_video_placeholders.py ]; then
1151
+ python3 /opt/glm53/patch_glm_video_placeholders.py
1152
+ fi
1153
+ if [ -f /opt/glm53/patch_suppress_stops_in_reasoning.py ]; then
1154
+ python3 /opt/glm53/patch_suppress_stops_in_reasoning.py
1155
+ fi
1156
+ if [ -f /opt/glm53/patch_scheduler_decode_floor.py ]; then
1157
+ python3 /opt/glm53/patch_scheduler_decode_floor.py
1158
+ fi
1159
+ if [ -f /opt/glm53/patch_glm5_drafter_group.py ]; then
1160
+ python3 /opt/glm53/patch_glm5_drafter_group.py
1161
+ fi
1162
+ if [ -f /opt/glm53/patch_hybrid_prefix_hit.py ]; then
1163
+ python3 /opt/glm53/patch_hybrid_prefix_hit.py
1164
+ fi
1165
+ if [ -f /opt/glm53/patch_xgrammar_termination.py ]; then
1166
+ python3 /opt/glm53/patch_xgrammar_termination.py
1167
+ fi
1168
+ if [ -f /opt/glm53/patch_kpool_tail_slotmap.py ]; then
1169
+ python3 /opt/glm53/patch_kpool_tail_slotmap.py
1170
+ fi
1171
+ if [ -f /opt/glm53/patch_spinwait.py ]; then
1172
+ python3 /opt/glm53/patch_spinwait.py
1173
+ fi
1174
+ if [ -f /opt/glm53/patch_indexer_workspace.py ]; then
1175
+ python3 /opt/glm53/patch_indexer_workspace.py
1176
+ fi
1177
+ if [ -f /opt/glm53/patch_ablit.py ]; then
1178
+ python3 /opt/glm53/patch_ablit.py
1179
+ fi
1180
+ if [ "${ABLIT:-0}" = "1" ]; then
1181
+ say "ablit: o_proj orthogonalization ON (method=${ABLIT_METHOD:-auto} direction=${ABLIT_DIRECTION:-dealign} layers=${ABLIT_LAYERS:-15-45} alpha=${ABLIT_ALPHA:-3.0})"
1182
+ else
1183
+ say "ablit: off — stock o_proj weights"
1184
+ fi
1185
+ say "launching: vllm serve ${MODEL_DIR} ${ARGS[*]}"
1186
+ exec vllm serve "${MODEL_DIR}" "${ARGS[@]}"
1187
+ EOF
1188
+
1189
+ cat > "$WORKER_SCRIPT" <<'EOF'
1190
+ #!/bin/bash
1191
+ set -euo pipefail
1192
+ say() { echo "[glm53-exl3-worker] $*"; }
1193
+
1194
+ ARGS=(
1195
+ --served-model-name "${SERVED_MODEL_NAME}"
1196
+ --host 0.0.0.0
1197
+ --port "${PORT}"
1198
+ --tensor-parallel-size "${TP}"
1199
+ --nnodes "${NNODES}"
1200
+ --node-rank "${NODE_RANK}"
1201
+ --master-addr "${HEAD_IP}"
1202
+ --master-port "${MASTER_PORT}"
1203
+ --distributed-executor-backend mp
1204
+ --headless
1205
+ --tool-call-parser glm47
1206
+ --enable-auto-tool-choice
1207
+ --reasoning-parser glm45
1208
+ --enable-prefix-caching
1209
+ --no-enable-flashinfer-autotune
1210
+ )
1211
+ [ "${ENFORCE_EAGER:-1}" = "1" ] && ARGS+=(--enforce-eager)
1212
+ [ -n "${QUANTIZATION:-}" ] && [ "${QUANTIZATION}" != "none" ] && ARGS+=(--quantization "${QUANTIZATION}")
1213
+ [ -n "${MAX_MODEL_LEN:-}" ] && ARGS+=(--max-model-len "${MAX_MODEL_LEN}")
1214
+ [ -n "${GPU_MEM_UTIL:-}" ] && ARGS+=(--gpu-memory-utilization "${GPU_MEM_UTIL}")
1215
+ [ -n "${MAX_NUM_SEQS:-}" ] && ARGS+=(--max-num-seqs "${MAX_NUM_SEQS}")
1216
+ [ -n "${MAX_NUM_BATCHED_TOKENS:-}" ] && ARGS+=(--max-num-batched-tokens "${MAX_NUM_BATCHED_TOKENS}")
1217
+ [ -n "${KV_CACHE_DTYPE:-}" ] && ARGS+=(--kv-cache-dtype "${KV_CACHE_DTYPE}")
1218
+ if [ "${SPEC_METHOD:-mtp}" = "dflash" ]; then
1219
+ ARGS+=(--speculative-config "$(python3 -S -c 'import json,os
1220
+ spec={"method":"dflash","model":os.environ["DFLASH_MODEL_DIR"],"num_speculative_tokens":int(os.environ.get("DFLASH_TOKENS","7")),"kv_cache_dtype":"auto","draft_sample_method":"probabilistic","rejection_sample_method":"standard"}
1221
+ tp=os.environ.get("DFLASH_DRAFT_TP","").strip()
1222
+ if tp:
1223
+ spec["draft_tensor_parallel_size"]=int(tp)
1224
+ print(json.dumps(spec,separators=(",",":")))')")
1225
+ elif [ "${SPEC_METHOD:-mtp}" = "none" ]; then
1226
+ :
1227
+ elif [ "${MTP_TOKENS:-0}" != "0" ]; then
1228
+ ARGS+=(--speculative-config "{\"method\":\"mtp\",\"num_speculative_tokens\":${MTP_TOKENS}}")
1229
+ fi
1230
+ if [ -n "${CHAT_TEMPLATE:-}" ] && [ -f "${CHAT_TEMPLATE}" ]; then
1231
+ ARGS+=(--chat-template "${CHAT_TEMPLATE}")
1232
+ fi
1233
+ if [ "${LANGUAGE_MODEL_ONLY:-0}" = "1" ]; then
1234
+ ARGS+=(--language-model-only)
1235
+ else
1236
+ [ -n "${LIMIT_MM:-}" ] && ARGS+=(--limit-mm-per-prompt "${LIMIT_MM}")
1237
+ [ "${SKIP_MM_PROFILING:-1}" = "1" ] && ARGS+=(--skip-mm-profiling)
1238
+ fi
1239
+ if [ -n "${EXTRA_ARGS:-}" ]; then
1240
+ # shellcheck disable=SC2206
1241
+ EXTRA=(${EXTRA_ARGS})
1242
+ ARGS+=("${EXTRA[@]}")
1243
+ fi
1244
+
1245
+ [ -f "${MODEL_DIR}/config.json" ] || { say "FATAL: ${MODEL_DIR}/config.json missing"; ls -la "${MODEL_DIR}" | head; exit 1; }
1246
+ if [ -f /opt/glm53/patch_glm_video_placeholders.py ]; then
1247
+ python3 /opt/glm53/patch_glm_video_placeholders.py
1248
+ fi
1249
+ if [ -f /opt/glm53/patch_suppress_stops_in_reasoning.py ]; then
1250
+ python3 /opt/glm53/patch_suppress_stops_in_reasoning.py
1251
+ fi
1252
+ if [ -f /opt/glm53/patch_scheduler_decode_floor.py ]; then
1253
+ python3 /opt/glm53/patch_scheduler_decode_floor.py
1254
+ fi
1255
+ if [ -f /opt/glm53/patch_glm5_drafter_group.py ]; then
1256
+ python3 /opt/glm53/patch_glm5_drafter_group.py
1257
+ fi
1258
+ if [ -f /opt/glm53/patch_hybrid_prefix_hit.py ]; then
1259
+ python3 /opt/glm53/patch_hybrid_prefix_hit.py
1260
+ fi
1261
+ if [ -f /opt/glm53/patch_xgrammar_termination.py ]; then
1262
+ python3 /opt/glm53/patch_xgrammar_termination.py
1263
+ fi
1264
+ if [ -f /opt/glm53/patch_kpool_tail_slotmap.py ]; then
1265
+ python3 /opt/glm53/patch_kpool_tail_slotmap.py
1266
+ fi
1267
+ if [ -f /opt/glm53/patch_spinwait.py ]; then
1268
+ python3 /opt/glm53/patch_spinwait.py
1269
+ fi
1270
+ if [ -f /opt/glm53/patch_indexer_workspace.py ]; then
1271
+ python3 /opt/glm53/patch_indexer_workspace.py
1272
+ fi
1273
+ if [ -f /opt/glm53/patch_ablit.py ]; then
1274
+ python3 /opt/glm53/patch_ablit.py
1275
+ fi
1276
+ if [ "${ABLIT:-0}" = "1" ]; then
1277
+ say "ablit: o_proj orthogonalization ON (method=${ABLIT_METHOD:-auto} direction=${ABLIT_DIRECTION:-dealign} layers=${ABLIT_LAYERS:-15-45} alpha=${ABLIT_ALPHA:-3.0})"
1278
+ else
1279
+ say "ablit: off — stock o_proj weights"
1280
+ fi
1281
+ say "joining TP${TP} at ${HEAD_IP}:${MASTER_PORT} as rank ${NODE_RANK}"
1282
+ exec vllm serve "${MODEL_DIR}" "${ARGS[@]}"
1283
+ EOF
1284
+ chmod +x "$HEAD_SCRIPT" "$WORKER_SCRIPT"
1285
+ }
1286
+
1287
+ # ------------------------------- launch ------------------------------------
1288
+ _tp4_scp_runtime() {
1289
+ local r="$1" ssh_t cname
1290
+ ssh_t="$(_tp4_ssh_target "$r")"
1291
+ cname="$(_tp4_rank_container "$r")"
1292
+ scp -q -o BatchMode=yes "$WORKER_SCRIPT" "${ssh_t}:/tmp/${cname}.sh"
1293
+ scp -q -o BatchMode=yes "$CHAT_TEMPLATE_HOST" "${ssh_t}:/tmp/glm53-chat_template.jinja"
1294
+ scp -q -o BatchMode=yes "$VIDEO_PATCH_HOST" "${ssh_t}:/tmp/patch_glm_video_placeholders.py"
1295
+ scp -q -o BatchMode=yes "$STOP_PATCH_HOST" "${ssh_t}:/tmp/patch_suppress_stops_in_reasoning.py"
1296
+ scp -q -o BatchMode=yes "$SCHED_PATCH_HOST" "${ssh_t}:/tmp/patch_scheduler_decode_floor.py"
1297
+ scp -q -o BatchMode=yes "$DRAFTER_PATCH_HOST" "${ssh_t}:/tmp/patch_glm5_drafter_group.py"
1298
+ scp -q -o BatchMode=yes "$APC_PATCH_HOST" "${ssh_t}:/tmp/patch_hybrid_prefix_hit.py"
1299
+ scp -q -o BatchMode=yes "$XGRAMMAR_PATCH_HOST" "${ssh_t}:/tmp/patch_xgrammar_termination.py"
1300
+ scp -q -o BatchMode=yes "$KPOOL_TAIL_PATCH_HOST" "${ssh_t}:/tmp/patch_kpool_tail_slotmap.py"
1301
+ scp -q -o BatchMode=yes "$SPINWAIT_PATCH_HOST" "${ssh_t}:/tmp/patch_spinwait.py"
1302
+ worker_ssh_n "$r" "rm -rf /tmp/glm53-ablit"
1303
+ scp -q -r -o BatchMode=yes "$SCRIPT_DIR/ablit" "${ssh_t}:/tmp/glm53-ablit"
1304
+ scp -q -o BatchMode=yes "$SCRIPT_DIR/overlay/ablit_runtime.py" "${ssh_t}:/tmp/glm53-ablit_runtime.py"
1305
+ scp -q -o BatchMode=yes "$SCRIPT_DIR/overlay/patch_ablit.py" "${ssh_t}:/tmp/patch_ablit.py"
1306
+ }
1307
+
1308
+ launch_cluster() {
1309
+ local r
1310
+ docker rm -f "$CONTAINER_HEAD" >/dev/null 2>&1 || true
1311
+ for r in 1 2 3; do
1312
+ worker_ssh_n "$r" "docker rm -f '$(_tp4_rank_container "$r")'" >/dev/null 2>&1 || true
1313
+ done
1314
+
1315
+ mkdir -p "$CACHE_ROOT" "$TRITON_HOST_CACHE" "$TILELANG_HOST_CACHE"
1316
+ [ -f "$CHAT_TEMPLATE_HOST" ] || die "missing chat template: $CHAT_TEMPLATE_HOST"
1317
+ for r in 1 2 3; do
1318
+ worker_ssh_n "$r" "mkdir -p '$(_tp4_rank_vllm "$r")' '$(_tp4_rank_triton "$r")' '$(_tp4_rank_tilelang "$r")'"
1319
+ _tp4_scp_runtime "$r"
1320
+ done
1321
+ # skip the old single-worker scp block
1322
+ true
1323
+ : <<'TP4_SKIP_OLD_SCP'
1324
+ [ -f "$CHAT_TEMPLATE_HOST" ] || die "missing chat template: $CHAT_TEMPLATE_HOST"
1325
+ scp -q -o BatchMode=yes "$CHAT_TEMPLATE_HOST" "${WORKER_SSH}:/tmp/glm53-chat_template.jinja"
1326
+ [ -f "$VIDEO_PATCH_HOST" ] || die "missing $VIDEO_PATCH_HOST"
1327
+ scp -q -o BatchMode=yes "$VIDEO_PATCH_HOST" "${WORKER_SSH}:/tmp/patch_glm_video_placeholders.py"
1328
+ [ -f "$STOP_PATCH_HOST" ] || die "missing $STOP_PATCH_HOST"
1329
+ scp -q -o BatchMode=yes "$STOP_PATCH_HOST" "${WORKER_SSH}:/tmp/patch_suppress_stops_in_reasoning.py"
1330
+ [ -f "$SCHED_PATCH_HOST" ] || die "missing $SCHED_PATCH_HOST"
1331
+ scp -q -o BatchMode=yes "$SCHED_PATCH_HOST" "${WORKER_SSH}:/tmp/patch_scheduler_decode_floor.py"
1332
+ [ -f "$DRAFTER_PATCH_HOST" ] || die "missing $DRAFTER_PATCH_HOST"
1333
+ scp -q -o BatchMode=yes "$DRAFTER_PATCH_HOST" "${WORKER_SSH}:/tmp/patch_glm5_drafter_group.py"
1334
+ [ -f "$APC_PATCH_HOST" ] || die "missing $APC_PATCH_HOST"
1335
+ scp -q -o BatchMode=yes "$APC_PATCH_HOST" "${WORKER_SSH}:/tmp/patch_hybrid_prefix_hit.py"
1336
+ [ -f "$XGRAMMAR_PATCH_HOST" ] || die "missing $XGRAMMAR_PATCH_HOST"
1337
+ scp -q -o BatchMode=yes "$XGRAMMAR_PATCH_HOST" "${WORKER_SSH}:/tmp/patch_xgrammar_termination.py"
1338
+ [ -f "$KPOOL_TAIL_PATCH_HOST" ] || die "missing $KPOOL_TAIL_PATCH_HOST"
1339
+ scp -q -o BatchMode=yes "$KPOOL_TAIL_PATCH_HOST" "${WORKER_SSH}:/tmp/patch_kpool_tail_slotmap.py"
1340
+ [ -f "$SPINWAIT_PATCH_HOST" ] || die "missing $SPINWAIT_PATCH_HOST"
1341
+ scp -q -o BatchMode=yes "$SPINWAIT_PATCH_HOST" "${WORKER_SSH}:/tmp/patch_spinwait.py"
1342
+
1343
+ worker_ssh "rm -rf /tmp/glm53-ablit"
1344
+ scp -q -r -o BatchMode=yes "$SCRIPT_DIR/ablit" "${WORKER_SSH}:/tmp/glm53-ablit"
1345
+ scp -q -o BatchMode=yes "$SCRIPT_DIR/overlay/ablit_runtime.py" "${WORKER_SSH}:/tmp/glm53-ablit_runtime.py"
1346
+ scp -q -o BatchMode=yes "$SCRIPT_DIR/overlay/patch_ablit.py" "${WORKER_SSH}:/tmp/patch_ablit.py"
1347
+ TP4_SKIP_OLD_SCP
1348
+
1349
+ local -a nccl_common=(
1350
+ -e NCCL_IB_DISABLE=0
1351
+ -e NCCL_IB_ROCE_VERSION_NUM=2
1352
+ -e NCCL_NET=IB
1353
+ -e NCCL_NET_PLUGIN=none
1354
+ -e NCCL_NVLS_ENABLE=0
1355
+ -e NCCL_CUMEM_ENABLE=0
1356
+ -e NCCL_IB_MERGE_NICS=0
1357
+ -e "NCCL_CROSS_NIC=$NCCL_CROSS_NIC"
1358
+ -e NCCL_IGNORE_CPU_AFFINITY=1
1359
+ -e "NCCL_DEBUG=$NCCL_DEBUG"
1360
+ -e HF_HUB_OFFLINE=1
1361
+ -e TRANSFORMERS_OFFLINE=1
1362
+ -e HF_HOME=/root/.cache/huggingface
1363
+ -e VLLM_CACHE_ROOT=/root/.cache/vllm
1364
+ -e "GLM53_SUPPRESS_STOPS_IN_REASONING=$GLM53_SUPPRESS_STOPS_IN_REASONING"
1365
+ -e "GLM53_MIXED_PREFILL_CHUNK=$GLM53_MIXED_PREFILL_CHUNK"
1366
+ -e "GLM53_INDEXER_WORKSPACE=$GLM53_INDEXER_WORKSPACE"
1367
+ -e "GLM53_SPINWAIT_MS=$GLM53_SPINWAIT_MS"
1368
+ -e "TRITON_CACHE_DIR=$TRITON_CACHE_DIR"
1369
+ -e "TILELANG_CACHE_DIR=$TILELANG_CACHE_DIR"
1370
+ -e "VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS=$VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS"
1371
+ -e "TORCH_CUDA_ARCH_LIST=$TORCH_CUDA_ARCH_LIST"
1372
+ -e "FLASHINFER_CUDA_ARCH_LIST=$FLASHINFER_CUDA_ARCH_LIST"
1373
+ -e FLASHINFER_DISABLE_VERSION_CHECK=1
1374
+ -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
1375
+ -e "VLLM_ENGINE_READY_TIMEOUT_S=$READY_TIMEOUT"
1376
+ # py-cpuinfo JSON-parses empty output on Grace/aarch64; the usage
1377
+ # thread then dumps JSONDecodeError. Stats are off on this private kit.
1378
+ -e VLLM_NO_USAGE_STATS=1
1379
+ -e DO_NOT_TRACK=1
1380
+ -e "VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=$CG_ESTIMATE"
1381
+ )
1382
+ local worker_nccl="" e
1383
+ for e in "${nccl_common[@]}"; do
1384
+ [ "$e" = "-e" ] && continue
1385
+ worker_nccl+=" -e $e"
1386
+ done
1387
+
1388
+ local -a head_preload=()
1389
+ if [ "$USE_HOST_NCCL" = "1" ]; then
1390
+ if [ -f "$NCCL_HOST_DIR/$NCCL_SO_NAME" ]; then
1391
+ head_preload=(-v "$NCCL_HOST_DIR:/nccl:ro" -e "LD_PRELOAD=/nccl/$NCCL_SO_NAME")
1392
+ log "head: LD_PRELOAD $NCCL_SO_NAME"
1393
+ else
1394
+ warn "head: $NCCL_HOST_DIR/$NCCL_SO_NAME missing — using image NCCL"
1395
+ fi
1396
+ fi
1397
+
1398
+ local serve_env=""
1399
+ local v
1400
+ for v in SERVED_MODEL_NAME PORT TP NNODES HEAD_IP MASTER_PORT QUANTIZATION \
1401
+ MAX_MODEL_LEN GPU_MEM_UTIL MAX_NUM_SEQS MAX_NUM_BATCHED_TOKENS \
1402
+ KV_CACHE_DTYPE MTP_TOKENS SPEC_METHOD DFLASH_TOKENS DFLASH_MODEL_DIR \
1403
+ DFLASH_DRAFT_TP \
1404
+ LANGUAGE_MODEL_ONLY SKIP_MM_PROFILING \
1405
+ LIMIT_MM CHAT_TEMPLATE ENFORCE_EAGER EXL3_FUSED_MOE EXL3_MOE_ROW_TILE EXL3_TEMP_ROWS_FUSED EXL3_FAT_SORTED EXL3_FAT_BATCHED EXL3_FAT_KERNEL MODEL_DIR EXTRA_ARGS \
1406
+ ABLIT ABLIT_METHOD ABLIT_DIRECTION ABLIT_LAYERS ABLIT_ALPHA ABLIT_INCLUDE_MTP; do
1407
+ serve_env+=" -e $v='${!v:-}'"
1408
+ done
1409
+ # VLLM_API_KEY is read by the head (rank 0) API server for bearer auth; the
1410
+ # worker runs --headless so it only needs the var for argv-parity, and
1411
+ # start.sh below passes it explicitly on the head. Keep it out of the
1412
+ # generic loop so the key never shows in process listings of either node
1413
+ # beyond the container env (same as the DeepSeek deployment).
1414
+ serve_env+=" -e VLLM_API_KEY='${VLLM_API_KEY:-}'"
1415
+
1416
+ local worker_preload="" nccl_dir cname
1417
+ for r in 1 2 3; do
1418
+ worker_preload=""
1419
+ if [ "$USE_HOST_NCCL" = "1" ]; then
1420
+ nccl_dir="$(_tp4_rank_nccl_dir "$r")"
1421
+ if worker_ssh_n "$r" "test -f '$nccl_dir/$NCCL_SO_NAME'"; then
1422
+ worker_preload="-v '$nccl_dir:/nccl:ro' -e LD_PRELOAD='/nccl/$NCCL_SO_NAME'"
1423
+ log "rank ${r}: LD_PRELOAD $NCCL_SO_NAME"
1424
+ else
1425
+ warn "rank ${r}: $nccl_dir/$NCCL_SO_NAME missing — using image NCCL"
1426
+ fi
1427
+ fi
1428
+ cname="$(_tp4_rank_container "$r")"
1429
+ log "starting rank ${r} on $(_tp4_ssh_target "$r") (NCCL if=$(_tp4_rank_cx7_if "$r") hca=$(_tp4_rank_cx7_ib "$r")) ..."
1430
+ [ -n "${LOCAL_MODEL_DIR:-}" ] && worker_preload="$worker_preload -v '$LOCAL_MODEL_DIR:/model:ro'"
1431
+ [ -n "${LOCAL_DFLASH_DIR:-}" ] && worker_preload="$worker_preload -v '$LOCAL_DFLASH_DIR:/draft:ro'"
1432
+ worker_ssh_n "$r" "docker run -d --name '$cname' \
1433
+ --gpus all --network host --ipc=host --shm-size 32g --stop-timeout 60 \
1434
+ --device /dev/infiniband --cap-add IPC_LOCK \
1435
+ --ulimit memlock=-1 --ulimit stack=67108864 --ulimit nofile=1048576:1048576 \
1436
+ -v '$(_tp4_rank_hf "$r"):/root/.cache/huggingface' \
1437
+ -v '$(_tp4_rank_vllm "$r"):/root/.cache/vllm' \
1438
+ -v '$(_tp4_rank_triton "$r"):/root/.triton/cache' \
1439
+ -v '$(_tp4_rank_tilelang "$r"):/root/.tilelang/cache' \
1440
+ -v '/tmp/${cname}.sh:/start.sh:ro' \
1441
+ -v '/tmp/glm53-chat_template.jinja:${CHAT_TEMPLATE}:ro' \
1442
+ -v '/tmp/patch_glm_video_placeholders.py:/opt/glm53/patch_glm_video_placeholders.py:ro' \
1443
+ -v '/tmp/patch_suppress_stops_in_reasoning.py:/opt/glm53/patch_suppress_stops_in_reasoning.py:ro' \
1444
+ -v '/tmp/patch_scheduler_decode_floor.py:/opt/glm53/patch_scheduler_decode_floor.py:ro' \
1445
+ -v '/tmp/patch_glm5_drafter_group.py:/opt/glm53/patch_glm5_drafter_group.py:ro' \
1446
+ -v '/tmp/patch_hybrid_prefix_hit.py:/opt/glm53/patch_hybrid_prefix_hit.py:ro' \
1447
+ -v '/tmp/patch_xgrammar_termination.py:/opt/glm53/patch_xgrammar_termination.py:ro' \
1448
+ -v '/tmp/patch_kpool_tail_slotmap.py:/opt/glm53/patch_kpool_tail_slotmap.py:ro' \
1449
+ -v '/tmp/patch_spinwait.py:/opt/glm53/patch_spinwait.py:ro' \
1450
+ -v '${ABLIT_HOST_DIR:-/tmp/glm53-ablit}:/opt/glm53/ablit:ro' \
1451
+ -v '/tmp/glm53-ablit_runtime.py:/opt/glm53/ablit_runtime.py:ro' \
1452
+ -v '/tmp/patch_ablit.py:/opt/glm53/patch_ablit.py:ro' \
1453
+ ${worker_preload} \
1454
+ ${worker_nccl} \
1455
+ -e NCCL_SOCKET_IFNAME='$(_tp4_rank_cx7_if "$r")' \
1456
+ -e GLOO_SOCKET_IFNAME='$(_tp4_rank_cx7_if "$r")' \
1457
+ -e NCCL_IB_HCA='$(_tp4_rank_cx7_ib "$r")' \
1458
+ -e NCCL_IB_GID_INDEX='$(_tp4_rank_gid "$r")' \
1459
+ -e VLLM_HOST_IP='$(_tp4_rank_ip "$r")' \
1460
+ -e NODE_RANK='$r' \
1461
+ ${serve_env} \
1462
+ --entrypoint bash '$IMAGE' /start.sh" >/dev/null
1463
+ done
1464
+
1465
+ log "starting head (vLLM API :${PORT}; NCCL if=${HEAD_CX7_IF} hca=${HEAD_CX7_IB}) ..."
1466
+ [ -n "${LOCAL_MODEL_DIR:-}" ] && head_preload+=(-v "$LOCAL_MODEL_DIR:/model:ro")
1467
+ [ -n "${LOCAL_DFLASH_DIR:-}" ] && head_preload+=(-v "$LOCAL_DFLASH_DIR:/draft:ro")
1468
+ docker run -d --name "$CONTAINER_HEAD" \
1469
+ --gpus all --network host --ipc=host --shm-size 32g --stop-timeout 60 \
1470
+ --device /dev/infiniband --cap-add IPC_LOCK \
1471
+ --ulimit memlock=-1 --ulimit stack=67108864 --ulimit nofile=1048576:1048576 \
1472
+ -v "$HF_CACHE_DIR:/root/.cache/huggingface" \
1473
+ -v "$CACHE_ROOT:/root/.cache/vllm" \
1474
+ -v "$TRITON_HOST_CACHE:/root/.triton/cache" \
1475
+ -v "$TILELANG_HOST_CACHE:/root/.tilelang/cache" \
1476
+ -v "$HEAD_SCRIPT:/start.sh:ro" \
1477
+ -v "$CHAT_TEMPLATE_HOST:$CHAT_TEMPLATE:ro" \
1478
+ -v "$VIDEO_PATCH_HOST:/opt/glm53/patch_glm_video_placeholders.py:ro" \
1479
+ -v "$STOP_PATCH_HOST:/opt/glm53/patch_suppress_stops_in_reasoning.py:ro" \
1480
+ -v "$SCHED_PATCH_HOST:/opt/glm53/patch_scheduler_decode_floor.py:ro" \
1481
+ -v "$DRAFTER_PATCH_HOST:/opt/glm53/patch_glm5_drafter_group.py:ro" \
1482
+ -v "$APC_PATCH_HOST:/opt/glm53/patch_hybrid_prefix_hit.py:ro" \
1483
+ -v "$XGRAMMAR_PATCH_HOST:/opt/glm53/patch_xgrammar_termination.py:ro" \
1484
+ -v "$KPOOL_TAIL_PATCH_HOST:/opt/glm53/patch_kpool_tail_slotmap.py:ro" \
1485
+ -v "$SPINWAIT_PATCH_HOST:/opt/glm53/patch_spinwait.py:ro" \
1486
+ -v "${ABLIT_HOST_DIR:-$SCRIPT_DIR/ablit}:/opt/glm53/ablit:ro" \
1487
+ -v "$SCRIPT_DIR/overlay/ablit_runtime.py:/opt/glm53/ablit_runtime.py:ro" \
1488
+ -v "$SCRIPT_DIR/overlay/patch_ablit.py:/opt/glm53/patch_ablit.py:ro" \
1489
+ "${head_preload[@]}" \
1490
+ "${nccl_common[@]}" \
1491
+ -e NCCL_SOCKET_IFNAME="$HEAD_CX7_IF" \
1492
+ -e GLOO_SOCKET_IFNAME="$HEAD_CX7_IF" \
1493
+ -e NCCL_IB_HCA="$HEAD_CX7_IB" \
1494
+ -e NCCL_IB_GID_INDEX="$HEAD_GID" \
1495
+ -e VLLM_HOST_IP="$HEAD_IP" \
1496
+ -e SERVED_MODEL_NAME="$SERVED_MODEL_NAME" \
1497
+ -e PORT="$PORT" -e TP="$TP" -e NNODES="$NNODES" \
1498
+ -e HEAD_IP="$HEAD_IP" -e MASTER_PORT="$MASTER_PORT" \
1499
+ -e QUANTIZATION="$QUANTIZATION" \
1500
+ -e MAX_MODEL_LEN="$MAX_MODEL_LEN" -e GPU_MEM_UTIL="$GPU_MEM_UTIL" \
1501
+ -e MAX_NUM_SEQS="$MAX_NUM_SEQS" \
1502
+ -e MAX_NUM_BATCHED_TOKENS="$MAX_NUM_BATCHED_TOKENS" \
1503
+ -e KV_CACHE_DTYPE="$KV_CACHE_DTYPE" -e MTP_TOKENS="$MTP_TOKENS" \
1504
+ -e SPEC_METHOD="$SPEC_METHOD" \
1505
+ -e DFLASH_TOKENS="${DFLASH_TOKENS:-7}" \
1506
+ -e DFLASH_MODEL_DIR="${DFLASH_MODEL_DIR:-}" \
1507
+ -e DFLASH_DRAFT_TP="${DFLASH_DRAFT_TP:-}" \
1508
+ -e LANGUAGE_MODEL_ONLY="$LANGUAGE_MODEL_ONLY" \
1509
+ -e SKIP_MM_PROFILING="$SKIP_MM_PROFILING" \
1510
+ -e LIMIT_MM="$LIMIT_MM" \
1511
+ -e CHAT_TEMPLATE="$CHAT_TEMPLATE" \
1512
+ -e ENFORCE_EAGER="$ENFORCE_EAGER" \
1513
+ -e EXL3_FUSED_MOE="$EXL3_FUSED_MOE" \
1514
+ -e EXL3_MOE_ROW_TILE="$EXL3_MOE_ROW_TILE" \
1515
+ -e EXL3_TEMP_ROWS_FUSED="$EXL3_TEMP_ROWS_FUSED" \
1516
+ -e EXL3_FAT_SORTED="$EXL3_FAT_SORTED" \
1517
+ -e EXL3_FAT_BATCHED="$EXL3_FAT_BATCHED" \
1518
+ -e EXL3_FAT_KERNEL="$EXL3_FAT_KERNEL" \
1519
+ -e ABLIT="$ABLIT" \
1520
+ -e ABLIT_METHOD="$ABLIT_METHOD" \
1521
+ -e ABLIT_DIRECTION="$ABLIT_DIRECTION" \
1522
+ -e ABLIT_LAYERS="$ABLIT_LAYERS" \
1523
+ -e ABLIT_ALPHA="$ABLIT_ALPHA" \
1524
+ -e ABLIT_INCLUDE_MTP="$ABLIT_INCLUDE_MTP" \
1525
+ -e MODEL_DIR="$MODEL_DIR" \
1526
+ -e VLLM_API_KEY="$VLLM_API_KEY" \
1527
+ -e EXTRA_ARGS="${EXTRA_ARGS:-}" \
1528
+ --entrypoint bash "$IMAGE" /start.sh >/dev/null
1529
+
1530
+ log "containers up — head=${CONTAINER_HEAD}, workers=${CONTAINER_WORKER} ${CONTAINER_WORKER2} ${CONTAINER_WORKER3}"
1531
+ }
1532
+
1533
+ # ---------------------------- health wait ----------------------------------
1534
+ wait_for_health() {
1535
+ local url="http://127.0.0.1:${PORT}/health"
1536
+ log "waiting for ${url} (weight load + warmup on a 320B MoE is slow; timeout ${READY_TIMEOUT}s) ..."
1537
+ log "streaming head logs live — Ctrl-C detaches, the server keeps running"
1538
+
1539
+ local logpid=""
1540
+ _stop_logtail() {
1541
+ [ -n "$logpid" ] && kill "$logpid" 2>/dev/null || true
1542
+ wait "$logpid" 2>/dev/null || true
1543
+ logpid=""
1544
+ }
1545
+ trap '_stop_logtail; warn "interrupted — containers keep running ('"'"'./start-tp4.sh logs'"'"' / '"'"'./start-tp4.sh stop'"'"')"; exit 130' INT
1546
+ docker logs -f --tail 0 "$CONTAINER_HEAD" 2>&1 &
1547
+ logpid=$!
1548
+
1549
+ local elapsed=0 healthy=0 exited=0 dead_side="" worker_fail=0
1550
+ while [ "$elapsed" -lt "$READY_TIMEOUT" ]; do
1551
+ if curl -fsS -m 5 "$url" >/dev/null 2>&1; then healthy=1; break; fi
1552
+ if ! docker inspect -f '{{.State.Running}}' "$CONTAINER_HEAD" 2>/dev/null | grep -q true; then
1553
+ log "head container exited during startup"
1554
+ exited=1; dead_side="head"; break
1555
+ fi
1556
+ # A dead worker rank can never make the head healthy — fail fast with
1557
+ # the log dump instead of polling for the full READY_TIMEOUT (issue
1558
+ # #22, item 4). Transient ssh/docker hiccups are tolerated; only
1559
+ # three consecutive non-running answers (~30 s) count as a dead
1560
+ # worker.
1561
+ local all_up=1 wr
1562
+ for wr in 1 2 3; do
1563
+ if worker_ssh_n "$wr" "docker inspect -f '{{.State.Running}}' '$(_tp4_rank_container "$wr")' 2>/dev/null" | grep -q true; then
1564
+ :
1565
+ else
1566
+ all_up=0
1567
+ dead_side="rank${wr}"
1568
+ fi
1569
+ done
1570
+ if [ "$all_up" = "1" ]; then
1571
+ worker_fail=0
1572
+ else
1573
+ worker_fail=$((worker_fail + 1))
1574
+ if [ "$worker_fail" -ge 3 ]; then
1575
+ log "a worker container is not running (${dead_side}, 3 consecutive checks)"
1576
+ exited=1; break
1577
+ fi
1578
+ fi
1579
+ sleep 10; elapsed=$((elapsed + 10))
1580
+ done
1581
+
1582
+ _stop_logtail
1583
+ trap 'warn "interrupted — containers keep running ('"'"'./start-tp4.sh logs'"'"' / '"'"'./start-tp4.sh stop'"'"')"; exit 130' INT
1584
+
1585
+ if [ "$healthy" = "1" ]; then
1586
+ log "health check passed after ${elapsed}s — server is up"
1587
+ elif [ "$exited" = "1" ]; then
1588
+ warn "${dead_side:-head} container exited/stopped after ${elapsed}s"
1589
+ else
1590
+ warn "timed out after ${elapsed}s without becoming healthy"
1591
+ fi
1592
+ [ "$healthy" = "1" ]
1593
+ }
1594
+
1595
+ post_ready_warmup() {
1596
+ if [ "${GLM53_BOOT_SHAPE_WARMUP:-1}" = "0" ]; then
1597
+ log "boot shape warmup skipped (GLM53_BOOT_SHAPE_WARMUP=0)"
1598
+ return 0
1599
+ fi
1600
+ [ -f "$SCRIPT_DIR/scripts/boot-shape-warmup.sh" ] \
1601
+ || { warn "boot-shape-warmup.sh missing — skipping"; return 0; }
1602
+ log "post-ready DFlash2/sampler warmup (nonfatal; timeout ${GLM53_WARMUP_REQ_TIMEOUT}s/req) ..."
1603
+ GLM53_WARMUP_MAX_CONCURRENCY="$MAX_NUM_SEQS" \
1604
+ GLM53_WARMUP_REQ_TIMEOUT="$GLM53_WARMUP_REQ_TIMEOUT" \
1605
+ GLM53_WARMUP_DFLASH_K="${DFLASH_TOKENS:-7}" \
1606
+ GLM53_WARMUP_TRITON_CACHE_DIR="$TRITON_HOST_CACHE" \
1607
+ GLM53_WARMUP_BEARER="${VLLM_API_KEY:-}" \
1608
+ bash "$SCRIPT_DIR/scripts/boot-shape-warmup.sh" \
1609
+ "http://127.0.0.1:${PORT}" "$SERVED_MODEL_NAME" \
1610
+ || warn "boot shape warmup incomplete — uncovered shapes may JIT mid-serve on TP=4"
1611
+ }
1612
+
1613
+ collect_failure_logs() {
1614
+ mkdir -p "$LOGDIR"
1615
+ docker logs "$CONTAINER_HEAD" >"$LOGDIR/head.log" 2>&1 || true
1616
+ local r
1617
+ for r in 1 2 3; do
1618
+ worker_ssh_n "$r" "docker logs '$(_tp4_rank_container "$r")' 2>&1" >"$LOGDIR/worker${r}.log" 2>&1 || true
1619
+ done
1620
+ }
1621
+
1622
+ on_ready() {
1623
+ log "======================================================================"
1624
+ log "GLM-5.3-Flash EXL3 is UP (TP=${TP}, nnodes=${NNODES})"
1625
+ log " endpoints : http://127.0.0.1:${PORT}/v1 (LAN: ${HEAD_IP}:${PORT})"
1626
+ log " model name : ${SERVED_MODEL_NAME}"
1627
+ log " weights : ${MODEL} quant=${QUANTIZATION} kv=${KV_CACHE_DTYPE}"
1628
+ local vision=on
1629
+ [ "${LANGUAGE_MODEL_ONLY}" = "1" ] && vision=off
1630
+ local spec="MTP k=${MTP_TOKENS}"
1631
+ [ "$SPEC_METHOD" = "dflash" ] && spec="DFlash2 k=${DFLASH_TOKENS} (${DFLASH_MODEL})"
1632
+ [ "$SPEC_METHOD" = "none" ] && spec=off
1633
+ local ablit="off (stock weights)"
1634
+ [ "$ABLIT" = "1" ] && ablit="ON method=${ABLIT_METHOD} direction=${ABLIT_DIRECTION} layers=${ABLIT_LAYERS} alpha=${ABLIT_ALPHA}"
1635
+ log " features : tools=glm47+auto, reasoning=glm45, spec=${spec}, vision=${vision}, ablit=${ablit}"
1636
+ local auth_line="none (VLLM_API_KEY empty)"
1637
+ if [ -n "${VLLM_API_KEY:-}" ]; then
1638
+ auth_line="bearer token set (VLLM_API_KEY) — send Authorization: Bearer <key> on /v1 requests"
1639
+ fi
1640
+ log " auth : ${auth_line}"
1641
+ log " quick test :"
1642
+ log " curl -s http://127.0.0.1:${PORT}/v1/chat/completions \\"
1643
+ if [ -n "${VLLM_API_KEY:-}" ]; then
1644
+ log " -H 'Authorization: Bearer <KEY>' \\"
1645
+ fi
1646
+ log " -H 'Content-Type: application/json' \\"
1647
+ log " -d '{\"model\": \"${SERVED_MODEL_NAME}\", \"messages\": [{\"role\": \"user\", \"content\": \"hello!\"}]}'"
1648
+ log " manage : ./start-tp4.sh status | ./start-tp4.sh logs | ./start-tp4.sh logs 1 | ./start-tp4.sh stop"
1649
+ log "======================================================================"
1650
+ if [ "${TAIL:-0}" = "1" ]; then
1651
+ log "tailing head logs — Ctrl-C just detaches, the server keeps running"
1652
+ trap '' INT
1653
+ docker logs -f --tail 20 "$CONTAINER_HEAD" || true
1654
+ trap 'warn "interrupted — containers keep running"; exit 130' INT
1655
+ fi
1656
+ }
1657
+
1658
+ # ------------------------------- start -------------------------------------
1659
+ start() {
1660
+ preflight
1661
+ ensure_image
1662
+ download_weights
1663
+ download_dflash
1664
+ sync_weights
1665
+ write_inner_scripts
1666
+
1667
+ if [ -n "${LOCAL_MODEL_DIR:-}" ]; then
1668
+ MODEL_DIR="/model"
1669
+ log "LOCAL_MODEL_DIR mode: $LOCAL_MODEL_DIR -> /model on all ranks"
1670
+ else
1671
+ MODEL_DIR="$(resolve_model_dir)"
1672
+ fi
1673
+ DFLASH_MODEL_DIR=""
1674
+ if [ "$SPEC_METHOD" = "dflash" ]; then
1675
+ if [ -n "${LOCAL_DFLASH_DIR:-}" ]; then
1676
+ DFLASH_MODEL_DIR="/draft"
1677
+ else
1678
+ DFLASH_MODEL_DIR="$(resolve_dflash_dir)"
1679
+ fi
1680
+ log "DFlash2 load path (in-container): ${DFLASH_MODEL_DIR}"
1681
+ fi
1682
+ log "model load path (in-container): ${MODEL_DIR}"
1683
+ log "config: image=${IMAGE} tp=${TP} nnodes=${NNODES} quant=${QUANTIZATION} spec=${SPEC_METHOD} mtp=${MTP_TOKENS} dflash_k=${DFLASH_TOKENS} max-len=${MAX_MODEL_LEN} gpu-util=${GPU_MEM_UTIL} kv=${KV_CACHE_DTYPE} lm-only=${LANGUAGE_MODEL_ONLY} port=${PORT}"
1684
+
1685
+ launch_cluster
1686
+ if wait_for_health; then
1687
+ post_ready_warmup
1688
+ on_ready
1689
+ return
1690
+ fi
1691
+ collect_failure_logs
1692
+ echo "---- last 60 lines of head log ($LOGDIR/head.log) ----"
1693
+ tail -n 60 "$LOGDIR/head.log" || true
1694
+ echo "---- last 40 lines of worker1 log ($LOGDIR/worker1.log) ----"
1695
+ tail -n 40 "$LOGDIR/worker1.log" || true
1696
+ echo "---- last 40 lines of worker2 log ($LOGDIR/worker2.log) ----"
1697
+ tail -n 40 "$LOGDIR/worker2.log" || true
1698
+ echo "---- last 40 lines of worker3 log ($LOGDIR/worker3.log) ----"
1699
+ tail -n 40 "$LOGDIR/worker3.log" || true
1700
+ die "server did not become healthy — full logs in $LOGDIR/"
1701
+ }
1702
+
1703
+ # ------------------------------- stop --------------------------------------
1704
+ stop() {
1705
+ local r
1706
+ log "stopping head container ..."
1707
+ docker rm -f "$CONTAINER_HEAD" >/dev/null 2>&1 || log " (no head container was running)"
1708
+ for r in 1 2 3; do
1709
+ log "stopping rank ${r} on $(_tp4_ssh_target "$r") ..."
1710
+ worker_ssh_n "$r" "docker rm -f '$(_tp4_rank_container "$r")'" >/dev/null 2>&1 \
1711
+ || log " (no rank ${r} container was running)"
1712
+ done
1713
+ log "stopped."
1714
+ }
1715
+
1716
+ # ------------------------------ status -------------------------------------
1717
+ status() {
1718
+ log "head (${CONTAINER_HEAD} on $(hostname)):"
1719
+ docker ps -a --filter "name=${CONTAINER_HEAD}" --format ' {{.Names}} {{.Status}}' || true
1720
+ if curl -fsS -m 5 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
1721
+ log " API: healthy — http://127.0.0.1:${PORT}/v1"
1722
+ else
1723
+ log " API: not responding"
1724
+ fi
1725
+ local r
1726
+ for r in 1 2 3; do
1727
+ log "rank ${r} ($(_tp4_rank_container "$r") on $(_tp4_ssh_target "$r")):"
1728
+ worker_ssh_n "$r" "docker ps -a --filter name=$(_tp4_rank_container "$r") --format ' {{.Names}} {{.Status}}'" 2>/dev/null \
1729
+ || log " (rank ${r} unreachable)"
1730
+ done
1731
+ }
1732
+
1733
+ # ------------------------------- logs --------------------------------------
1734
+ logs() {
1735
+ case "${1:-head}" in
1736
+ 1|2|3|worker|worker1)
1737
+ local r="${1}"
1738
+ if [ "$r" = "worker" ] || [ "$r" = "worker1" ]; then r=1; fi
1739
+ log "following rank ${r} logs on $(_tp4_ssh_target "$r") ..."
1740
+ trap '' INT
1741
+ worker_ssh_n "$r" "docker logs -f --tail 100 '$(_tp4_rank_container "$r")'" || true
1742
+ trap 'warn "interrupted"; exit 130' INT
1743
+ ;;
1744
+ head|*)
1745
+ log "following head logs (driver + API server) ..."
1746
+ trap '' INT
1747
+ docker logs -f --tail 100 "$CONTAINER_HEAD" || true
1748
+ trap 'warn "interrupted"; exit 130' INT
1749
+ ;;
1750
+ esac
1751
+ }
1752
+
1753
+ # ------------------------------- main --------------------------------------
1754
+ main() {
1755
+ local cmd="${1:-start}"
1756
+ case "$cmd" in
1757
+ start|restart) validate_numeric_config ;;
1758
+ esac
1759
+ case "$cmd" in
1760
+ stop) banner stop.sh ;;
1761
+ download) banner download.sh ;;
1762
+ *) banner start-tp4.sh ;;
1763
+ esac
1764
+ case "$cmd" in
1765
+ start) shift || true; start ;;
1766
+ download) download_only ;;
1767
+ stop) stop ;;
1768
+ restart) stop; start ;;
1769
+ status) status ;;
1770
+ logs) shift || true; logs "$@" ;;
1771
+ -h|--help|help) usage ;;
1772
+ *) usage; exit 1 ;;
1773
+ esac
1774
+ }
1775
+
1776
+ main "$@"
serving/taskbench.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Task-class x concurrency decode benchmark for the AEON serve (OpenAI chat API, vLLM metrics).
3
+ gen tok/s = completion tokens / (wall - TTFT) per stream; aggregate = sum tokens / max wall (decode window).
4
+ tok/pass = delta(spec accepted)/delta(drafts) + 1 from /metrics."""
5
+ import json, os, sys, time, threading, urllib.request, statistics, random
6
+ BASE=os.environ.get("BASE","http://localhost:8000"); MODEL=os.environ.get("MODEL","aeon"); OUT=int(os.environ.get("OUT","512"))
7
+ THINK=os.environ.get("THINK","0")=="1"
8
+ TOPICS=["a lighthouse keeper in 1890s Norway","a courier robot in a flooded Bangkok","a violinist who loses her hearing","a mountain village's last winter","two brothers repairing a boat","a night-shift nurse in Lagos","a chess prodigy in exile","a beekeeper during a drought"]
9
+ TASKS={
10
+ "prose": lambda i: f"Write a vivid literary short story (at least 700 words) about {TOPICS[i%8]}. No headings, flowing prose only.",
11
+ "code": lambda i: f"Write a complete, well-documented Python module implementing an LRU cache with TTL expiry, thread safety, statistics, and a small CLI demo. Variant #{i}: name the class Cache{i}. Code only, no prose.",
12
+ "list": lambda i: f"List 120 distinct {['programming concepts','world cities','chemical compounds','historical events','musical instruments','mathematical theorems','plant species','cooking techniques'][i%8]}, numbered, each with a one-line description.",
13
+ "essay": lambda i: f"Write a structured argumentative essay (about 800 words, with an introduction, three sections with headers, and a conclusion) on: '{['Should cities ban private cars?','Is remote work good for innovation?','Should AI systems have a right to refuse tasks?','Is nuclear power essential for decarbonization?','Should voting be mandatory?','Do social networks make us lonelier?','Should space exploration be privatized?','Is a four-day work week viable?'][i%8]}'",
14
+ }
15
+ def metrics():
16
+ t=urllib.request.urlopen(BASE+"/metrics",timeout=10).read().decode(); d={}
17
+ for l in t.splitlines():
18
+ for k in ("spec_decode_num_drafts_total","spec_decode_num_accepted_tokens_total"):
19
+ if l.startswith("vllm:"+k+"{"): d[k]=float(l.rsplit(" ",1)[1])
20
+ return d
21
+ def one(prompt, res, idx):
22
+ body={"model":MODEL,"messages":[{"role":"user","content":prompt}],"max_tokens":OUT,"stream":True,
23
+ "stream_options":{"include_usage":True},"chat_template_kwargs":{"enable_thinking":THINK}}
24
+ body.update(json.loads(os.environ.get("SAMPLING","{}")))
25
+ req=urllib.request.Request(BASE+"/v1/chat/completions",data=json.dumps(body).encode(),headers={"Content-Type":"application/json"})
26
+ t0=time.time(); ttft=None; ntok=0
27
+ with urllib.request.urlopen(req,timeout=1800) as r:
28
+ for line in r:
29
+ line=line.decode().strip()
30
+ if not line.startswith("data:") or line=="data: [DONE]": continue
31
+ ev=json.loads(line[5:])
32
+ if ev.get("usage"): ntok=ev["usage"]["completion_tokens"]
33
+ ch=ev.get("choices")
34
+ d=(ch[0].get("delta") or {}) if ch else {}
35
+ if ttft is None and (d.get("content") or d.get("reasoning_content") or d.get("reasoning")): ttft=time.time()-t0
36
+ wall=time.time()-t0; res[idx]=(ntok,ttft or wall,wall)
37
+ def run(task,conc):
38
+ m0=metrics(); res=[None]*conc; th=[threading.Thread(target=one,args=(TASKS[task](i+conc*7),res,i)) for i in range(conc)]
39
+ T=time.time(); [t.start() for t in th]; [t.join() for t in th]; W=time.time()-T; m1=metrics()
40
+ toks=sum(r[0] for r in res); per=[r[0]/(r[2]-r[1]) for r in res if r[2]>r[1]] or [float("nan")]
41
+ g=lambda m,k: m.get(k,0.0)
42
+ dr=g(m1,"spec_decode_num_drafts_total")-g(m0,"spec_decode_num_drafts_total"); ac=g(m1,"spec_decode_num_accepted_tokens_total")-g(m0,"spec_decode_num_accepted_tokens_total")
43
+ tpp=(ac/dr+1) if dr else float("nan"); ttft=statistics.median(r[1] for r in res)
44
+ return dict(task=task,conc=conc,agg=toks/W,per=statistics.mean(per),ttft=ttft,tpp=tpp,tok=toks/conc)
45
+ if __name__=="__main__":
46
+ tasks=sys.argv[1].split(",") if len(sys.argv)>1 else list(TASKS); concs=[int(c) for c in (sys.argv[2] if len(sys.argv)>2 else "1,2,4,8").split(",")]
47
+ run("prose",1) # warm
48
+ print(f"thinking={'on' if THINK else 'off'} out={OUT} sampling=server default (temp 0.6/top_p .95/top_k 20/rep 1.05)")
49
+ print(f"{'task':6} {'C':>2} {'agg t/s':>8} {'per-stream':>10} {'TTFT':>6} {'tok/pass':>8} {'tok/req':>7}")
50
+ for task in tasks:
51
+ for c in concs:
52
+ r=run(task,c); print(f"{r['task']:6} {r['conc']:>2} {r['agg']:8.1f} {r['per']:10.1f} {r['ttft']:6.2f} {r['tpp']:8.2f} {r['tok']:7.0f}",flush=True)
tokenizer_config.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "clean_up_tokenization_spaces": false,
4
+ "do_lower_case": false,
5
+ "eos_token": "<|endoftext|>",
6
+ "extra_special_tokens": [
7
+ "<|endoftext|>",
8
+ "[MASK]",
9
+ "[gMASK]",
10
+ "[sMASK]",
11
+ "<sop>",
12
+ "<eop>",
13
+ "<|system|>",
14
+ "<|user|>",
15
+ "<|assistant|>",
16
+ "<|observation|>",
17
+ "<|begin_of_image|>",
18
+ "<|end_of_image|>",
19
+ "<|begin_of_video|>",
20
+ "<|end_of_video|>",
21
+ "<|begin_of_audio|>",
22
+ "<|end_of_audio|>",
23
+ "<|begin_of_transcription|>",
24
+ "<|end_of_transcription|>"
25
+ ],
26
+ "is_local": true,
27
+ "model_max_length": 1048576,
28
+ "model_specific_special_tokens": {},
29
+ "pad_token": "<|endoftext|>",
30
+ "padding_side": "left",
31
+ "remove_space": false,
32
+ "tokenizer_class": "TokenizersBackend"
33
+ }