refactor(inputs): build the Omni renderer as a subclass instead of a proxy

OmniRenderer wrapped the upstream renderer, forwarded unknown attributes via
__getattr__, and re-bound eight BaseRenderer methods (render_cmpl, render_chat,
process_for_engine, _process_enc_dec and their async twins) onto the wrapper
so that they would reach Omni's _process_singleton. That allow-list hid any
concrete-renderer override of those methods and would silently skip the
Omni extras for any future BaseRenderer entry point not on the list.

Make OmniRenderer a mixin and create the renderer as a real subclass of the
upstream class instead:

- omni_renderer_cls(base_cls) builds (and caches) Omni<Base> = (OmniRenderer,
  base_cls); only _process_singleton / _process_singleton_async are extended,
  everything else is the upstream renderer unchanged.
- build_omni_renderer(vllm_config) resolves the class the way upstream
  renderer_from_config does (cached_tokenizer_from_config,
  tokenizer_args_from_config, RENDERER_REGISTRY.load_renderer_cls) and
  instantiates the Omni subclass; renderer_cls/tokenizer can be passed to
  skip the resolution.
- build_stage0_input_processor passes the renderer to InputProcessor up
  front instead of replacing it afterwards; the skip-tokenizer branch keeps
  _build_token_only_renderer and never resolves a tokenizer.

Rebinding an existing instance's __class__ was rejected: CPython refuses it
for HfRenderer ("object layout differs"), so the subclass is constructed.

Tests: tests/inputs/test_preprocess.py builds the renderer with
omni_renderer_cls and adds checks that the subclass is created once, that a
concrete renderer's render_cmpl override is no longer suppressed, and that
build_omni_renderer resolves like upstream or honours an explicit class;
tests/engine/test_async_omni_engine_stage_init.py asserts the stage-0
processor receives the built subclass instance and that the skip-tokenizer
branch never calls build_omni_renderer. Two pre-existing mypy complaints in
test_preprocess.py are fixed with annotations.

Validation: ruff, pre-commit (incl. mypy) on the four files; the modules
import and omni_renderer_cls(HfRenderer) builds; tests/inputs (32),
tests/engine/test_async_omni_engine_stage_init.py (55) and tests/entrypoints
(548) pass on CPU.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MpXe2LeRrUQGjZfKFaocMZ
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
This commit is contained in:
tzhouamandClaude Fable 5.1 committed 2026-09-10 00:41:53 +08:00
1 parent a58825ed6c
commit fb94b87d4b
4 files changed
+184 -53

No files matched your search

@@ -1593,22 +1593,34 @@ def test_model_path_resolver_is_generic_and_model_owned(tmp_path):
assert "model_path_resolver" not in engine_args
def test_build_stage0_input_processor_wraps_active_renderer(monkeypatch):
import vllm_omni.engine.stage_init_utils as init_mod
from vllm_omni.inputs.preprocess import OmniRenderer
def test_build_stage0_input_processor_uses_omni_renderer_subclass(monkeypatch):
from vllm.renderers import BaseRenderer
original = object()
import vllm_omni.engine.stage_init_utils as init_mod
from vllm_omni.inputs.preprocess import OmniRenderer, omni_renderer_cls
class _Base(BaseRenderer):
def __init__(self, config, tokenizer):
self.config, self.tokenizer = config, tokenizer
def render_messages(self, messages, params): # pragma: no cover - abstract stub
raise NotImplementedError
config = types.SimpleNamespace(model_config=types.SimpleNamespace(try_get_generation_config=lambda: {}))
built = omni_renderer_cls(_Base)(config, "tok")
seen = {}
class DummyInputProcessor:
def __init__(self, vllm_config, renderer=None):
self.renderer = renderer or original
seen["renderer"] = renderer
self.renderer = renderer
monkeypatch.setattr(init_mod, "InputProcessor", DummyInputProcessor)
processor = build_stage0_input_processor(
types.SimpleNamespace(model_config=types.SimpleNamespace(try_get_generation_config=lambda: {}))
)
monkeypatch.setattr(init_mod, "build_omni_renderer", lambda cfg: built if cfg is config else None)
processor = build_stage0_input_processor(config)
assert seen["renderer"] is built
assert isinstance(processor.renderer, OmniRenderer)
assert processor.renderer._renderer is original
assert isinstance(processor.renderer, _Base)
assert not hasattr(processor, "input_preprocessor")
@@ -1622,13 +1634,17 @@ def test_build_stage0_input_processor_does_not_resolve_tokenizer_when_skipped(mo
assert renderer is original
self.renderer = renderer
def _must_not_resolve(_cfg):
raise AssertionError("tokenizer must not be resolved when skip_tokenizer_init=True")
monkeypatch.setattr(init_mod, "InputProcessor", DummyInputProcessor)
monkeypatch.setattr(init_mod, "_build_token_only_renderer", lambda _: original)
monkeypatch.setattr(init_mod, "build_omni_renderer", _must_not_resolve)
config = types.SimpleNamespace(
model_config=types.SimpleNamespace(skip_tokenizer_init=True, try_get_generation_config=lambda: {})
)
processor = build_stage0_input_processor(config)
assert processor.renderer._renderer is original
assert processor.renderer is original
def test_inject_kv_stage_info_infers_sender_tp_topology():