[Rust] Split and rename embedded server components (#37220)

This commit is contained in:
Lianmin Zheng
2026-08-31 12:28:43 -07:00
committed by GitHub
parent cf51650335
commit 1da86b9801
41 changed files with 3409 additions and 3293 deletions
@@ -1,4 +1,4 @@
"""Shared fixtures for the native Rust multimodal suites.
"""Shared fixtures for the Rust multimodal suites.
Imported via ``sys.path`` from the sibling suites (unittest runs these files by
path, so a package-relative import would break ``python <file>``); the module
@@ -76,7 +76,7 @@ def make_processor(case, config, image_processor_cls=None):
skip_tokenizer_init=False,
mm_preprocess_cache_size_mb=0,
trust_mm_content_hashes=False,
# Read by NativeMmHost._use_feature_shm (single-rank fixture → the
# Read by RustMmProcessor._use_feature_shm (single-rank fixture → the
# inline zero-copy transport, like the 1-GPU e2e).
tp_size=1,
dist_init_addr=None,
@@ -1,7 +1,7 @@
"""Native driver error paths: out-of-scope and malformed inputs are rejected.
Covers ``process`` in ``rust/sglang-mm/src/driver.rs`` (via the
``_core.qwen_vl.process_native_mm`` binding). The wire-payload parsing that
``_core.qwen_vl.process_mm`` binding). The wire-payload parsing that
feeds this driver (modality/shape rejection) lives in ``sglang-server``'s
message layer and is tested with the integration PR.
@@ -47,13 +47,13 @@ def gif_bytes():
@unittest.skipUnless(
QWEN_CORE and hasattr(QWEN_CORE, "process_native_mm"),
QWEN_CORE and hasattr(QWEN_CORE, "process_mm"),
"sglang-mm native Qwen driver not built",
)
class TestNativeDriverErrorPaths(CustomTestCase):
def assert_rejected(self, input_ids, images, pattern, spec=SPEC):
with self.assertRaisesRegex(ValueError, pattern):
QWEN_CORE.process_native_mm(input_ids, images, spec)
QWEN_CORE.process_mm(input_ids, images, spec)
def test_degenerate_geometry_rejected_not_panicked(self):
"""A thin image against a tight ``max_pixels`` floors a side of the
@@ -88,7 +88,7 @@ class TestNativeDriverErrorPaths(CustomTestCase):
"""GIF moved from rejected to served when the pure-Rust webp/gif/bmp
decoders were enabled; this pins the accept side of that contract flip
(the reject side used to be asserted here and broke in CI)."""
_, _, grids, _, offsets, _, _ = QWEN_CORE.process_native_mm(
_, _, grids, _, offsets, _, _ = QWEN_CORE.process_mm(
IMAGE_IDS, [gif_bytes()], SPEC
)
self.assertEqual(len(grids), 1)
@@ -1,7 +1,7 @@
"""End-to-end parity at the scheduler-input boundary.
`test_preprocess.py` pins the `preprocess` binding; this drives the whole native
path — the `process_native_mm` driver, then `NativeMmHost.build_native_mm` — and
path — the `process_mm` driver, then `RustMmProcessor.build_output` — and
compares every field the scheduler reads against the Python `mm_processor`.
Bitwise, for both HF backends: the Rust resize clones PIL's fixed-point bicubic
and ATen's uint8 antialias kernel, so whichever one a server is configured with
@@ -23,7 +23,7 @@ from sglang.test.test_utils import CustomTestCase, maybe_stub_sgl_kernel
maybe_stub_sgl_kernel()
from sglang.srt.managers.rust_server import NativeMmHost # noqa: E402
from sglang.srt.rust_server.multimodal import RustMmProcessor # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
@@ -33,7 +33,7 @@ from _mm_rust_utils import PROCESSOR_CONFIGS, image_bytes, load_core # noqa: E4
register_cpu_ci(est_time=40, suite="base-a-test-cpu")
CORE = load_core()
DRIVER = getattr(getattr(CORE, "qwen_vl", None), "process_native_mm", None)
DRIVER = getattr(getattr(CORE, "qwen_vl", None), "process_mm", None)
# The fixture tokenizer's vocab (see `_fixtures.make_processor`):
# 1 = <|vision_start|>, 2 = <|image_pad|>, 3 = <|vision_end|>, 4 = "hello".
@@ -68,21 +68,21 @@ class TestQwenE2eParity(CustomTestCase):
import_processors("sglang.srt.multimodal.processors")
# Skip __init__: it would build a processor; reuse the fixture's.
host = NativeMmHost.__new__(NativeMmHost)
host = RustMmProcessor.__new__(RustMmProcessor)
host.model_config = SimpleNamespace(hf_config=self.processor.hf_config)
host._processor = self.processor._processor
host.server_args = self.processor.server_args
spec = host.resolve_native_spec()
spec = host.resolve_spec()
self.assertIsNotNone(spec, f"gate rejected {self.image_processor}")
return spec
def run_native(self, spec, sources):
"""The Rust path: the `process_native_mm` driver, then the drain
"""The Rust path: the `process_mm` driver, then the drain
adapter — the same two steps `RustServer.drain` performs."""
ids, features, grids, hashes, offsets, mrope, delta = DRIVER(
PROMPT_PER_IMAGE * len(sources), sources, spec.rust_json()
)
# The shape of Rust's MmEncodeResult, inline transport (test_build_native_mm
# The shape of Rust's MmEncodeResult, inline transport (test_build_output
# pins the shm shape).
handoff = SimpleNamespace(
features=features,
@@ -93,7 +93,7 @@ class TestQwenE2eParity(CustomTestCase):
mrope=mrope,
mrope_delta=delta,
)
return snapshot(ids, NativeMmHost.build_native_mm(spec, handoff))
return snapshot(ids, RustMmProcessor.build_output(spec, handoff))
def run_python(self, sources):
"""The reference path: the Python `mm_processor` the scheduler would use."""
@@ -22,7 +22,7 @@ from sglang.test.test_utils import CustomTestCase, maybe_stub_sgl_kernel
maybe_stub_sgl_kernel()
from sglang.srt.managers.mm_utils import hash_feature # noqa: E402
from sglang.srt.managers.rust_server import NativeMmHost # noqa: E402
from sglang.srt.rust_server.multimodal import RustMmProcessor # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
@@ -32,7 +32,7 @@ from _mm_rust_utils import PROCESSOR_CONFIGS, image_bytes, load_core # noqa: E4
register_cpu_ci(est_time=15, suite="base-a-test-cpu")
CORE = load_core()
DRIVER = getattr(getattr(CORE, "qwen_vl", None), "process_native_mm", None)
DRIVER = getattr(getattr(CORE, "qwen_vl", None), "process_mm", None)
def raw_bytes(source):
@@ -45,7 +45,7 @@ def raw_bytes(source):
@unittest.skipUnless(DRIVER, "sglang-mm native Qwen driver not built")
class TestQwenNativeMmHashes(CustomTestCase):
class TestQwenRustMmHashes(CustomTestCase):
def setUp(self):
from sglang.srt.managers.multimodal_processor import import_processors
@@ -58,11 +58,11 @@ class TestQwenNativeMmHashes(CustomTestCase):
def native_hashes(self, sources):
"""Per-item hashes the Rust driver returns, via the production gate."""
host = NativeMmHost.__new__(NativeMmHost)
host = RustMmProcessor.__new__(RustMmProcessor)
host.model_config = SimpleNamespace(hf_config=self.processor.hf_config)
host._processor = self.processor._processor
host.server_args = self.processor.server_args
spec = host.resolve_native_spec()
spec = host.resolve_spec()
self.assertIsNotNone(spec, "gate rejected the fixture processor")
input_ids = [t for _ in sources for t in (1, 2, 3, 4)]
return DRIVER(input_ids, sources, spec.rust_json())[3]
@@ -3,7 +3,7 @@
Covers ``layout_by_placeholder`` / ``apply_layout`` in
``rust/sglang-mm/src/common/token_layout.rs`` and ``mrope_image_only`` in
``rust/sglang-mm/src/qwen_vl/mod.rs`` (via the
``_core.qwen_vl.process_native_mm`` and ``mrope_image_only_py``
``_core.qwen_vl.process_mm`` and ``mrope_image_only_py``
bindings), against ``BaseMultimodalProcessor`` expansion/offsets and
``MRotaryEmbedding.get_rope_index``.
"""
@@ -36,7 +36,7 @@ QWEN_CORE = getattr(load_core(), "qwen_vl", None)
@unittest.skipUnless(
QWEN_CORE and hasattr(QWEN_CORE, "process_native_mm"),
QWEN_CORE and hasattr(QWEN_CORE, "process_mm"),
"sglang-mm native Qwen driver not built",
)
class TestQwenPromptGeometry(CustomTestCase):
@@ -52,7 +52,7 @@ class TestQwenPromptGeometry(CustomTestCase):
ids.extend((VISION_START_ID, IMAGE_TOKEN_ID, VISION_END_ID, 8))
images = [image_bytes(96 + 8 * i, 80, i) for i in range(image_count)]
with self.subTest(image_count=image_count):
actual_ids, _, grids, _, offsets, _, _ = QWEN_CORE.process_native_mm(
actual_ids, _, grids, _, offsets, _, _ = QWEN_CORE.process_mm(
ids, images, spec_json(config)
)
counts = [t * h * w // config["merge_size"] ** 2 for t, h, w in grids]
@@ -1,4 +1,4 @@
"""``NativeMmHost.build_native_mm`` (managers/rust_server.py): the drain-time
"""``RustMmProcessor.build_output``: the drain-time
wrapping contracts — tensors are zero-copy views over the Rust-owned buffers, and
pad values come from worker-precomputed hashes, since the scheduler loop must
never hash features. Synthetic buffers, so this needs no Rust extension."""
@@ -15,15 +15,18 @@ from sglang.test.test_utils import CustomTestCase, maybe_stub_sgl_kernel
maybe_stub_sgl_kernel()
from sglang.srt.managers.rust_server import NativeMmHost, NativeMmSpec # noqa: E402
from sglang.srt.rust_server.multimodal import ( # noqa: E402
RustMmProcessor,
RustMmSpec,
)
register_cpu_ci(est_time=3, suite="base-a-test-cpu")
class TestBuildNativeMm(CustomTestCase):
class TestBuildRustMmOutput(CustomTestCase):
def setUp(self):
# feature_dim == 3 * temporal_patch_size * patch_size**2 == 6.
self.spec = NativeMmSpec(
self.spec = RustMmSpec(
family="qwen_vl",
feature_shm=False,
image_token_id=10,
@@ -50,7 +53,7 @@ class TestBuildNativeMm(CustomTestCase):
def build(self):
features = np.arange(30, dtype=np.float32)
output = NativeMmHost.build_native_mm(
output = RustMmProcessor.build_output(
self.spec,
SimpleNamespace( # the shape of Rust's MmEncodeResult
grids=self.GRIDS,
@@ -99,7 +102,7 @@ class TestBuildNativeMm(CustomTestCase):
)
class TestBuildNativeMmShm(TestBuildNativeMm):
class TestBuildRustMmOutputShm(TestBuildRustMmOutput):
"""The shm entry shape (TP>1): features arrive as named POSIX segments, and
each item becomes a ``ShmPointerMMData`` stub whose ``materialize()`` yields
that item's slice — and unlinks, taking the cleanup duty exactly once."""
@@ -28,7 +28,7 @@ FETCH = CORE and CORE.common.fetch_bytes
@unittest.skipUnless(FETCH, "sglang-mm fetch binding not built")
class TestRustMediaSourceLoading(CustomTestCase):
DATA = b"native-mm-source"
DATA = b"rust-mm-source"
def test_inline_sources(self):
encoded = base64.b64encode(self.DATA).decode()
@@ -1,4 +1,4 @@
"""Model-independent image decode parity for native Rust MM.
"""Model-independent image decode parity for Rust MM.
Covers ``decode_rgb`` in ``rust/sglang-mm/src/common/mod.rs`` (via the
``_core.common.image_decode_rgb`` binding), against PIL's
@@ -1,4 +1,4 @@
"""``RustServer._partition_cores`` (managers/rust_server.py): the pool cores must
"""``rust_server.config._partition_cores``: the pool cores must
be a *bounded* slice of this rank's allowed cores, not the whole remainder —
sibling TP ranks share the NUMA node, so an unbounded mask lets MM preprocessing
bursts preempt a sibling's CUDA-launch thread (measured: ~20 ms of ViT wall time
@@ -12,14 +12,14 @@ from sglang.test.test_utils import CustomTestCase, maybe_stub_sgl_kernel
maybe_stub_sgl_kernel()
from sglang.srt.managers.rust_server import RustServer # noqa: E402
from sglang.srt.rust_server.config import _partition_cores # noqa: E402
register_cpu_ci(est_time=1, suite="base-a-test-cpu")
def partition(node_cores, **kwargs):
with patch("os.sched_getaffinity", return_value=set(node_cores), create=True):
return RustServer._partition_cores(**kwargs)
return _partition_cores(**kwargs)
class TestPartitionCores(CustomTestCase):
@@ -1,10 +1,10 @@
"""The native-MM launch gate's family selection (managers/rust_server.py).
"""The Rust-MM launch gate's family selection.
``NATIVE_MM_FAMILIES`` decides which models the Rust pipeline serves natively;
for everything else ``native_mm_family_for`` must return ``None``, which
``RUST_MM_FAMILIES`` decides which models the Rust pipeline serves;
for everything else ``rust_mm_family_for`` must return ``None``, which
``RustServer.launch`` turns into a hard launch error. Pins that non-Qwen
multimodal models — Inkling being the in-tree case — keep their Python
processor and never match a native family, so growing the registry cannot
processor and never match a Rust family, so growing the registry cannot
silently reroute them.
"""
@@ -20,25 +20,25 @@ from sglang.srt.managers.multimodal_processor import ( # noqa: E402
get_mm_processor_cls,
import_processors,
)
from sglang.srt.managers.rust_server import native_mm_family_for # noqa: E402
from sglang.srt.rust_server.multimodal import rust_mm_family_for # noqa: E402
register_cpu_ci(est_time=10, suite="base-a-test-cpu")
def processor_cls_for(architecture, model_type):
"""Through the production selection, as `resolve_native_spec` calls it."""
"""Through the production selection, as `resolve_spec` calls it."""
hf_config = SimpleNamespace(architectures=[architecture], model_type=model_type)
return get_mm_processor_cls(hf_config, SimpleNamespace(model_impl="sglang"))
class TestNativeMmGate(CustomTestCase):
class TestRustMmGate(CustomTestCase):
@classmethod
def setUpClass(cls):
import_processors("sglang.srt.multimodal.processors")
def test_qwen_vl_resolves_its_family(self):
cls = processor_cls_for("Qwen2_5_VLForConditionalGeneration", "qwen2_5_vl")
family = native_mm_family_for(cls, "qwen2_5_vl")
family = rust_mm_family_for(cls, "qwen2_5_vl")
self.assertEqual(family and family.name, "qwen_vl")
def test_inkling_keeps_its_python_processor(self):
@@ -46,15 +46,15 @@ class TestNativeMmGate(CustomTestCase):
cls = processor_cls_for("InklingForConditionalGeneration", "inkling_model")
self.assertIs(cls, InklingMultimodalProcessor)
self.assertIsNone(native_mm_family_for(cls, "inkling_model"))
self.assertIsNone(rust_mm_family_for(cls, "inkling_model"))
def test_family_requires_both_processor_and_model_type(self):
qwen = processor_cls_for("Qwen2_5_VLForConditionalGeneration", "qwen2_5_vl")
self.assertIsNone(native_mm_family_for(qwen, "inkling_model"))
self.assertIsNone(rust_mm_family_for(qwen, "inkling_model"))
# Identity, not name: an override class must not match (the
# SGLANG_EXTERNAL_MM_PROCESSOR_PACKAGE contract).
impostor = type("QwenVLImageProcessor", (), {})
self.assertIsNone(native_mm_family_for(impostor, "qwen2_5_vl"))
self.assertIsNone(rust_mm_family_for(impostor, "qwen2_5_vl"))
if __name__ == "__main__":
@@ -2,7 +2,7 @@
Covers what the CPU parity units structurally cannot: the sidecar handoff, the
drain ordering, Rust-side tokenization of multimodal prompts, and the rejection
of inputs outside the native pipeline's scope (there is no Python fallback).
of inputs outside the Rust pipeline's scope (there is no Python fallback).
"""
import base64
@@ -51,7 +51,7 @@ def solid_image_data_url(fmt):
importlib.util.find_spec("sglang.srt.rust_extensions._server") is None,
"sglang-server rust extension not installed (e.g. AMD suite)",
)
class TestRustServerNativeMm(CustomTestCase):
class TestRustServerMm(CustomTestCase):
env = {"SGLANG_RUST_SERVER": "1"}
@classmethod
+11 -11
View File
@@ -1,7 +1,7 @@
"""MMMU accuracy gate for the Rust tokenizer manager's native multimodal path.
"""MMMU accuracy gate for the Rust tokenizer manager's multimodal path.
``test_rust_native_mm_e2e.py`` checks that the output is *valid*; this checks that
native Rust preprocessing yields *equally good* model inputs. A systematic skew
Rust preprocessing yields *equally good* model inputs. A systematic skew
(wrong resample filter, channel order, normalization, patch layout) still reads as
fluent text and passes a keyword smoke check, but drops MMMU below the gate.
@@ -36,7 +36,7 @@ MODEL = "Qwen/Qwen3.5-0.8B"
VISION_BLOCK = "<|vision_start|><|image_pad|><|vision_end|>"
NUM_EXAMPLES = 100
# Calibrated 2026-07-24 on H200: the native path scores 0.37 on this fixed subset
# Calibrated 2026-07-24 on H200: the Rust path scores 0.37 on this fixed subset
# at temperature 0 (two runs), matching the Python reference (0.37, same sampler
# and samples). The gate leaves headroom for batching nondeterminism.
MMMU_ACCURACY_THRESHOLD = 0.30
@@ -101,10 +101,10 @@ class QwenGenerateVisionSampler(SamplerBase):
importlib.util.find_spec("sglang.srt.rust_extensions._server") is None,
"sglang-server rust extension not installed (e.g. AMD suite)",
)
class TestRustNativeMmMMMU(CustomTestCase):
class TestRustMmMMMU(CustomTestCase):
@classmethod
def setUpClass(cls):
# Capture the server log so the test can pin that the native MM
# Capture the server log so the test can pin that the Rust MM
# pipeline is active.
cls.log_dir = tempfile.TemporaryDirectory()
cls.server_logs = tuple(
@@ -139,12 +139,12 @@ class TestRustNativeMmMMMU(CustomTestCase):
def test_mmmu_accuracy(self):
# Guard the path under test: if the model ever drops off
# NATIVE_MM_FAMILIES, launch fails and this names why.
# RUST_MM_FAMILIES, launch fails and this names why.
self.assertIn(
"native MM pipeline enabled",
"Rust MM pipeline enabled",
self._read_server_log(),
"rust server did not enable the native MM pipeline for "
f"{MODEL}; this test must exercise the native path",
"rust server did not enable the Rust MM pipeline for "
f"{MODEL}; this test must exercise the Rust path",
)
eval_obj = MMMUVLMEval(num_examples=NUM_EXAMPLES, num_threads=32)
@@ -154,12 +154,12 @@ class TestRustNativeMmMMMU(CustomTestCase):
dump_metric(
"mmmu_score",
result.score,
labels={"model": MODEL, "eval": "mmmu", "api": "generate-rust-native-mm"},
labels={"model": MODEL, "eval": "mmmu", "api": "generate-rust-mm"},
)
self.assertGreaterEqual(
result.score,
MMMU_ACCURACY_THRESHOLD,
f"Rust native MM path scored {result.score:.4f} on MMMU, below the "
f"Rust MM path scored {result.score:.4f} on MMMU, below the "
f"{MMMU_ACCURACY_THRESHOLD:.2f} gate",
)