Add Ling-3.0-flash-VL model support (#38526)

This commit is contained in:
Xinyuan Tong
2026-09-17 00:19:26 +08:00
committed by GitHub
parent 1b78083b42
commit f0bf652534
37 changed files with 3332 additions and 123 deletions
@@ -98,8 +98,8 @@ class TestWaterfillEPLB(CustomTestCase):
self.assertTrue(torch.equal(processed_ids, torch.tensor([[0, 34, 270, 271]])))
self.assertTrue(torch.equal(recorder_ids, torch.tensor([[0, 33, 263]])))
def test_topk_recorder_ids_match_dispatch_ids_without_per_rank_shared_slots(self):
topk_ids = torch.tensor([[0, 33, 263, 256]], dtype=torch.int32)
def test_topk_recorder_ids_exclude_global_fused_shared_slot(self):
topk_ids = torch.tensor([[0, 33, 200, 256]], dtype=torch.int32)
topk_weights = torch.ones_like(topk_ids, dtype=torch.float32)
topk_config = TopKConfig(
top_k=4,
@@ -107,7 +107,7 @@ class TestWaterfillEPLB(CustomTestCase):
routed_scaling_factor=1.0,
)
dispatch_info = SimpleNamespace(
num_physical_experts=264, ep_dispatch_algorithm="static"
num_physical_experts=256, ep_dispatch_algorithm="static"
)
def fake_eplb_postprocess(
@@ -136,8 +136,8 @@ class TestWaterfillEPLB(CustomTestCase):
expert_location_dispatch_info=dispatch_info,
)
self.assertTrue(torch.equal(processed_ids, torch.tensor([[1, 34, 264, 257]])))
self.assertTrue(torch.equal(recorder_ids, processed_ids))
self.assertTrue(torch.equal(processed_ids, torch.tensor([[1, 34, 201, 256]])))
self.assertTrue(torch.equal(recorder_ids, torch.tensor([[1, 34, 201]])))
if __name__ == "__main__":
@@ -0,0 +1,212 @@
"""Regression tests for Bailing multimodal rotary positions and config bounds."""
import unittest
from types import SimpleNamespace
from unittest import mock
import torch
from sglang.srt.configs.bailing_hybrid import BailingMoeV3VLConfig
from sglang.srt.layers.rotary_embedding.bailing_mrope import BailingMRotaryEmbedding
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
def _position_config(max_position_embeddings=131072):
return SimpleNamespace(
vision_config=SimpleNamespace(spatial_merge_size=2),
text_config=SimpleNamespace(
image_patch_token=11,
video_patch_token=12,
image_start_token=10,
video_start_token=13,
use_interleaved_frame_timestamp=False,
max_position_embeddings=max_position_embeddings,
),
)
class TestBailingMRotaryEmbedding(CustomTestCase):
def test_text_and_single_multimodal_position_shapes(self):
"""A singleton sequence must retain the [3, batch, seq] contract."""
config = _position_config()
text_positions, text_delta = (
BailingMRotaryEmbedding.bailing_3drope_get_input_positions_tensor(
torch.tensor([7]), config, None, None
)
)
image_positions, image_delta = (
BailingMRotaryEmbedding.bailing_3drope_get_input_positions_tensor(
torch.tensor([10, 11]),
config,
image_grid_thw=torch.tensor([[1, 2, 2]]),
video_grid_thw=None,
)
)
self.assertEqual(text_positions.shape, (3, 1, 1))
self.assertEqual(text_delta.shape, (1, 1))
self.assertEqual(image_positions.shape, (3, 1, 2))
self.assertEqual(image_delta.shape, (1, 1))
def test_centered_height_positions_can_be_negative(self):
"""Tall images require negative H coordinates instead of clamping to zero."""
config = _position_config()
input_ids = torch.tensor([10] + [11] * 7 + [99])
positions, _ = (
BailingMRotaryEmbedding.bailing_3drope_get_input_positions_tensor(
input_ids,
config,
image_grid_thw=torch.tensor([[1, 14, 2]]),
video_grid_thw=None,
)
)
self.assertEqual(positions.shape, (3, 1, 9))
self.assertLess(int(positions[1].min()), 0)
def test_checkpoint_position_bound_is_enforced(self):
"""Media positions at or beyond the checkpoint context must fail clearly."""
config = _position_config(max_position_embeddings=4)
input_ids = torch.tensor([10] + [11] * 7)
with self.assertRaisesRegex(ValueError, "checkpoint bounds"):
BailingMRotaryEmbedding.bailing_3drope_get_input_positions_tensor(
input_ids,
config,
image_grid_thw=torch.tensor([[1, 14, 2]]),
video_grid_thw=None,
)
def test_negative_start_cache_growth_preserves_phase(self):
"""Growing a negative-origin cache must append the next logical phase."""
with mock.patch("sglang.srt.layers.rotary_embedding.base._is_cpu", True):
rotary = BailingMRotaryEmbedding(
head_size=8,
rotary_dim=8,
max_position_embeddings=16,
base=10000,
is_neox_style=True,
dtype=torch.float32,
mrope_section=[2, 1, 1],
video_rope=True,
)
self.assertEqual(rotary.position_start, -16)
self.assertEqual(rotary.cos_sin_cache.shape[0], 32)
rotary._ensure_cos_sin_cache_length(32)
inv_freq = rotary._compute_inv_freq(rotary.base)
expected = torch.cat(((16 * inv_freq).cos(), (16 * inv_freq).sin()))
torch.testing.assert_close(rotary.cos_sin_cache[32], expected)
def test_yarn_scaling_extends_positive_cache_only(self):
"""YaRN factor stretches the positive side; the negative side is fixed."""
with mock.patch("sglang.srt.layers.rotary_embedding.base._is_cpu", True):
rotary = BailingMRotaryEmbedding(
head_size=8,
rotary_dim=8,
max_position_embeddings=16,
base=10000,
is_neox_style=True,
dtype=torch.float32,
mrope_section=[2, 1, 1],
video_rope=True,
scaling_factor=2.0,
original_max_position_embeddings=16,
)
self.assertEqual(rotary.position_start, -16)
self.assertEqual(rotary.cos_sin_cache.shape[0], 16 + 32)
self.assertGreater(rotary.mscale, 1.0)
inv_freq = rotary._compute_inv_freq(rotary.base)
position = 20 # beyond the native bound of 16, row = 20 - (-16)
expected = torch.cat(
(
(position * inv_freq).cos() * rotary.mscale,
(position * inv_freq).sin() * rotary.mscale,
)
)
torch.testing.assert_close(rotary.cos_sin_cache[36], expected)
# Growth past the initial cache keeps the same scaled phase.
rotary._ensure_cos_sin_cache_length(48)
torch.testing.assert_close(rotary.cos_sin_cache[36], expected)
def test_yarn_scaling_relaxes_positive_position_bound(self):
"""A yarn rope_parameters entry must raise the allowed positive bound."""
config = _position_config(max_position_embeddings=4)
config.text_config.rope_parameters = {
"rope_type": "yarn",
"factor": 2.0,
"original_max_position_embeddings": 4,
}
input_ids = torch.tensor([10] + [11] * 7)
# max position is 7: fails against the native bound of 4, passes once
# the yarn factor doubles the positive bound to 8.
positions, _ = (
BailingMRotaryEmbedding.bailing_3drope_get_input_positions_tensor(
input_ids,
config,
image_grid_thw=torch.tensor([[1, 14, 2]]),
video_grid_thw=None,
)
)
self.assertEqual(positions.shape, (3, 1, 8))
def test_rope_scaling_override_merges_into_text_config(self):
"""A top-level rope_scaling override must not drop the mrope markers."""
config = BailingMoeV3VLConfig(
mrope_section=[8, 12, 12],
text_config={"max_position_embeddings": 131072},
vision_config={},
)
config.rope_scaling = {
"rope_type": "yarn",
"factor": 2.0,
"original_max_position_embeddings": 131072,
}
rope_parameters = config.text_config.rope_parameters
self.assertEqual(rope_parameters["rope_type"], "yarn")
self.assertEqual(rope_parameters["factor"], 2.0)
self.assertEqual(rope_parameters["mrope_section"], [8, 12, 12])
self.assertTrue(rope_parameters["video_rope"])
def test_public_checkpoint_config_contract(self):
"""External Ling-3.0-flash-VL config literals must survive local parsing."""
config = BailingMoeV3VLConfig(
image_token_id=157157,
video_token_id=156909,
mrope_section=[8, 12, 12],
text_config={
"num_hidden_layers": 42,
"vocab_size": 157184,
"max_position_embeddings": 131072,
"moe_router_enable_expert_bias": True,
"num_experts": 512,
"num_experts_per_tok": 8,
"n_group": 8,
"topk_group": 4,
"score_function": "sigmoid",
"routed_scaling_factor": 2.5,
"short_conv_kernel_size": 4,
},
vision_config={"disable_merger_proj": True},
)
self.assertEqual(config.text_config.num_hidden_layers, 42)
self.assertEqual(config.text_config.max_position_embeddings, 131072)
self.assertTrue(config.text_config.moe_router_enable_expert_bias)
self.assertEqual(
config.text_config.rope_parameters["mrope_section"], [8, 12, 12]
)
self.assertTrue(config.text_config.rope_parameters["video_rope"])
self.assertTrue(config.vision_config.disable_merger_proj)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,197 @@
"""Regression tests for Bailing modality metadata and per-token routing bias."""
import unittest
from types import SimpleNamespace
import torch
from sglang.srt.configs.model_config import requires_mm_token_modalities
from sglang.srt.layers.moe.topk import biased_grouped_topk_impl
from sglang.srt.layers.multi_gate import create_multi_gate_mm_indices
from sglang.srt.managers.schedule_batch import (
Modality,
MultimodalDataItem,
MultimodalInputs,
MultimodalProcessorOutput,
)
from sglang.srt.model_executor.forward_batch_info import (
_build_forward_token_modalities,
_maybe_build_forward_token_modalities,
)
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=15, suite="base-a-test-cpu")
class TestBailingModalityMetadata(CustomTestCase):
def test_offsets_survive_hash_padding(self):
"""Hash-derived token replacement must not erase image/audio identity."""
items = [
MultimodalDataItem(
modality=Modality.IMAGE,
offsets=[(1, 2)],
feature=torch.ones(1),
),
MultimodalDataItem(
modality=Modality.AUDIO,
offsets=[(4, 5)],
feature=torch.ones(1),
),
]
output = MultimodalProcessorOutput(
mm_items=items,
input_ids=[100, 11, 11, 101, 12, 12, 102],
)
inputs = MultimodalInputs.from_processor_output(
output, requires_mm_token_modalities=True
)
self.assertEqual(
inputs.token_modalities,
[
0,
Modality.IMAGE.value,
Modality.IMAGE.value,
0,
Modality.AUDIO.value,
Modality.AUDIO.value,
0,
],
)
self.assertNotEqual(items[0].pad_value, 11)
self.assertNotEqual(items[1].pad_value, 12)
def test_offset_validation_only_runs_for_multirouter(self):
item = MultimodalDataItem(
modality=Modality.IMAGE,
offsets=[(1, 3)],
feature=torch.ones(1),
)
output = MultimodalProcessorOutput(mm_items=[item], input_ids=[100, 101])
inputs = MultimodalInputs.from_processor_output(output)
self.assertIsNone(inputs.token_modalities)
with self.assertRaisesRegex(ValueError, "Invalid multimodal token offsets"):
MultimodalInputs.from_processor_output(
output, requires_mm_token_modalities=True
)
def test_chunked_metadata_is_identical_on_every_pp_stage(self):
"""Each PP stage must independently receive the same active token map."""
mm_inputs = [
MultimodalInputs(
mm_items=[],
token_modalities=[0, Modality.IMAGE.value, Modality.IMAGE.value, 0],
),
MultimodalInputs(
mm_items=[],
token_modalities=[Modality.AUDIO.value, Modality.AUDIO.value, 0],
),
]
expected = torch.tensor(
[
Modality.IMAGE.value,
Modality.IMAGE.value,
0,
Modality.AUDIO.value,
Modality.AUDIO.value,
],
dtype=torch.int8,
)
stage_maps = [
_build_forward_token_modalities(
mm_inputs,
extend_prefix_lens=[1, 0],
extend_seq_lens=[3, 2],
num_tokens=5,
device=torch.device("cpu"),
)
for _ in range(2)
]
for stage_map in stage_maps:
torch.testing.assert_close(stage_map, expected)
def test_only_bailing_multirouter_requires_token_modalities(self):
bailing_arch = ["BailingMoeV3VLForConditionalGeneration"]
self.assertFalse(
requires_mm_token_modalities(
bailing_arch, SimpleNamespace(multi_gate=False, router_type="topN")
)
)
self.assertTrue(
requires_mm_token_modalities(
bailing_arch, SimpleNamespace(multi_gate=True, router_type="topN")
)
)
self.assertFalse(
requires_mm_token_modalities(
["DeepseekV4ForCausalLM"],
SimpleNamespace(multi_gate=True, router_type="MultiRouter"),
)
)
def test_unrelated_model_skips_mismatched_metadata(self):
mm_inputs = [
MultimodalInputs(mm_items=[], token_modalities=[Modality.IMAGE.value])
]
result = _maybe_build_forward_token_modalities(
SimpleNamespace(requires_mm_token_modalities=False),
mm_inputs,
extend_prefix_lens=[0],
extend_seq_lens=[1],
num_tokens=6,
device=torch.device("cpu"),
)
self.assertIsNone(result)
with self.assertRaisesRegex(ValueError, "does not match the forward batch"):
_maybe_build_forward_token_modalities(
SimpleNamespace(requires_mm_token_modalities=True),
mm_inputs,
extend_prefix_lens=[0],
extend_seq_lens=[1],
num_tokens=6,
device=torch.device("cpu"),
)
def test_mixed_modalities_select_reference_experts(self):
"""Per-token bias must select image/audio experts after modality grouping."""
modalities = torch.tensor(
[
Modality.IMAGE.value,
Modality.IMAGE.value,
0,
Modality.AUDIO.value,
Modality.AUDIO.value,
],
dtype=torch.int8,
)
token_indices, modality_ids = create_multi_gate_mm_indices(modalities)
self.assertEqual(modality_ids.tolist(), [0, 1, 2])
self.assertEqual(token_indices[:1].tolist(), [2])
self.assertEqual(token_indices[64:66].tolist(), [0, 1])
self.assertEqual(token_indices[128:130].tolist(), [3, 4])
router_logits = torch.zeros(5, 8)
dynamic_bias = torch.zeros_like(router_logits)
expected_experts = torch.tensor([1, 1, 0, 6, 6], dtype=torch.int32)
dynamic_bias.scatter_(1, expected_experts.long().unsqueeze(1), 10.0)
_, expert_ids = biased_grouped_topk_impl(
hidden_states=torch.zeros(5, 4),
gating_output=router_logits,
correction_bias=dynamic_bias,
topk=1,
renormalize=True,
num_expert_group=2,
topk_group=1,
)
torch.testing.assert_close(expert_ids.squeeze(1), expected_experts)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,277 @@
"""Regression tests for streaming Bailing multimodal weight dispatch."""
import unittest
import torch
import torch.nn as nn
from sglang.srt.configs.bailing_hybrid import (
BailingHybridConfig,
BailingMoeV3VLConfig,
is_bailing_multi_gate_enabled,
)
from sglang.srt.models.bailing_mm_v3 import (
BailingMoeV3VLForConditionalGeneration,
)
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
class _OneShotWeights:
def __init__(self, values):
self.values = values
self.iterations = 0
def __iter__(self):
self.iterations += 1
if self.iterations > 1:
raise AssertionError("checkpoint iterator was consumed more than once")
return iter(self.values)
class _PublicRouter(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.zeros(3, 2))
self.expert_bias = nn.Parameter(torch.zeros(3))
class _PublicTextLayer(nn.Module):
def __init__(self):
super().__init__()
self.mlp = nn.Module()
self.mlp.gate = _PublicRouter()
class _TextModel(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Module()
self.model.word_embeddings = nn.Embedding(3, 2)
self.model.layers = nn.ModuleList([_PublicTextLayer()])
self.model.norm = nn.LayerNorm(2, bias=False)
self.lm_head = nn.Linear(2, 3, bias=False)
def load_weights(self, weights):
params = dict(self.named_parameters())
loaded = set()
for name, value in weights:
params[name].data.copy_(value)
loaded.add(name)
return loaded
class _PublicVisionBlock(nn.Module):
def __init__(self):
super().__init__()
self.attn = nn.Module()
self.attn.qkv_proj = nn.Linear(2, 6)
self.attn.proj = nn.Linear(2, 2)
self.mlp = nn.Module()
self.mlp.linear_fc1 = nn.Linear(2, 4)
self.mlp.linear_fc2 = nn.Linear(4, 2)
class _PublicVision(nn.Module):
def __init__(self):
super().__init__()
self.patch_embed = nn.Module()
self.patch_embed.proj = nn.Linear(2, 2)
self.pos_embed = nn.Embedding(3, 2)
self.blocks = nn.ModuleList([_PublicVisionBlock()])
self.merger = nn.Module()
self.merger.norm = nn.LayerNorm(2)
# Simulate modules created by the inherited Qwen default. Coverage
# must ignore them when Bailing deepstack is disabled.
self.deepstack_merger_list = nn.ModuleList([nn.Linear(2, 2)])
class TestBailingVLWeightLoading(CustomTestCase):
@staticmethod
def _wrapper():
wrapper = BailingMoeV3VLForConditionalGeneration.__new__(
BailingMoeV3VLForConditionalGeneration
)
nn.Module.__init__(wrapper)
wrapper.model = _TextModel()
wrapper._build_mm_encoders = True
wrapper.visual = _PublicVision()
wrapper.linear_proj = nn.Sequential(nn.Linear(2, 2), nn.GELU(), nn.Linear(2, 2))
wrapper.deepstack_visual_indexes = ()
wrapper.multi_gate_enabled = False
return wrapper
@staticmethod
def _filled_weights(wrapper, checkpoint_to_parameter):
params = dict(wrapper.named_parameters())
return [
(checkpoint_name, torch.full_like(params[parameter_name], value))
for checkpoint_name, parameter_name, value in checkpoint_to_parameter
]
@classmethod
def _public_weights(cls, wrapper):
return cls._filled_weights(
wrapper,
[
(
"model.word_embeddings.weight",
"model.model.word_embeddings.weight",
1,
),
(
"model.layers.0.mlp.gate.weight",
"model.model.layers.0.mlp.gate.weight",
2,
),
(
"model.layers.0.mlp.gate.expert_bias",
"model.model.layers.0.mlp.gate.expert_bias",
3,
),
("model.norm.weight", "model.model.norm.weight", 4),
("lm_head.weight", "model.lm_head.weight", 5),
(
"model.visual.blocks.0.attn.qkv.weight",
"visual.blocks.0.attn.qkv_proj.weight",
6,
),
(
"model.visual.blocks.0.attn.qkv.bias",
"visual.blocks.0.attn.qkv_proj.bias",
7,
),
(
"model.visual.blocks.0.attn.proj.weight",
"visual.blocks.0.attn.proj.weight",
8,
),
(
"model.visual.blocks.0.attn.proj.bias",
"visual.blocks.0.attn.proj.bias",
9,
),
(
"model.visual.blocks.0.mlp.linear_fc1.weight",
"visual.blocks.0.mlp.linear_fc1.weight",
10,
),
(
"model.visual.blocks.0.mlp.linear_fc1.bias",
"visual.blocks.0.mlp.linear_fc1.bias",
11,
),
(
"model.visual.blocks.0.mlp.linear_fc2.weight",
"visual.blocks.0.mlp.linear_fc2.weight",
12,
),
(
"model.visual.blocks.0.mlp.linear_fc2.bias",
"visual.blocks.0.mlp.linear_fc2.bias",
13,
),
(
"model.visual.patch_embed.proj.weight",
"visual.patch_embed.proj.weight",
14,
),
(
"model.visual.patch_embed.proj.bias",
"visual.patch_embed.proj.bias",
15,
),
("model.visual.pos_embed.weight", "visual.pos_embed.weight", 16),
("model.visual.merger.norm.weight", "visual.merger.norm.weight", 17),
("model.visual.merger.norm.bias", "visual.merger.norm.bias", 18),
("linear_proj.0.weight", "linear_proj.0.weight", 19),
("linear_proj.0.bias", "linear_proj.0.bias", 20),
("linear_proj.2.weight", "linear_proj.2.weight", 21),
("linear_proj.2.bias", "linear_proj.2.bias", 22),
],
)
def test_v3_loader_accepts_public_checkpoint_names_once(self):
"""The public checkpoint layout must load without a second iterator pass."""
wrapper = self._wrapper()
weights = _OneShotWeights(self._public_weights(wrapper))
wrapper.load_weights(weights)
self.assertEqual(weights.iterations, 1)
torch.testing.assert_close(
wrapper.model.model.layers[0].mlp.gate.expert_bias,
torch.full((3,), 3.0),
)
torch.testing.assert_close(
wrapper.visual.blocks[0].attn.qkv_proj.weight,
torch.full((6, 2), 6.0),
)
torch.testing.assert_close(wrapper.linear_proj[2].bias, torch.full((2,), 22.0))
def test_public_config_does_not_enable_qwen_deepstack_defaults(self):
"""An omitted public deepstack field must not create random modules."""
config = BailingMoeV3VLConfig(vision_config={"disable_merger_proj": True})
self.assertEqual(config.vision_config.deepstack_visual_indexes, [])
def test_public_config_selects_standard_single_router(self):
"""Absent MultiRouter evidence must retain the public single gate and bias."""
config = BailingMoeV3VLConfig(
text_config={
"score_function": "sigmoid",
"moe_router_enable_expert_bias": True,
"routed_scaling_factor": 2.5,
"n_group": 8,
"topk_group": 4,
"num_experts": 512,
"num_experts_per_tok": 8,
}
)
self.assertFalse(is_bailing_multi_gate_enabled(config.text_config))
self.assertTrue(config.text_config.moe_router_enable_expert_bias)
self.assertEqual(config.text_config.score_function, "sigmoid")
def test_multi_gate_requires_explicit_config_evidence(self):
"""Internal MultiRouter checkpoints remain reachable only by declaration."""
for config in (
BailingHybridConfig(multi_gate=True),
BailingHybridConfig(router_type="MultiRouter"),
):
with self.subTest(config=config):
self.assertTrue(is_bailing_multi_gate_enabled(config))
def test_required_multimodal_weight_coverage_is_enforced(self):
"""A truncated public checkpoint must not leave random projection bias."""
wrapper = self._wrapper()
weights = _OneShotWeights(self._public_weights(wrapper)[:-1])
with self.assertRaisesRegex(
RuntimeError, "Missing required Bailing VL weights"
):
wrapper.load_weights(weights)
def test_required_single_router_weight_coverage_is_enforced(self):
"""Every public MoE layer must load its sole gate weight and expert bias."""
wrapper = self._wrapper()
public_weights = self._public_weights(wrapper)
for missing_name in (
"model.layers.0.mlp.gate.weight",
"model.layers.0.mlp.gate.expert_bias",
):
with self.subTest(missing_name=missing_name):
weights = _OneShotWeights(
[item for item in public_weights if item[0] != missing_name]
)
with self.assertRaisesRegex(
RuntimeError, "Missing required Bailing VL router weights"
):
wrapper.load_weights(weights)
if __name__ == "__main__":
unittest.main()
@@ -315,7 +315,14 @@ class TestBailingMoeV3Gate(_FusionGateCase):
packed_modules_mapping={},
)
def _reason_on_cuda(self, quant_config):
def _width_only_config(self):
return SimpleNamespace(
architectures=["BailingMoeV3ForCausalLM"],
moe_intermediate_size=1024,
moe_shared_expert_intermediate_size=1024,
)
def _reason_on_cuda(self, quant_config, config=None, model_class=None):
bailing_moe_v3, _ = _import_bailing_modules()
self._seed()
@@ -328,11 +335,61 @@ class TestBailingMoeV3Gate(_FusionGateCase):
),
):
return self._reason(
bailing_moe_v3.BailingMoeV3ForCausalLM,
self._config(),
model_class or bailing_moe_v3.BailingMoeV3ForCausalLM,
config if config is not None else self._config(),
quant_config,
)
def test_width_only_fp4_mixed_experts_cannot_fuse(self):
quant_config = SimpleNamespace(get_name=lambda: "fp8", is_fp4_experts=True)
reason = self._reason_on_cuda(quant_config, self._width_only_config())
self.assertIn("different quant methods", reason)
def test_vl_wrapper_checks_the_width_on_its_text_config(self):
from sglang.srt.models.bailing_mm_v3 import (
BailingMoeV3VLForConditionalGeneration,
)
quant_config = SimpleNamespace(get_name=lambda: "fp8", is_fp4_experts=True)
config = SimpleNamespace(text_config=self._width_only_config())
reason = self._reason_on_cuda(
quant_config, config, BailingMoeV3VLForConditionalGeneration
)
self.assertIn("different quant methods", reason)
def test_width_only_bf16_experts_can_fuse(self):
self.assertIsNone(self._reason_on_cuda(None, self._width_only_config()))
def test_num_shared_experts_only_config_still_fuses(self):
self.assertIsNone(self._reason_on_cuda(None, self._config()))
def test_width_only_int4_mixed_experts_cannot_fuse(self):
reason = self._reason_on_cuda(
self._compressed_tensors(
[r"re:.*mlp\.shared_experts\.(gate|up|down)_proj.*"]
),
self._width_only_config(),
)
self.assertIn("different quant methods", reason)
def test_width_controls_construction_count(self):
bailing_moe_v3, _ = _import_bailing_modules()
self.assertEqual(
bailing_moe_v3._get_bailing_num_shared_experts(self._width_only_config()),
1,
)
self.assertEqual(
bailing_moe_v3._get_bailing_num_shared_experts(self._config()), 1
)
legacy_multi_shared = self._config()
legacy_multi_shared.num_shared_experts = 2
self.assertEqual(
bailing_moe_v3._get_bailing_num_shared_experts(legacy_multi_shared), 2
)
no_shared = self._width_only_config()
no_shared.moe_shared_expert_intermediate_size = 0
self.assertEqual(bailing_moe_v3._get_bailing_num_shared_experts(no_shared), 0)
def test_compressed_tensors_mixed_expert_layout_cannot_fuse(self):
reason = self._reason_on_cuda(
self._compressed_tensors(
@@ -0,0 +1,34 @@
"""Regression tests for the Ling image/video-only processor contract."""
import asyncio
import unittest
from types import SimpleNamespace
from sglang.srt.multimodal.processors.bailing_mm import (
BailingMMMultimodalProcessor,
)
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
class TestBailingMMProcessor(CustomTestCase):
def test_audio_request_fails_before_preprocessing(self):
"""The public image/video checkpoint must reject audio at the API boundary."""
processor = BailingMMMultimodalProcessor.__new__(BailingMMMultimodalProcessor)
request = SimpleNamespace(audio_data=["audio.wav"])
with self.assertRaisesRegex(ValueError, "Audio inputs are not supported"):
asyncio.run(
processor.process_mm_data_async(
image_data=[],
audio_data=request.audio_data,
input_text="test",
request_obj=request,
)
)
if __name__ == "__main__":
unittest.main()
@@ -770,6 +770,7 @@ class TestSchedulerMmTransportBoundary(unittest.TestCase):
@staticmethod
def _prepare_scheduler(scheduler):
scheduler.model_config = SimpleNamespace(requires_mm_token_modalities=False)
scheduler.scheduler_stage_metrics = None
scheduler.session_controller = SimpleNamespace(maybe_reap=MagicMock())
scheduler._request_dispatcher = MagicMock(return_value=None)
@@ -785,6 +786,7 @@ class TestSchedulerMmTransportBoundary(unittest.TestCase):
self.mm_inputs = object()
scheduler = object.__new__(scheduler_module.Scheduler)
scheduler.model_config = SimpleNamespace(requires_mm_token_modalities=False)
scheduler.dp_tp_cpu_group = object()
request = TokenizedRequest()
@@ -861,7 +863,9 @@ class TestSchedulerMmTransportBoundary(unittest.TestCase):
):
scheduler.process_input_requests([request])
build_inputs.assert_called_once_with(raw_inputs)
build_inputs.assert_called_once_with(
raw_inputs, requires_mm_token_modalities=False
)
self.assertIs(request.mm_inputs, materialized)
scheduler._request_dispatcher.assert_called_once_with(request)
cpu_broadcast.assert_not_called()
@@ -914,7 +918,7 @@ class TestSchedulerMmTransportBoundary(unittest.TestCase):
self.assertEqual(
build_inputs.call_args_list,
[call(value) for value in raw_inputs],
[call(value, requires_mm_token_modalities=False) for value in raw_inputs],
)
self.assertEqual(
[inner.mm_inputs for inner in inner_requests],
@@ -940,6 +944,7 @@ class TestSchedulerMmTransportBoundary(unittest.TestCase):
from sglang.srt.managers import scheduler as scheduler_module
scheduler = object.__new__(scheduler_module.Scheduler)
scheduler.model_config = SimpleNamespace(requires_mm_token_modalities=False)
scheduler.dp_tp_group = SimpleNamespace(rank_in_group=0, first_rank=0)
scheduler.dp_tp_cpu_group = object()
@@ -17,6 +17,7 @@ from sglang.srt.parser.template_detection import (
)
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=2.0, suite="base-a-test-cpu")
@@ -58,7 +59,7 @@ def _glm53_template(concat):
)
class TestTemplateManagerReasoningDetection(unittest.TestCase):
class TestTemplateManagerReasoningDetection(CustomTestCase):
def _detect(self, template, vocab):
force, config = detect_reasoning_pattern(template)
parser = detect_reasoning_parser(
@@ -99,6 +100,25 @@ class TestTemplateManagerReasoningDetection(unittest.TestCase):
)
self.assertEqual(parser, "glm45")
def test_ling3_template_uses_ling3_parsers(self):
template = """
{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}
{{ '<role>SYSTEM</role>' }}
{{ '<role>ASSISTANT</role>' }}
{{ '<|role_end|>' }}
<tool_call>{function-name}
<arg_key>{arg-key}</arg_key>
<arg_value>{arg-value}</arg_value>
</tool_call>
"""
force, config, reasoning_parser = self._detect(template, [])
tool_call_parser = detect_tool_call_parser(
template, _DummyTokenizer([]), config, force
)
self.assertEqual(reasoning_parser, "ling3")
self.assertEqual(tool_call_parser, "ling3")
def test_glm53_effort_template_resolves_glm_parsers(self):
# Without an enable_thinking toggle the GLM-4.5 rule misses, and the
# template used to fall through to deepseek-r1 + the xml_kv fallback
@@ -929,7 +949,7 @@ def _declared(server_args, field):
return resolution_result(server_args, field)
class TestResolveAutoParsers(unittest.TestCase):
class TestResolveAutoParsers(CustomTestCase):
"""Tests for resolve_auto_parsers()."""
qwen3_template = "{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}"
@@ -1065,6 +1085,31 @@ class TestResolveAutoParsers(unittest.TestCase):
self.assertEqual(_declared(args, "reasoning_parser"), "kimi_k3")
self.assertEqual(_declared(args, "tool_call_parser"), "kimi_k3")
def test_bailing_architectures_and_model_types_use_ling3_parsers(self):
cases = (
(["BailingMoeV3VLForConditionalGeneration"], ""),
(None, "bailing_moe_v3_vl"),
(["BailingMoeV3ForCausalLM"], ""),
(None, "bailing_hybrid"),
)
for architectures, model_type in cases:
with self.subTest(architectures=architectures, model_type=model_type):
args = self._make_server_args(
reasoning_parser="auto", tool_call_parser="auto"
)
tokenizer = _DummyTokenizer([])
config = SimpleNamespace(
architectures=architectures, model_type=model_type
)
with _patch_hf_transformers_utils(
Mock(return_value=tokenizer), Mock(return_value=config)
):
resolve_auto_parsers(args)
self.assertEqual(_declared(args, "reasoning_parser"), "ling3")
self.assertEqual(_declared(args, "tool_call_parser"), "ling3")
def test_deepseek_arch_fallback_runs_when_tokenizer_load_fails(self):
args = self._make_server_args(reasoning_parser="auto", tool_call_parser="auto")
config = SimpleNamespace(architectures=["DeepseekV32ForCausalLM"])
@@ -1777,6 +1777,74 @@ class TestGoldenModelOverrides(_IsolatedPublish):
"flashinfer_trtllm_routed",
)
def test_bailing_v3_mixed_mxfp4_selects_native_runner(self):
"""Packed MXFP4 experts must not reach the FP8 Triton runner."""
def _args(**kw):
defaults = dict(
device="cuda",
moe_a2a_backend="none",
moe_runner_backend="auto",
_model_config=SimpleNamespace(quantization="fp8", is_fp4_experts=True),
)
defaults.update(kw)
return SimpleNamespace(**defaults)
with override_platform(
is_sm90=False, is_sm100=True, is_sm120=False, is_hip=False
):
for architecture in (
"BailingMoeV3ForCausalLM",
"BailingMoeV3VLForConditionalGeneration",
):
with self.subTest(architecture=architecture):
declarations = collect_model_override_declarations(
architecture,
_args(),
SimpleNamespace(architectures=[architecture]),
)
self.assertEqual(
declarations,
[
(
"_bailing_moe_v3_overrides",
{"moe_runner_backend": "flashinfer_mxfp4"},
)
],
)
from sglang.srt.arg_groups.model_overrides.bailing_moe_v3 import (
_bailing_moe_v3_overrides,
)
hf = SimpleNamespace(
architectures=["BailingMoeV3VLForConditionalGeneration"]
)
self.assertEqual(
_bailing_moe_v3_overrides(_args(moe_runner_backend="triton"), hf),
{},
)
self.assertEqual(
_bailing_moe_v3_overrides(_args(moe_a2a_backend="deepep"), hf),
{},
)
self.assertEqual(
_bailing_moe_v3_overrides(
_args(
_model_config=SimpleNamespace(
quantization="fp8", is_fp4_experts=False
)
),
hf,
),
{},
)
with override_platform(
is_sm90=False, is_sm100=False, is_sm120=False, is_hip=False
):
self.assertEqual(_bailing_moe_v3_overrides(_args(), hf), {})
def test_nemotron_h_overrides_at_callable_level(self):
from sglang.srt.arg_groups.model_overrides.nemotron_h import (
_nemotron_h_overrides,