Add Ling-3.0-flash-VL model support (#38526)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user