[Rust] Split and rename embedded server components (#37220)
This commit is contained in:
@@ -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."""
|
||||
|
||||
+5
-5
@@ -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]
|
||||
|
||||
+9
-6
@@ -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):
|
||||
|
||||
+11
-11
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user