engine plugin (vllm + sglang)
Browse files- plugins/._pyproject.toml +0 -0
- plugins/._sglang_glm5v +0 -0
- plugins/._vllm_glm5v +0 -0
- plugins/pyproject.toml +20 -0
- plugins/sglang_glm5v/.___init__.py +0 -0
- plugins/sglang_glm5v/._glm5v.py +0 -0
- plugins/sglang_glm5v/._patch.py +0 -0
- plugins/sglang_glm5v/._patch_sglang_dsa_arch.py +0 -0
- plugins/sglang_glm5v/._processor.py +0 -0
- plugins/sglang_glm5v/__init__.py +18 -0
- plugins/sglang_glm5v/glm5v.py +92 -0
- plugins/sglang_glm5v/patch.py +162 -0
- plugins/sglang_glm5v/patch_sglang_dsa_arch.py +162 -0
- plugins/sglang_glm5v/processor.py +54 -0
- plugins/vllm_glm5v/.___init__.py +0 -0
- plugins/vllm_glm5v/._glm5v.py +0 -0
- plugins/vllm_glm5v/._glm5v_config.py +0 -0
- plugins/vllm_glm5v/._register.py +0 -0
- plugins/vllm_glm5v/__init__.py +2 -0
- plugins/vllm_glm5v/glm5v.py +241 -0
- plugins/vllm_glm5v/glm5v_config.py +92 -0
- plugins/vllm_glm5v/register.py +29 -0
plugins/._pyproject.toml
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/._sglang_glm5v
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/._vllm_glm5v
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/pyproject.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=68"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "glm5v-serve"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Serve GLM-5.2-Vision (GLM-5.2 text + MoonViT vision + PatchMerger projector) on vLLM and SGLang"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.10"
|
| 11 |
+
license = { text = "Apache-2.0" }
|
| 12 |
+
|
| 13 |
+
# vLLM discovers plugins through this entry-point group and calls them during
|
| 14 |
+
# engine init — so `pip install glm5v-serve` is all that stands between a stock
|
| 15 |
+
# `vllm serve baseten/GLM-5.2-Vision-FP8` and a working VLM. No flags, no patches.
|
| 16 |
+
[project.entry-points."vllm.general_plugins"]
|
| 17 |
+
glm5v = "vllm_glm5v.register:register"
|
| 18 |
+
|
| 19 |
+
[tool.setuptools]
|
| 20 |
+
packages = ["vllm_glm5v", "sglang_glm5v"]
|
plugins/sglang_glm5v/.___init__.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/sglang_glm5v/._glm5v.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/sglang_glm5v/._patch.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/sglang_glm5v/._patch_sglang_dsa_arch.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/sglang_glm5v/._processor.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/sglang_glm5v/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""sglang_glm5v: out-of-tree SGLang support for GLM-5.2-Vision.
|
| 3 |
+
|
| 4 |
+
MoonViT vision tower + trained PatchMerger projector + GLM-5.2 text backbone.
|
| 5 |
+
|
| 6 |
+
Registered through SGLang's shipped external-package hooks — no in-tree edits:
|
| 7 |
+
|
| 8 |
+
export SGLANG_EXTERNAL_MODEL_PACKAGE=sglang_glm5v # models (EntryClass)
|
| 9 |
+
export SGLANG_EXTERNAL_MM_PROCESSOR_PACKAGE=sglang_glm5v # image processor
|
| 10 |
+
export SGLANG_EXTERNAL_MM_MODEL_ARCH=Glm5vForConditionalGeneration
|
| 11 |
+
|
| 12 |
+
Those hooks do not cover one thing: SGLang gates its DeepSeek-sparse-attention
|
| 13 |
+
path on a hardcoded architecture list. Register the arch there with
|
| 14 |
+
|
| 15 |
+
python -m sglang_glm5v.patch
|
| 16 |
+
|
| 17 |
+
See patch.py for why that is a genuine gap rather than a workaround.
|
| 18 |
+
"""
|
plugins/sglang_glm5v/glm5v.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""glm5v on SGLang: MoonViT (Kimi-K2.5) vision tower + trained PatchMerger
|
| 3 |
+
projector + GLM-5.2 (GlmMoeDsa) text decoder.
|
| 4 |
+
|
| 5 |
+
Mirror of ``vllm_glm5v/glm5v.py`` for SGLang. It is SGLang's in-tree
|
| 6 |
+
``KimiK25ForConditionalGeneration`` with the same three swaps we made on vLLM:
|
| 7 |
+
|
| 8 |
+
1. text backbone ``DeepseekV3ForCausalLM`` -> ``GlmMoeDsaForCausalLM``
|
| 9 |
+
(GLM-5.2; SGLang ships it natively in ``glm4_moe.py``, DSA path included).
|
| 10 |
+
2. projector ``linear_2`` output dim -> GLM hidden 6144, via
|
| 11 |
+
``vision_config.text_hidden_size`` (pure config, no code).
|
| 12 |
+
3. image placeholder -> GLM's ``<|image|>`` (154854), via
|
| 13 |
+
``config.media_placeholder_token_id`` + the Glm5v processor (see
|
| 14 |
+
``processor.py``; the model itself is placeholder-agnostic).
|
| 15 |
+
|
| 16 |
+
Only swap (1) needs code, so ``__init__`` is a copy of the parent's with the
|
| 17 |
+
language-model class replaced. Everything else (MoonViT tower, projector,
|
| 18 |
+
``get_image_feature``, ``forward``, ``load_weights`` incl. the
|
| 19 |
+
``mm_projector.proj.0 -> linear_1`` key remap) is inherited verbatim.
|
| 20 |
+
|
| 21 |
+
Vision tower + projector stay bf16: the parent passes ``quant_config=None`` to
|
| 22 |
+
the tower (unless ModelSlim) and builds the projector unquantized; only the GLM
|
| 23 |
+
text backbone sees the FP8 quant config.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import logging
|
| 27 |
+
|
| 28 |
+
from torch import nn
|
| 29 |
+
|
| 30 |
+
from sglang.srt.layers.quantization.modelslim.modelslim import ModelSlimConfig
|
| 31 |
+
from sglang.srt.layers.quantization.quark.quark import QuarkConfig
|
| 32 |
+
from sglang.srt.models.glm4_moe import GlmMoeDsaForCausalLM
|
| 33 |
+
from sglang.srt.models.kimi_k25 import (
|
| 34 |
+
K2VLMultiModalProjector,
|
| 35 |
+
KimiK25ForConditionalGeneration,
|
| 36 |
+
MoonViT3dPretrainedModel,
|
| 37 |
+
)
|
| 38 |
+
from sglang.srt.server_args import get_global_server_args
|
| 39 |
+
|
| 40 |
+
logger = logging.getLogger(__name__)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class Glm5vForConditionalGeneration(KimiK25ForConditionalGeneration):
|
| 44 |
+
"""glm5v: MoonViT vision + trained projector + GLM-5.2 text."""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
config,
|
| 49 |
+
quant_config=None,
|
| 50 |
+
prefix: str = "",
|
| 51 |
+
**kwargs, # match parent (absorbs init_tts etc.)
|
| 52 |
+
) -> None:
|
| 53 |
+
# Skip KimiK25ForConditionalGeneration.__init__ (it hardcodes the
|
| 54 |
+
# DeepseekV3 text class); replicate its body with GlmMoeDsa instead.
|
| 55 |
+
nn.Module.__init__(self)
|
| 56 |
+
self.config = config
|
| 57 |
+
self.quant_config = quant_config
|
| 58 |
+
self.use_data_parallel = get_global_server_args().mm_enable_dp_encoder
|
| 59 |
+
|
| 60 |
+
# Vision tower (bf16; quant only under ModelSlim, same as parent).
|
| 61 |
+
self.vision_tower = MoonViT3dPretrainedModel(
|
| 62 |
+
config.vision_config,
|
| 63 |
+
use_data_parallel=self.use_data_parallel,
|
| 64 |
+
quant_config=(
|
| 65 |
+
quant_config if isinstance(quant_config, ModelSlimConfig) else None
|
| 66 |
+
),
|
| 67 |
+
prefix="vision_tower",
|
| 68 |
+
)
|
| 69 |
+
# MM projector (unquantized; linear_2 out = vision_config.text_hidden_size).
|
| 70 |
+
self.mm_projector = K2VLMultiModalProjector(config.vision_config)
|
| 71 |
+
|
| 72 |
+
self.language_model = None
|
| 73 |
+
if not config.encoder_only:
|
| 74 |
+
# THE swap: GLM-5.2 (MLA + MoE + DSA sparse attention) text backbone.
|
| 75 |
+
self.language_model = GlmMoeDsaForCausalLM(
|
| 76 |
+
config.text_config,
|
| 77 |
+
quant_config,
|
| 78 |
+
prefix=(
|
| 79 |
+
"language_model"
|
| 80 |
+
if isinstance(quant_config, (ModelSlimConfig, QuarkConfig))
|
| 81 |
+
else ""
|
| 82 |
+
),
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Match vision/projector dtype to the language model (parent behavior).
|
| 86 |
+
if self.language_model is not None and hasattr(self.language_model, "dtype"):
|
| 87 |
+
target_dtype = self.language_model.dtype
|
| 88 |
+
self.vision_tower = self.vision_tower.to(dtype=target_dtype)
|
| 89 |
+
self.mm_projector = self.mm_projector.to(dtype=target_dtype)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
EntryClass = [Glm5vForConditionalGeneration]
|
plugins/sglang_glm5v/patch.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
"""Register Glm5vForConditionalGeneration as a DSA-capable architecture in SGLang.
|
| 4 |
+
|
| 5 |
+
WHY THIS PATCH IS NECESSARY (not an environment workaround):
|
| 6 |
+
SGLang gates the DeepSeek-Sparse-Attention path on a hardcoded architecture
|
| 7 |
+
tuple in ``is_deepseek_dsa`` (``srt/configs/model_config.py``). Upstream's own
|
| 8 |
+
DSA vision-language model (Mistral Large 3) is supported by adding its VLM
|
| 9 |
+
arch ``PixtralForConditionalGeneration`` to exactly this tuple in-tree — that
|
| 10 |
+
is the upstream mechanism; no env-var/out-of-tree hook exists for it (unlike
|
| 11 |
+
models/processors/mm-arch, which we register via the shipped
|
| 12 |
+
``SGLANG_EXTERNAL_*`` hooks). Our GLM5V wrapper needs the same one-line entry
|
| 13 |
+
so the DSA attention backend (and the ``server_args`` DSA backend defaults)
|
| 14 |
+
recognize the model whose text backbone is GlmMoeDsa.
|
| 15 |
+
|
| 16 |
+
Three insertions, each at a place where upstream registered its own DSA/MLA
|
| 17 |
+
VLM archs (KimiK25 / Pixtral precedents):
|
| 18 |
+
|
| 19 |
+
1. ``model_config.py``: the ``is_deepseek_dsa`` arch tuple — gates the DSA
|
| 20 |
+
attention backend;
|
| 21 |
+
2. ``model_config.py``: the MLA attention-arch condition — without it the
|
| 22 |
+
KV pool is built as MHA and the DSA backend crashes on
|
| 23 |
+
``dsa_kv_cache_store_fp8``;
|
| 24 |
+
3. ``server_args.py``: the DSA model-adjustments gate list — sets the DSA
|
| 25 |
+
dense-attn kv-len threshold (= model index_topk) and the prefill/decode
|
| 26 |
+
DSA backend defaults (trtllm on Blackwell); without it decode dies with
|
| 27 |
+
``Unsupported self.dsa_decode_impl = None``.
|
| 28 |
+
|
| 29 |
+
A 4th insertion (added later) enables MTP/NEXTN speculative decoding: without
|
| 30 |
+
it the draft worker rebuilds the entire ~90GB multimodal model as the "draft"
|
| 31 |
+
and OOMs (our wrapper arch is in none of SGLang's draft-model rewrite lists).
|
| 32 |
+
See the ``DRAFT_ANCHOR`` block below. This is only needed when serving with
|
| 33 |
+
``--speculative-algorithm NEXTN``; it is a no-op otherwise. (Measured: MTP gives
|
| 34 |
+
~1.1-1.16x tok/s at low/moderate batch + ~2.0 accept-len, greedy outputs are
|
| 35 |
+
token-identical to non-spec, but it is graph-capped at batch 256 so plain
|
| 36 |
+
high-batch TP8 wins for throughput-bound RL rollout — MTP is a single-stream
|
| 37 |
+
LATENCY lever, not a throughput one.)
|
| 38 |
+
|
| 39 |
+
Other in-tree arch lists mentioning GlmMoeDsa/KimiK25 are deliberately NOT
|
| 40 |
+
patched: they gate features we do not use (encoder disaggregation) or pure
|
| 41 |
+
performance defaults we set explicitly (flashinfer allreduce fusion,
|
| 42 |
+
attention-backend fallback).
|
| 43 |
+
|
| 44 |
+
The same entries will be needed wherever slime launches SGLang rollout engines
|
| 45 |
+
for glm5v (Milestone 1) — candidate for an upstream sglang PR.
|
| 46 |
+
|
| 47 |
+
Idempotent; run before ``sglang.launch_server`` (serve.sh does this).
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
import pathlib
|
| 51 |
+
import subprocess
|
| 52 |
+
import sys
|
| 53 |
+
|
| 54 |
+
ARCH = "Glm5vForConditionalGeneration"
|
| 55 |
+
DSA_ANCHOR = '"GlmMoeDsaForCausalLM",'
|
| 56 |
+
DSA_INSERT = (
|
| 57 |
+
DSA_ANCHOR
|
| 58 |
+
+ f'\n "{ARCH}", # glm5v: GLM-5.2 (DSA) text + MoonViT vision (sglang_glm5v)'
|
| 59 |
+
)
|
| 60 |
+
MLA_ANCHOR = 'or "KimiK25ForConditionalGeneration" in self.hf_config.architectures'
|
| 61 |
+
MLA_INSERT = (
|
| 62 |
+
MLA_ANCHOR
|
| 63 |
+
+ f'\n or "{ARCH}" in self.hf_config.architectures # glm5v (sglang_glm5v)'
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# server_args.py DSA adjustments gate: the list containing KimiK25 is unique
|
| 68 |
+
# to that block (other GlmMoeDsa lists in the file lack it).
|
| 69 |
+
SA_ANCHOR = (
|
| 70 |
+
'"KimiK25ForConditionalGeneration",\n'
|
| 71 |
+
' "MistralLarge3ForCausalLM",\n'
|
| 72 |
+
' "PixtralForConditionalGeneration",\n'
|
| 73 |
+
' "GlmMoeDsaForCausalLM",'
|
| 74 |
+
)
|
| 75 |
+
SA_INSERT = SA_ANCHOR + f'\n "{ARCH}", # glm5v (sglang_glm5v)'
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# --- MTP / speculative NEXTN draft-model support (4th insertion) ---
|
| 79 |
+
# WHY: with --speculative-algorithm NEXTN, SGLang builds a SEPARATE draft model
|
| 80 |
+
# (is_draft_model=True) whose arch it rewrites to a single-layer NextN module in
|
| 81 |
+
# ``_config_draft_model``. That method maps the *text* backbone
|
| 82 |
+
# ``GlmMoeDsaForCausalLM`` -> ``DeepseekV3ForCausalLMNextN`` (correct: our MTP
|
| 83 |
+
# weights live at text layer 78: eh_proj/enorm/hnorm + one block). But our
|
| 84 |
+
# TOP-LEVEL arch is the multimodal wrapper ``Glm5vForConditionalGeneration``,
|
| 85 |
+
# which is in NONE of the rewrite lists -> the draft worker instead rebuilds the
|
| 86 |
+
# ENTIRE multimodal model (MoonViT + full 78-layer 90GB text model) as the
|
| 87 |
+
# "draft" and OOMs (a second ~90GB copy on top of base weights + KV + graphs).
|
| 88 |
+
#
|
| 89 |
+
# FIX: mirror the shipped ``Step3p7ForConditionalGeneration`` precedent (a VLM
|
| 90 |
+
# wrapper whose draft is its text MTP): when is_draft_model and arch is our
|
| 91 |
+
# wrapper, swap hf_config -> hf_text_config and set arch to GlmMoeDsaForCausalLM.
|
| 92 |
+
# Inserted at the TOP of _config_draft_model so the existing
|
| 93 |
+
# GlmMoeDsaForCausalLM -> DeepseekV3ForCausalLMNextN rule then fires on it.
|
| 94 |
+
DRAFT_ANCHOR = (
|
| 95 |
+
" def _config_draft_model(self):\n"
|
| 96 |
+
" is_draft_model = self.is_draft_model\n"
|
| 97 |
+
)
|
| 98 |
+
DRAFT_INSERT = (
|
| 99 |
+
DRAFT_ANCHOR
|
| 100 |
+
+ "\n"
|
| 101 |
+
+ " # glm5v (sglang_glm5v): map the multimodal wrapper draft to its\n"
|
| 102 |
+
+ " # text backbone so the GlmMoeDsa->NextN rewrite below applies.\n"
|
| 103 |
+
+ f' if is_draft_model and self.hf_config.architectures[0] == "{ARCH}":\n'
|
| 104 |
+
+ " self.hf_config = self.hf_text_config\n"
|
| 105 |
+
+ ' self.hf_config.architectures = ["GlmMoeDsaForCausalLM"]\n'
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def find_module(mod: str) -> pathlib.Path:
|
| 110 |
+
out = subprocess.run(
|
| 111 |
+
[sys.executable, "-c", f"import {mod} as m; print(m.__file__)"],
|
| 112 |
+
capture_output=True,
|
| 113 |
+
text=True,
|
| 114 |
+
check=True,
|
| 115 |
+
)
|
| 116 |
+
return pathlib.Path(out.stdout.strip())
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def apply(path: pathlib.Path, anchor: str, insert: str, marker: str, label: str) -> None:
|
| 120 |
+
src = path.read_text()
|
| 121 |
+
if marker in src:
|
| 122 |
+
print(f"{label}: already patched")
|
| 123 |
+
return
|
| 124 |
+
assert anchor in src, f"{label}: anchor not found in {path} (sglang version changed?)"
|
| 125 |
+
path.write_text(src.replace(anchor, insert, 1))
|
| 126 |
+
print(f"{label}: patched ({path})")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def main() -> None:
|
| 130 |
+
mc = find_module("sglang.srt.configs.model_config")
|
| 131 |
+
apply(mc, DSA_ANCHOR, DSA_INSERT, f'"{ARCH}", # glm5v', "DSA tuple")
|
| 132 |
+
apply(mc, MLA_ANCHOR, MLA_INSERT, f'or "{ARCH}" in self.hf_config.architectures', "MLA condition")
|
| 133 |
+
apply(
|
| 134 |
+
mc,
|
| 135 |
+
DRAFT_ANCHOR,
|
| 136 |
+
DRAFT_INSERT,
|
| 137 |
+
f'if is_draft_model and self.hf_config.architectures[0] == "{ARCH}"',
|
| 138 |
+
"NEXTN draft-model map",
|
| 139 |
+
)
|
| 140 |
+
sa = find_module("sglang.srt.server_args")
|
| 141 |
+
apply(sa, SA_ANCHOR, SA_INSERT, f'"{ARCH}", # glm5v', "server_args DSA gate")
|
| 142 |
+
|
| 143 |
+
# verify the DSA gate end-to-end
|
| 144 |
+
check = subprocess.run(
|
| 145 |
+
[
|
| 146 |
+
sys.executable,
|
| 147 |
+
"-c",
|
| 148 |
+
(
|
| 149 |
+
"from sglang.srt.configs.model_config import is_deepseek_dsa;"
|
| 150 |
+
f"print(is_deepseek_dsa({{'architectures': ['{ARCH}'], 'index_topk': 2048}}))"
|
| 151 |
+
),
|
| 152 |
+
],
|
| 153 |
+
capture_output=True,
|
| 154 |
+
text=True,
|
| 155 |
+
check=True,
|
| 156 |
+
)
|
| 157 |
+
assert check.stdout.strip() == "True", check.stdout + check.stderr
|
| 158 |
+
print("verified: is_deepseek_dsa recognizes the arch")
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
main()
|
plugins/sglang_glm5v/patch_sglang_dsa_arch.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
"""Register Glm5vForConditionalGeneration as a DSA-capable architecture in SGLang.
|
| 4 |
+
|
| 5 |
+
WHY THIS PATCH IS NECESSARY (not an environment workaround):
|
| 6 |
+
SGLang gates the DeepSeek-Sparse-Attention path on a hardcoded architecture
|
| 7 |
+
tuple in ``is_deepseek_dsa`` (``srt/configs/model_config.py``). Upstream's own
|
| 8 |
+
DSA vision-language model (Mistral Large 3) is supported by adding its VLM
|
| 9 |
+
arch ``PixtralForConditionalGeneration`` to exactly this tuple in-tree — that
|
| 10 |
+
is the upstream mechanism; no env-var/out-of-tree hook exists for it (unlike
|
| 11 |
+
models/processors/mm-arch, which we register via the shipped
|
| 12 |
+
``SGLANG_EXTERNAL_*`` hooks). Our GLM5V wrapper needs the same one-line entry
|
| 13 |
+
so the DSA attention backend (and the ``server_args`` DSA backend defaults)
|
| 14 |
+
recognize the model whose text backbone is GlmMoeDsa.
|
| 15 |
+
|
| 16 |
+
Three insertions, each at a place where upstream registered its own DSA/MLA
|
| 17 |
+
VLM archs (KimiK25 / Pixtral precedents):
|
| 18 |
+
|
| 19 |
+
1. ``model_config.py``: the ``is_deepseek_dsa`` arch tuple — gates the DSA
|
| 20 |
+
attention backend;
|
| 21 |
+
2. ``model_config.py``: the MLA attention-arch condition — without it the
|
| 22 |
+
KV pool is built as MHA and the DSA backend crashes on
|
| 23 |
+
``dsa_kv_cache_store_fp8``;
|
| 24 |
+
3. ``server_args.py``: the DSA model-adjustments gate list — sets the DSA
|
| 25 |
+
dense-attn kv-len threshold (= model index_topk) and the prefill/decode
|
| 26 |
+
DSA backend defaults (trtllm on Blackwell); without it decode dies with
|
| 27 |
+
``Unsupported self.dsa_decode_impl = None``.
|
| 28 |
+
|
| 29 |
+
A 4th insertion (added later) enables MTP/NEXTN speculative decoding: without
|
| 30 |
+
it the draft worker rebuilds the entire ~90GB multimodal model as the "draft"
|
| 31 |
+
and OOMs (our wrapper arch is in none of SGLang's draft-model rewrite lists).
|
| 32 |
+
See the ``DRAFT_ANCHOR`` block below. This is only needed when serving with
|
| 33 |
+
``--speculative-algorithm NEXTN``; it is a no-op otherwise. (Measured: MTP gives
|
| 34 |
+
~1.1-1.16x tok/s at low/moderate batch + ~2.0 accept-len, greedy outputs are
|
| 35 |
+
token-identical to non-spec, but it is graph-capped at batch 256 so plain
|
| 36 |
+
high-batch TP8 wins for throughput-bound RL rollout — MTP is a single-stream
|
| 37 |
+
LATENCY lever, not a throughput one.)
|
| 38 |
+
|
| 39 |
+
Other in-tree arch lists mentioning GlmMoeDsa/KimiK25 are deliberately NOT
|
| 40 |
+
patched: they gate features we do not use (encoder disaggregation) or pure
|
| 41 |
+
performance defaults we set explicitly (flashinfer allreduce fusion,
|
| 42 |
+
attention-backend fallback).
|
| 43 |
+
|
| 44 |
+
The same entries will be needed wherever slime launches SGLang rollout engines
|
| 45 |
+
for glm5v (Milestone 1) — candidate for an upstream sglang PR.
|
| 46 |
+
|
| 47 |
+
Idempotent; run before ``sglang.launch_server`` (serve.sh does this).
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
import pathlib
|
| 51 |
+
import subprocess
|
| 52 |
+
import sys
|
| 53 |
+
|
| 54 |
+
ARCH = "Glm5vForConditionalGeneration"
|
| 55 |
+
DSA_ANCHOR = '"GlmMoeDsaForCausalLM",'
|
| 56 |
+
DSA_INSERT = (
|
| 57 |
+
DSA_ANCHOR
|
| 58 |
+
+ f'\n "{ARCH}", # glm5v: GLM-5.2 (DSA) text + MoonViT vision (sglang_glm5v)'
|
| 59 |
+
)
|
| 60 |
+
MLA_ANCHOR = 'or "KimiK25ForConditionalGeneration" in self.hf_config.architectures'
|
| 61 |
+
MLA_INSERT = (
|
| 62 |
+
MLA_ANCHOR
|
| 63 |
+
+ f'\n or "{ARCH}" in self.hf_config.architectures # glm5v (sglang_glm5v)'
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# server_args.py DSA adjustments gate: the list containing KimiK25 is unique
|
| 68 |
+
# to that block (other GlmMoeDsa lists in the file lack it).
|
| 69 |
+
SA_ANCHOR = (
|
| 70 |
+
'"KimiK25ForConditionalGeneration",\n'
|
| 71 |
+
' "MistralLarge3ForCausalLM",\n'
|
| 72 |
+
' "PixtralForConditionalGeneration",\n'
|
| 73 |
+
' "GlmMoeDsaForCausalLM",'
|
| 74 |
+
)
|
| 75 |
+
SA_INSERT = SA_ANCHOR + f'\n "{ARCH}", # glm5v (sglang_glm5v)'
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# --- MTP / speculative NEXTN draft-model support (4th insertion) ---
|
| 79 |
+
# WHY: with --speculative-algorithm NEXTN, SGLang builds a SEPARATE draft model
|
| 80 |
+
# (is_draft_model=True) whose arch it rewrites to a single-layer NextN module in
|
| 81 |
+
# ``_config_draft_model``. That method maps the *text* backbone
|
| 82 |
+
# ``GlmMoeDsaForCausalLM`` -> ``DeepseekV3ForCausalLMNextN`` (correct: our MTP
|
| 83 |
+
# weights live at text layer 78: eh_proj/enorm/hnorm + one block). But our
|
| 84 |
+
# TOP-LEVEL arch is the multimodal wrapper ``Glm5vForConditionalGeneration``,
|
| 85 |
+
# which is in NONE of the rewrite lists -> the draft worker instead rebuilds the
|
| 86 |
+
# ENTIRE multimodal model (MoonViT + full 78-layer 90GB text model) as the
|
| 87 |
+
# "draft" and OOMs (a second ~90GB copy on top of base weights + KV + graphs).
|
| 88 |
+
#
|
| 89 |
+
# FIX: mirror the shipped ``Step3p7ForConditionalGeneration`` precedent (a VLM
|
| 90 |
+
# wrapper whose draft is its text MTP): when is_draft_model and arch is our
|
| 91 |
+
# wrapper, swap hf_config -> hf_text_config and set arch to GlmMoeDsaForCausalLM.
|
| 92 |
+
# Inserted at the TOP of _config_draft_model so the existing
|
| 93 |
+
# GlmMoeDsaForCausalLM -> DeepseekV3ForCausalLMNextN rule then fires on it.
|
| 94 |
+
DRAFT_ANCHOR = (
|
| 95 |
+
" def _config_draft_model(self):\n"
|
| 96 |
+
" is_draft_model = self.is_draft_model\n"
|
| 97 |
+
)
|
| 98 |
+
DRAFT_INSERT = (
|
| 99 |
+
DRAFT_ANCHOR
|
| 100 |
+
+ "\n"
|
| 101 |
+
+ " # glm5v (sglang_glm5v): map the multimodal wrapper draft to its\n"
|
| 102 |
+
+ " # text backbone so the GlmMoeDsa->NextN rewrite below applies.\n"
|
| 103 |
+
+ f' if is_draft_model and self.hf_config.architectures[0] == "{ARCH}":\n'
|
| 104 |
+
+ " self.hf_config = self.hf_text_config\n"
|
| 105 |
+
+ ' self.hf_config.architectures = ["GlmMoeDsaForCausalLM"]\n'
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def find_module(mod: str) -> pathlib.Path:
|
| 110 |
+
out = subprocess.run(
|
| 111 |
+
[sys.executable, "-c", f"import {mod} as m; print(m.__file__)"],
|
| 112 |
+
capture_output=True,
|
| 113 |
+
text=True,
|
| 114 |
+
check=True,
|
| 115 |
+
)
|
| 116 |
+
return pathlib.Path(out.stdout.strip())
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def apply(path: pathlib.Path, anchor: str, insert: str, marker: str, label: str) -> None:
|
| 120 |
+
src = path.read_text()
|
| 121 |
+
if marker in src:
|
| 122 |
+
print(f"{label}: already patched")
|
| 123 |
+
return
|
| 124 |
+
assert anchor in src, f"{label}: anchor not found in {path} (sglang version changed?)"
|
| 125 |
+
path.write_text(src.replace(anchor, insert, 1))
|
| 126 |
+
print(f"{label}: patched ({path})")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def main() -> None:
|
| 130 |
+
mc = find_module("sglang.srt.configs.model_config")
|
| 131 |
+
apply(mc, DSA_ANCHOR, DSA_INSERT, f'"{ARCH}", # glm5v', "DSA tuple")
|
| 132 |
+
apply(mc, MLA_ANCHOR, MLA_INSERT, f'or "{ARCH}" in self.hf_config.architectures', "MLA condition")
|
| 133 |
+
apply(
|
| 134 |
+
mc,
|
| 135 |
+
DRAFT_ANCHOR,
|
| 136 |
+
DRAFT_INSERT,
|
| 137 |
+
f'if is_draft_model and self.hf_config.architectures[0] == "{ARCH}"',
|
| 138 |
+
"NEXTN draft-model map",
|
| 139 |
+
)
|
| 140 |
+
sa = find_module("sglang.srt.server_args")
|
| 141 |
+
apply(sa, SA_ANCHOR, SA_INSERT, f'"{ARCH}", # glm5v', "server_args DSA gate")
|
| 142 |
+
|
| 143 |
+
# verify the DSA gate end-to-end
|
| 144 |
+
check = subprocess.run(
|
| 145 |
+
[
|
| 146 |
+
sys.executable,
|
| 147 |
+
"-c",
|
| 148 |
+
(
|
| 149 |
+
"from sglang.srt.configs.model_config import is_deepseek_dsa;"
|
| 150 |
+
f"print(is_deepseek_dsa({{'architectures': ['{ARCH}'], 'index_topk': 2048}}))"
|
| 151 |
+
),
|
| 152 |
+
],
|
| 153 |
+
capture_output=True,
|
| 154 |
+
text=True,
|
| 155 |
+
check=True,
|
| 156 |
+
)
|
| 157 |
+
assert check.stdout.strip() == "True", check.stdout + check.stderr
|
| 158 |
+
print("verified: is_deepseek_dsa recognizes the arch")
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
main()
|
plugins/sglang_glm5v/processor.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""SGLang multimodal processor for glm5v.
|
| 3 |
+
|
| 4 |
+
Subclass of the in-tree Kimi-K2.5 GPU image processor with GLM's image
|
| 5 |
+
placeholder: ``<|image|>`` (id from ``config.media_placeholder_token_id``,
|
| 6 |
+
154854) instead of Kimi's ``<|media_pad|>``. The GLM chat template
|
| 7 |
+
(``chat_template.jinja`` in the assembled checkpoint) wraps each image as
|
| 8 |
+
``<|begin_of_image|><|image|><|end_of_image|>``; only the inner ``<|image|>``
|
| 9 |
+
run is expanded to per-patch tokens, exactly as in the vLLM deployment.
|
| 10 |
+
|
| 11 |
+
All preprocessing (NaViT resize math, GPU patchify, grid_thw handling) is
|
| 12 |
+
inherited unchanged — it is the same MoonViT pipeline the projector was
|
| 13 |
+
trained against.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import re
|
| 17 |
+
|
| 18 |
+
from sglang.srt.multimodal.processors.base_processor import MultimodalSpecialTokens
|
| 19 |
+
from sglang.srt.multimodal.processors.kimi_k25 import (
|
| 20 |
+
KimiGPUProcessorWrapper,
|
| 21 |
+
KimiK2_5VLImageProcessor,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
from sglang_glm5v.glm5v import Glm5vForConditionalGeneration
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Glm5vImageProcessor(KimiK2_5VLImageProcessor):
|
| 28 |
+
models = [Glm5vForConditionalGeneration]
|
| 29 |
+
|
| 30 |
+
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
| 31 |
+
# Call the grandparent chain directly: the parent __init__ hardcodes
|
| 32 |
+
# Kimi's <|media_pad|> token; we rebuild mm_tokens + the GPU wrapper
|
| 33 |
+
# with GLM's <|image|> and keep everything else identical.
|
| 34 |
+
super(KimiK2_5VLImageProcessor, self).__init__(
|
| 35 |
+
hf_config, server_args, _processor, *args, **kwargs
|
| 36 |
+
)
|
| 37 |
+
self.mm_tokens = MultimodalSpecialTokens(
|
| 38 |
+
image_token="<|image|>",
|
| 39 |
+
image_token_id=hf_config.media_placeholder_token_id,
|
| 40 |
+
image_token_regex=re.compile(r"(?:<\|image\|>)+"),
|
| 41 |
+
).build(_processor)
|
| 42 |
+
|
| 43 |
+
media_proc_cfg = _processor.media_processor.media_proc_cfg
|
| 44 |
+
self._processor = KimiGPUProcessorWrapper(
|
| 45 |
+
_processor,
|
| 46 |
+
image_token=self.mm_tokens.image_token,
|
| 47 |
+
patch_size=media_proc_cfg["patch_size"],
|
| 48 |
+
merge_kernel_size=media_proc_cfg["merge_kernel_size"],
|
| 49 |
+
in_patch_limit=media_proc_cfg["in_patch_limit"],
|
| 50 |
+
patch_limit_on_one_side=media_proc_cfg["patch_limit_on_one_side"],
|
| 51 |
+
fixed_output_tokens=media_proc_cfg.get("fixed_output_tokens"),
|
| 52 |
+
image_mean=media_proc_cfg["image_mean"],
|
| 53 |
+
image_std=media_proc_cfg["image_std"],
|
| 54 |
+
)
|
plugins/vllm_glm5v/.___init__.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/vllm_glm5v/._glm5v.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/vllm_glm5v/._glm5v_config.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/vllm_glm5v/._register.py
ADDED
|
Binary file (163 Bytes). View file
|
|
|
plugins/vllm_glm5v/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""glm5v: out-of-tree vLLM VLM (MoonViT + projector + GLM-5.2)."""
|
plugins/vllm_glm5v/glm5v.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""
|
| 3 |
+
glm5v: out-of-tree vLLM model = MoonViT (Kimi-K2.6) vision tower + trained MLP
|
| 4 |
+
projector + GLM-5.2 (GlmMoeDsa) text decoder.
|
| 5 |
+
|
| 6 |
+
This is a thin frankenstein over vLLM's in-tree `kimi_k25` VLM:
|
| 7 |
+
* vision tower + projector classes are imported VERBATIM from kimi_k25_vit
|
| 8 |
+
(the MoonViT3d encoder + KimiK25MultiModalProjector are bit-identical to the
|
| 9 |
+
encoder/projector we trained against).
|
| 10 |
+
* the text backbone is swapped from DeepseekV2ForCausalLM -> GlmMoeDsaForCausalLM
|
| 11 |
+
(GLM-5.2; same `deepseek_v2.py` MLA+MoE+DSA stack in vLLM).
|
| 12 |
+
* the image placeholder is GLM's `<|image|>` (154854), span
|
| 13 |
+
`<|begin_of_image|><|image|><|end_of_image|>`, instead of Kimi's `<|media_pad|>`.
|
| 14 |
+
* projector `linear_2` outputs GLM hidden (6144) via vision_config.mm_hidden_size.
|
| 15 |
+
|
| 16 |
+
Register out-of-tree via `ModelRegistry.register_model` (see register.py) with the
|
| 17 |
+
config.json `architectures = ["Glm5vForConditionalGeneration"]`.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from collections.abc import Iterable, Mapping
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
from torch import nn
|
| 25 |
+
|
| 26 |
+
from vllm.config import VllmConfig
|
| 27 |
+
from vllm.logger import init_logger
|
| 28 |
+
from vllm.model_executor.layers.quantization import QuantizationConfig
|
| 29 |
+
from vllm.model_executor.layers.quantization.compressed_tensors import (
|
| 30 |
+
compressed_tensors,
|
| 31 |
+
)
|
| 32 |
+
from vllm.model_executor.models.interfaces import (
|
| 33 |
+
SupportsMultiModal,
|
| 34 |
+
SupportsPP,
|
| 35 |
+
SupportsQuant,
|
| 36 |
+
)
|
| 37 |
+
from vllm.model_executor.models.kimi_k25 import (
|
| 38 |
+
KimiK25DummyInputsBuilder,
|
| 39 |
+
KimiK25MultiModalProcessor,
|
| 40 |
+
KimiK25ProcessingInfo,
|
| 41 |
+
)
|
| 42 |
+
from vllm.model_executor.models.kimi_k25_vit import (
|
| 43 |
+
KimiK25MultiModalProjector,
|
| 44 |
+
MoonViT3dPretrainedModel,
|
| 45 |
+
vision_tower_forward,
|
| 46 |
+
)
|
| 47 |
+
from vllm.multimodal import MULTIMODAL_REGISTRY
|
| 48 |
+
from vllm.multimodal.inputs import NestedTensors
|
| 49 |
+
from vllm.platforms import current_platform
|
| 50 |
+
from vllm.sequence import IntermediateTensors
|
| 51 |
+
|
| 52 |
+
from .glm5v_config import Glm5vConfig
|
| 53 |
+
from vllm.model_executor.models.utils import (
|
| 54 |
+
AutoWeightsLoader,
|
| 55 |
+
WeightsMapper,
|
| 56 |
+
init_vllm_registered_model,
|
| 57 |
+
maybe_prefix,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
logger = init_logger(__name__)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class Glm5vProcessingInfo(KimiK25ProcessingInfo):
|
| 64 |
+
"""Processing info: GLM tokenizer + `<|image|>` placeholder + MoonViT image proc.
|
| 65 |
+
|
| 66 |
+
Reuses Kimi-K2.6's image processor (identical MoonViT preprocessing) and its
|
| 67 |
+
`KimiK25Processor` wiring; only the config type and placeholder token differ.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def get_hf_config(self):
|
| 71 |
+
return self.ctx.get_hf_config(Glm5vConfig)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class Glm5vDummyInputsBuilder(KimiK25DummyInputsBuilder):
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class Glm5vMultiModalProcessor(KimiK25MultiModalProcessor):
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@MULTIMODAL_REGISTRY.register_processor(
|
| 83 |
+
Glm5vMultiModalProcessor,
|
| 84 |
+
info=Glm5vProcessingInfo,
|
| 85 |
+
dummy_inputs=Glm5vDummyInputsBuilder,
|
| 86 |
+
)
|
| 87 |
+
class Glm5vForConditionalGeneration(
|
| 88 |
+
nn.Module,
|
| 89 |
+
SupportsMultiModal,
|
| 90 |
+
SupportsPP,
|
| 91 |
+
SupportsQuant,
|
| 92 |
+
):
|
| 93 |
+
"""glm5v: MoonViT vision + trained projector + GLM-5.2 text."""
|
| 94 |
+
|
| 95 |
+
supports_encoder_tp_data = True
|
| 96 |
+
|
| 97 |
+
hf_to_vllm_mapper = WeightsMapper(
|
| 98 |
+
orig_to_new_prefix={
|
| 99 |
+
# GLM-5.2 text shards are stored with raw `model.*` / `lm_head.*` keys;
|
| 100 |
+
# nest them under the `language_model` submodule. (vision_tower.* and
|
| 101 |
+
# mm_projector.* keys are already correctly namespaced in our assembly.)
|
| 102 |
+
"lm_head.": "language_model.lm_head.",
|
| 103 |
+
"model.": "language_model.model.",
|
| 104 |
+
}
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
@classmethod
|
| 108 |
+
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
|
| 109 |
+
if modality == "image":
|
| 110 |
+
# GLM image span; the inner <|image|> token is the one expanded.
|
| 111 |
+
return "<|begin_of_image|><|image|><|end_of_image|>"
|
| 112 |
+
raise ValueError(f"Unsupported modality: {modality}")
|
| 113 |
+
|
| 114 |
+
def __init__(self, vllm_config: VllmConfig, prefix: str = "") -> None:
|
| 115 |
+
super().__init__()
|
| 116 |
+
model_config = vllm_config.model_config
|
| 117 |
+
config: Glm5vConfig = model_config.hf_config
|
| 118 |
+
self.config = config
|
| 119 |
+
quant_config = vllm_config.quant_config
|
| 120 |
+
|
| 121 |
+
self.use_data_parallel = (
|
| 122 |
+
model_config.multimodal_config.mm_encoder_tp_mode == "data"
|
| 123 |
+
)
|
| 124 |
+
self.hidden_size = config.text_config.hidden_size
|
| 125 |
+
self.device = current_platform.current_device()
|
| 126 |
+
|
| 127 |
+
# Vision tower + projector (MoonViT, kept out of compressed-tensors quant).
|
| 128 |
+
with self._mark_tower_model(vllm_config, "vision_chunk"):
|
| 129 |
+
self.vision_tower = MoonViT3dPretrainedModel(
|
| 130 |
+
config.vision_config,
|
| 131 |
+
quant_config=self._maybe_ignore_quant_config(quant_config),
|
| 132 |
+
prefix=maybe_prefix(prefix, "vision_tower"),
|
| 133 |
+
)
|
| 134 |
+
self.vision_tower = self.vision_tower.to(
|
| 135 |
+
device=self.device, dtype=model_config.dtype
|
| 136 |
+
)
|
| 137 |
+
self.mm_projector = KimiK25MultiModalProjector(
|
| 138 |
+
config=config.vision_config,
|
| 139 |
+
use_data_parallel=self.use_data_parallel,
|
| 140 |
+
quant_config=self._maybe_ignore_quant_config(quant_config),
|
| 141 |
+
prefix=maybe_prefix(prefix, "mm_projector"),
|
| 142 |
+
)
|
| 143 |
+
self.mm_projector = self.mm_projector.to(
|
| 144 |
+
device=self.device, dtype=model_config.dtype
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
self.quant_config = quant_config
|
| 148 |
+
with self._mark_language_model(vllm_config):
|
| 149 |
+
self.language_model = init_vllm_registered_model(
|
| 150 |
+
vllm_config=vllm_config,
|
| 151 |
+
hf_config=config.text_config,
|
| 152 |
+
prefix=maybe_prefix(prefix, "language_model"),
|
| 153 |
+
architectures=["GlmMoeDsaForCausalLM"],
|
| 154 |
+
)
|
| 155 |
+
self.make_empty_intermediate_tensors = (
|
| 156 |
+
self.language_model.make_empty_intermediate_tensors
|
| 157 |
+
)
|
| 158 |
+
self.media_placeholder: int = self.config.media_placeholder_token_id
|
| 159 |
+
|
| 160 |
+
def _maybe_ignore_quant_config(self, quant_config: QuantizationConfig):
|
| 161 |
+
# Keep the MoonViT vision tower + trained projector in bf16; only the GLM
|
| 162 |
+
# text backbone is quantized. The GLM-5.2-NVFP4 checkpoint uses ModelOpt
|
| 163 |
+
# NVFP4 (modelopt_fp4) for the text Linears, but our vision/projector
|
| 164 |
+
# weights are bf16 -- so skip quant for them regardless of method.
|
| 165 |
+
from vllm.model_executor.layers.quantization.modelopt import (
|
| 166 |
+
ModelOptNvFp4Config,
|
| 167 |
+
ModelOptFp8Config,
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
if isinstance(
|
| 171 |
+
quant_config,
|
| 172 |
+
(
|
| 173 |
+
compressed_tensors.CompressedTensorsConfig,
|
| 174 |
+
ModelOptNvFp4Config,
|
| 175 |
+
ModelOptFp8Config,
|
| 176 |
+
),
|
| 177 |
+
):
|
| 178 |
+
return None
|
| 179 |
+
return quant_config
|
| 180 |
+
|
| 181 |
+
def _parse_and_validate_media_input(self, **kwargs: object):
|
| 182 |
+
pixel_values = kwargs.pop("pixel_values", None)
|
| 183 |
+
grid_thws = kwargs.pop("grid_thws", None)
|
| 184 |
+
if pixel_values is None:
|
| 185 |
+
return None
|
| 186 |
+
if isinstance(pixel_values, list):
|
| 187 |
+
pixel_values = torch.cat(pixel_values, dim=0)
|
| 188 |
+
if len(pixel_values.shape) == 5 or len(pixel_values.shape) == 3:
|
| 189 |
+
pixel_values = pixel_values.reshape(
|
| 190 |
+
pixel_values.shape[0] * pixel_values.shape[1], *pixel_values.shape[2:]
|
| 191 |
+
)
|
| 192 |
+
target_dtype = next(self.vision_tower.parameters()).dtype
|
| 193 |
+
pixel_values = pixel_values.to(target_dtype)
|
| 194 |
+
assert isinstance(grid_thws, torch.Tensor)
|
| 195 |
+
grid_thws = grid_thws.reshape(-1, grid_thws.shape[-1])
|
| 196 |
+
assert grid_thws.ndim == 2 and grid_thws.size(1) == 3
|
| 197 |
+
return {"pixel_values": pixel_values, "grid_thws": grid_thws}
|
| 198 |
+
|
| 199 |
+
def _process_media_input(self, media_input) -> list[torch.Tensor]:
|
| 200 |
+
return vision_tower_forward(
|
| 201 |
+
self.vision_tower,
|
| 202 |
+
media_input["pixel_values"],
|
| 203 |
+
media_input["grid_thws"],
|
| 204 |
+
mm_projector=self.mm_projector,
|
| 205 |
+
use_data_parallel=self.use_data_parallel,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
def embed_multimodal(self, **kwargs: object) -> NestedTensors | None:
|
| 209 |
+
media_input = self._parse_and_validate_media_input(**kwargs)
|
| 210 |
+
if media_input is None:
|
| 211 |
+
return None
|
| 212 |
+
return self._process_media_input(media_input)
|
| 213 |
+
|
| 214 |
+
def forward(
|
| 215 |
+
self,
|
| 216 |
+
input_ids: torch.Tensor,
|
| 217 |
+
positions: torch.Tensor,
|
| 218 |
+
intermediate_tensors: IntermediateTensors | None = None,
|
| 219 |
+
inputs_embeds: torch.Tensor | None = None,
|
| 220 |
+
**kwargs: object,
|
| 221 |
+
) -> IntermediateTensors:
|
| 222 |
+
if intermediate_tensors is not None:
|
| 223 |
+
inputs_embeds = None
|
| 224 |
+
return self.language_model(
|
| 225 |
+
input_ids=input_ids,
|
| 226 |
+
positions=positions,
|
| 227 |
+
intermediate_tensors=intermediate_tensors,
|
| 228 |
+
inputs_embeds=inputs_embeds,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def compute_logits(self, hidden_states: torch.Tensor, **kwargs) -> torch.Tensor:
|
| 232 |
+
return self.language_model.compute_logits(hidden_states)
|
| 233 |
+
|
| 234 |
+
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
|
| 235 |
+
loader = AutoWeightsLoader(self)
|
| 236 |
+
loaded = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
|
| 237 |
+
# Sanity log: confirm the trained projector tensors were actually loaded
|
| 238 |
+
# (not left at init). Helps catch key-name mismatches.
|
| 239 |
+
proj = sorted(k for k in loaded if "mm_projector" in k)
|
| 240 |
+
logger.info("[glm5v] mm_projector keys loaded (%d): %s", len(proj), proj)
|
| 241 |
+
return loaded
|
plugins/vllm_glm5v/glm5v_config.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""Config for glm5v = MoonViT vision + GLM-5.2 (glm_moe_dsa) text."""
|
| 3 |
+
|
| 4 |
+
from transformers import AutoConfig
|
| 5 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 6 |
+
|
| 7 |
+
from vllm.transformers_utils.configs.kimi_k25 import KimiK25VisionConfig
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Glm5vConfig(PretrainedConfig):
|
| 11 |
+
"""glm5v top-level config.
|
| 12 |
+
|
| 13 |
+
Holds a MoonViT `vision_config` (KimiK25VisionConfig) and a GLM-5.2
|
| 14 |
+
`text_config` (model_type `glm_moe_dsa`). `media_placeholder_token_id` is GLM's
|
| 15 |
+
`<|image|>` id (154854). `vision_config.mm_hidden_size` is the projector's output
|
| 16 |
+
dim and MUST equal the GLM text hidden size (6144).
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
model_type = "glm5v"
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
vision_config: dict | KimiK25VisionConfig | None = None,
|
| 24 |
+
text_config: dict | PretrainedConfig | None = None,
|
| 25 |
+
ignore_index: int = -100,
|
| 26 |
+
media_placeholder_token_id: int = 154854,
|
| 27 |
+
pad_token_id: int = 154820,
|
| 28 |
+
# True => vLLM's chat layer maps OpenAI `image`/`video` content to the
|
| 29 |
+
# model's `vision_chunk` modality (the Kimi MoonViT path expects this).
|
| 30 |
+
use_unified_vision_chunk: bool = True,
|
| 31 |
+
video_placeholder: str = "<|glm5v_video_placeholder|>",
|
| 32 |
+
**kwargs,
|
| 33 |
+
):
|
| 34 |
+
# Vision config (MoonViT).
|
| 35 |
+
if vision_config is None:
|
| 36 |
+
self.vision_config = KimiK25VisionConfig()
|
| 37 |
+
elif isinstance(vision_config, dict):
|
| 38 |
+
self.vision_config = KimiK25VisionConfig(**vision_config)
|
| 39 |
+
else:
|
| 40 |
+
self.vision_config = vision_config
|
| 41 |
+
|
| 42 |
+
# Text config (GLM-5.2 / glm_moe_dsa). Build via AutoConfig so the
|
| 43 |
+
# GlmMoeDsa config class (and all its MLA/DSA/MoE fields) is used.
|
| 44 |
+
# NOTE: transformers calls Glm5vConfig() with no args (to_diff_dict),
|
| 45 |
+
# so text_config=None must default to a bare GlmMoeDsaConfig.
|
| 46 |
+
if text_config is None:
|
| 47 |
+
self.text_config = AutoConfig.for_model("glm_moe_dsa")
|
| 48 |
+
elif isinstance(text_config, dict):
|
| 49 |
+
tc = dict(text_config)
|
| 50 |
+
tc.setdefault("model_type", "glm_moe_dsa")
|
| 51 |
+
self.text_config = AutoConfig.for_model(**tc)
|
| 52 |
+
else:
|
| 53 |
+
self.text_config = text_config
|
| 54 |
+
|
| 55 |
+
# Projector output dim must match GLM hidden.
|
| 56 |
+
self.vision_config.mm_hidden_size = self.text_config.hidden_size
|
| 57 |
+
|
| 58 |
+
self.ignore_index = ignore_index
|
| 59 |
+
self.media_placeholder_token_id = media_placeholder_token_id
|
| 60 |
+
self.use_unified_vision_chunk = use_unified_vision_chunk
|
| 61 |
+
self.video_placeholder = video_placeholder
|
| 62 |
+
|
| 63 |
+
if getattr(self.text_config, "quantization_config", None) is not None:
|
| 64 |
+
self.quantization_config = self.text_config.quantization_config
|
| 65 |
+
|
| 66 |
+
super().__init__(pad_token_id=pad_token_id, **kwargs)
|
| 67 |
+
|
| 68 |
+
@property
|
| 69 |
+
def hidden_size(self) -> int:
|
| 70 |
+
return self.text_config.hidden_size
|
| 71 |
+
|
| 72 |
+
@property
|
| 73 |
+
def vocab_size(self) -> int:
|
| 74 |
+
return self.text_config.vocab_size
|
| 75 |
+
|
| 76 |
+
def __getattr__(self, name: str):
|
| 77 |
+
"""Delegate unknown attributes to the GLM text_config.
|
| 78 |
+
|
| 79 |
+
vLLM's MLA / DSA-indexer machinery sometimes reads text-model fields
|
| 80 |
+
(e.g. `index_topk`, `index_n_heads`, `kv_lora_rank`) off the top-level
|
| 81 |
+
model config rather than off `text_config`. Forwarding here lets the GLM
|
| 82 |
+
sparse-attention path find them without copying every field up.
|
| 83 |
+
NOTE: only called when normal attribute lookup fails, so it won't shadow
|
| 84 |
+
real attributes; guard against recursion on `text_config` itself.
|
| 85 |
+
"""
|
| 86 |
+
if name == "text_config":
|
| 87 |
+
raise AttributeError(name)
|
| 88 |
+
try:
|
| 89 |
+
text_config = self.__dict__["text_config"]
|
| 90 |
+
except KeyError as exc: # during unpickling / early init
|
| 91 |
+
raise AttributeError(name) from exc
|
| 92 |
+
return getattr(text_config, name)
|
plugins/vllm_glm5v/register.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""Register glm5v config + model with HF AutoConfig and vLLM's ModelRegistry.
|
| 3 |
+
|
| 4 |
+
Import this module (or call `register()`) BEFORE constructing `LLM(...)` /
|
| 5 |
+
`vllm serve`. For `vllm serve`, point `VLLM_PLUGINS`/entrypoint at it, or simply
|
| 6 |
+
`import vllm_glm5v.register` from a sitecustomize / -X importtime hook. The
|
| 7 |
+
simplest path used here: copy these files next to vLLM's models and import.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def register() -> None:
|
| 12 |
+
from transformers import AutoConfig
|
| 13 |
+
|
| 14 |
+
from .glm5v_config import Glm5vConfig
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
AutoConfig.register("glm5v", Glm5vConfig)
|
| 18 |
+
except ValueError:
|
| 19 |
+
pass # already registered
|
| 20 |
+
|
| 21 |
+
from vllm import ModelRegistry
|
| 22 |
+
|
| 23 |
+
ModelRegistry.register_model(
|
| 24 |
+
"Glm5vForConditionalGeneration",
|
| 25 |
+
"vllm_glm5v.glm5v:Glm5vForConditionalGeneration",
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
register()
|