harrypart's picture
engine plugin (vllm + sglang)
697ef33 verified
Raw
History Blame Contribute Delete
7.02 kB
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Register Glm5vForConditionalGeneration as a DSA-capable architecture in SGLang.
WHY THIS PATCH IS NECESSARY (not an environment workaround):
SGLang gates the DeepSeek-Sparse-Attention path on a hardcoded architecture
tuple in ``is_deepseek_dsa`` (``srt/configs/model_config.py``). Upstream's own
DSA vision-language model (Mistral Large 3) is supported by adding its VLM
arch ``PixtralForConditionalGeneration`` to exactly this tuple in-tree — that
is the upstream mechanism; no env-var/out-of-tree hook exists for it (unlike
models/processors/mm-arch, which we register via the shipped
``SGLANG_EXTERNAL_*`` hooks). Our GLM5V wrapper needs the same one-line entry
so the DSA attention backend (and the ``server_args`` DSA backend defaults)
recognize the model whose text backbone is GlmMoeDsa.
Three insertions, each at a place where upstream registered its own DSA/MLA
VLM archs (KimiK25 / Pixtral precedents):
1. ``model_config.py``: the ``is_deepseek_dsa`` arch tuple — gates the DSA
attention backend;
2. ``model_config.py``: the MLA attention-arch condition — without it the
KV pool is built as MHA and the DSA backend crashes on
``dsa_kv_cache_store_fp8``;
3. ``server_args.py``: the DSA model-adjustments gate list — sets the DSA
dense-attn kv-len threshold (= model index_topk) and the prefill/decode
DSA backend defaults (trtllm on Blackwell); without it decode dies with
``Unsupported self.dsa_decode_impl = None``.
A 4th insertion (added later) enables MTP/NEXTN speculative decoding: without
it the draft worker rebuilds the entire ~90GB multimodal model as the "draft"
and OOMs (our wrapper arch is in none of SGLang's draft-model rewrite lists).
See the ``DRAFT_ANCHOR`` block below. This is only needed when serving with
``--speculative-algorithm NEXTN``; it is a no-op otherwise. (Measured: MTP gives
~1.1-1.16x tok/s at low/moderate batch + ~2.0 accept-len, greedy outputs are
token-identical to non-spec, but it is graph-capped at batch 256 so plain
high-batch TP8 wins for throughput-bound RL rollout — MTP is a single-stream
LATENCY lever, not a throughput one.)
Other in-tree arch lists mentioning GlmMoeDsa/KimiK25 are deliberately NOT
patched: they gate features we do not use (encoder disaggregation) or pure
performance defaults we set explicitly (flashinfer allreduce fusion,
attention-backend fallback).
The same entries will be needed wherever slime launches SGLang rollout engines
for glm5v (Milestone 1) — candidate for an upstream sglang PR.
Idempotent; run before ``sglang.launch_server`` (serve.sh does this).
"""
import pathlib
import subprocess
import sys
ARCH = "Glm5vForConditionalGeneration"
DSA_ANCHOR = '"GlmMoeDsaForCausalLM",'
DSA_INSERT = (
DSA_ANCHOR
+ f'\n "{ARCH}", # glm5v: GLM-5.2 (DSA) text + MoonViT vision (sglang_glm5v)'
)
MLA_ANCHOR = 'or "KimiK25ForConditionalGeneration" in self.hf_config.architectures'
MLA_INSERT = (
MLA_ANCHOR
+ f'\n or "{ARCH}" in self.hf_config.architectures # glm5v (sglang_glm5v)'
)
# server_args.py DSA adjustments gate: the list containing KimiK25 is unique
# to that block (other GlmMoeDsa lists in the file lack it).
SA_ANCHOR = (
'"KimiK25ForConditionalGeneration",\n'
' "MistralLarge3ForCausalLM",\n'
' "PixtralForConditionalGeneration",\n'
' "GlmMoeDsaForCausalLM",'
)
SA_INSERT = SA_ANCHOR + f'\n "{ARCH}", # glm5v (sglang_glm5v)'
# --- MTP / speculative NEXTN draft-model support (4th insertion) ---
# WHY: with --speculative-algorithm NEXTN, SGLang builds a SEPARATE draft model
# (is_draft_model=True) whose arch it rewrites to a single-layer NextN module in
# ``_config_draft_model``. That method maps the *text* backbone
# ``GlmMoeDsaForCausalLM`` -> ``DeepseekV3ForCausalLMNextN`` (correct: our MTP
# weights live at text layer 78: eh_proj/enorm/hnorm + one block). But our
# TOP-LEVEL arch is the multimodal wrapper ``Glm5vForConditionalGeneration``,
# which is in NONE of the rewrite lists -> the draft worker instead rebuilds the
# ENTIRE multimodal model (MoonViT + full 78-layer 90GB text model) as the
# "draft" and OOMs (a second ~90GB copy on top of base weights + KV + graphs).
#
# FIX: mirror the shipped ``Step3p7ForConditionalGeneration`` precedent (a VLM
# wrapper whose draft is its text MTP): when is_draft_model and arch is our
# wrapper, swap hf_config -> hf_text_config and set arch to GlmMoeDsaForCausalLM.
# Inserted at the TOP of _config_draft_model so the existing
# GlmMoeDsaForCausalLM -> DeepseekV3ForCausalLMNextN rule then fires on it.
DRAFT_ANCHOR = (
" def _config_draft_model(self):\n"
" is_draft_model = self.is_draft_model\n"
)
DRAFT_INSERT = (
DRAFT_ANCHOR
+ "\n"
+ " # glm5v (sglang_glm5v): map the multimodal wrapper draft to its\n"
+ " # text backbone so the GlmMoeDsa->NextN rewrite below applies.\n"
+ f' if is_draft_model and self.hf_config.architectures[0] == "{ARCH}":\n'
+ " self.hf_config = self.hf_text_config\n"
+ ' self.hf_config.architectures = ["GlmMoeDsaForCausalLM"]\n'
)
def find_module(mod: str) -> pathlib.Path:
out = subprocess.run(
[sys.executable, "-c", f"import {mod} as m; print(m.__file__)"],
capture_output=True,
text=True,
check=True,
)
return pathlib.Path(out.stdout.strip())
def apply(path: pathlib.Path, anchor: str, insert: str, marker: str, label: str) -> None:
src = path.read_text()
if marker in src:
print(f"{label}: already patched")
return
assert anchor in src, f"{label}: anchor not found in {path} (sglang version changed?)"
path.write_text(src.replace(anchor, insert, 1))
print(f"{label}: patched ({path})")
def main() -> None:
mc = find_module("sglang.srt.configs.model_config")
apply(mc, DSA_ANCHOR, DSA_INSERT, f'"{ARCH}", # glm5v', "DSA tuple")
apply(mc, MLA_ANCHOR, MLA_INSERT, f'or "{ARCH}" in self.hf_config.architectures', "MLA condition")
apply(
mc,
DRAFT_ANCHOR,
DRAFT_INSERT,
f'if is_draft_model and self.hf_config.architectures[0] == "{ARCH}"',
"NEXTN draft-model map",
)
sa = find_module("sglang.srt.server_args")
apply(sa, SA_ANCHOR, SA_INSERT, f'"{ARCH}", # glm5v', "server_args DSA gate")
# verify the DSA gate end-to-end
check = subprocess.run(
[
sys.executable,
"-c",
(
"from sglang.srt.configs.model_config import is_deepseek_dsa;"
f"print(is_deepseek_dsa({{'architectures': ['{ARCH}'], 'index_topk': 2048}}))"
),
],
capture_output=True,
text=True,
check=True,
)
assert check.stdout.strip() == "True", check.stdout + check.stderr
print("verified: is_deepseek_dsa recognizes the arch")
if __name__ == "__main__":
main()