Merge main and preserve dispatch backlog across monitor windows

Signed-off-by: Sy03 <1370724210@qq.com>
This commit is contained in:
Sy03 committed 2026-09-21 17:13:27 +08:00
commit ea7de7e3de
769 files changed
+73802 -22805

No files matched your search

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Characterization of ``OmniGPUModelRunner.initialize_metadata_builders``.
Upstream ``FlashAttentionMetadataBuilder`` pre-allocates ``scheduler_metadata``
@@ -19,14 +19,12 @@ from types import SimpleNamespace
import pytest
import torch
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
# The class ``super().initialize_metadata_builders`` dispatches to.
_UPSTREAM_RUNNER = OmniGPUModelRunner.__mro__[1]
def _builder(*, prealloc: int | None, max_num_splits: int) -> SimpleNamespace:
sm = None if prealloc is None else torch.zeros(prealloc, dtype=torch.int32)
@@ -36,10 +34,14 @@ def _builder(*, prealloc: int | None, max_num_splits: int) -> SimpleNamespace:
def _make_runner(monkeypatch, *, max_num_seqs: int, builders: list) -> OmniGPUModelRunner:
# Stub the upstream initialize_metadata_builders; our fake runner already
# carries the attn_groups the Omni override iterates over.
monkeypatch.setattr(_UPSTREAM_RUNNER, "initialize_metadata_builders", lambda self, *a, **k: None)
monkeypatch.setattr(GPUModelRunner, "initialize_metadata_builders", lambda self, *a, **k: None)
runner = object.__new__(OmniGPUModelRunner)
runner.scheduler_config = SimpleNamespace(max_num_seqs=max_num_seqs)
runner.cache_config = SimpleNamespace(enable_prefix_caching=False) # skip omni_prefix_cache branch
runner.vllm_config = SimpleNamespace(kv_transfer_config=None)
runner.model_config = SimpleNamespace()
runner.is_pooling_model = False
runner.speculative_config = None
runner.attn_groups = [[SimpleNamespace(metadata_builders=builders)]]
return runner
+30 -11
View File
@@ -18,6 +18,7 @@ from vllm.sampling_params import SamplingParams
from vllm.v1.worker import gpu_input_batch
from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
from vllm_omni.core.prefix_cache import ModelCachePolicy
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
from vllm_omni.entrypoints.openai.tts_adapters.base import PreparedRequest
@@ -37,6 +38,8 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_runner(engine_output_type: str | None, downstream_req_ids: set[str]) -> GPUARModelRunner:
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
runner.vllm_config = SimpleNamespace(
model_config=SimpleNamespace(engine_output_type=engine_output_type),
)
@@ -57,6 +60,8 @@ def _mtp_runner(*, async_scheduling: bool, buffers: dict[str, dict]) -> tuple[GP
return torch.tensor([[11, 12, 13]], dtype=torch.long)
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
runner.use_async_scheduling = async_scheduling
runner.model = SimpleNamespace(post_sample_talker_mtp=post_sample_talker_mtp)
runner.requests = {req_id: SimpleNamespace() for req_id in buffers}
@@ -143,6 +148,8 @@ def test_post_sample_talker_mtp_uses_gpu_token_with_async_scheduling() -> None:
def test_post_sample_talker_mtp_skips_shape_validation_without_duplex_rows() -> None:
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
runner.use_async_scheduling = False
runner.model = SimpleNamespace(
post_sample_talker_mtp=lambda **_: pytest.fail("hook must not run"),
@@ -178,6 +185,8 @@ def test_post_sample_talker_mtp_rejects_invalid_selected_token_shape(
error_match: str,
) -> None:
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
runner.use_async_scheduling = False
runner.model = SimpleNamespace(
post_sample_talker_mtp=lambda **_: pytest.fail("hook must not run"),
@@ -265,6 +274,8 @@ def test_speech_extra_params_reach_model_sampler_as_sampling_metadata(monkeypatc
received = []
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
runner.input_batch = input_batch
runner.model = SimpleNamespace(
prefer_model_sampler=True,
@@ -323,6 +334,8 @@ def test_sparse_mm_req_ids_requires_sparse_audio_marker():
def test_runner_assisted_full_attention_metadata_request_is_opt_in():
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
runner.model = object()
runner.scheduler_config = SimpleNamespace(max_num_seqs=16)
@@ -377,6 +390,8 @@ def test_runner_assisted_full_attention_metadata_request_and_context_hooks():
calls.append(("context", {"enabled": enabled, "num_reqs": num_reqs}))
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
runner.model = Model()
runner.scheduler_config = SimpleNamespace(max_num_seqs=8)
@@ -466,6 +481,8 @@ def test_omni_async_gpu_model_runner_output_reraises_background_exception():
def _make_async_output_runner(engine_output_type: str = "audio"):
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
model_config = SimpleNamespace(
engine_output_type=engine_output_type,
async_chunk=True,
@@ -642,10 +659,11 @@ def test_async_omni_output_guard_requires_safe_conditions():
assert GPUARModelRunner._should_use_async_omni_output(runner)
# Prefix cache no longer forces the sync output path; leftover mm is
# snapshotted at save so the builder can materialize a step late.
runner.omni_prefix_cache = object()
assert not GPUARModelRunner._should_use_async_omni_output(runner)
assert GPUARModelRunner._should_use_async_omni_output(runner)
runner.omni_prefix_cache = None
runner.model.has_postprocess = True
assert not GPUARModelRunner._should_use_async_omni_output(runner)
@@ -656,6 +674,7 @@ def test_async_omni_output_guard_requires_safe_conditions():
def test_build_omni_output_skips_hidden_when_model_opts_out(monkeypatch):
runner = _make_async_output_runner(engine_output_type="latent")
runner.model.omni_pooler_payload_include_hidden = False
runner._pooler_payload_include_hidden_flag = False
monkeypatch.setattr(
GPUARModelRunner,
@@ -699,6 +718,7 @@ def test_build_omni_output_skips_hidden_when_model_opts_out(monkeypatch):
def test_build_omni_output_splits_mm_by_scheduled_tokens_when_hidden_is_tail_only(monkeypatch):
runner = _make_async_output_runner(engine_output_type="latent")
runner.model.omni_pooler_payload_include_hidden = False
runner._pooler_payload_include_hidden_flag = False
runner._async_chunk = False
runner.requests = {"r1": object(), "r2": object(), "r3": object()}
@@ -745,6 +765,7 @@ def test_build_omni_output_splits_mm_by_hidden_len_when_scheduled_is_padded(monk
"""Thinker mm rows align to hidden_states.shape[0], not padded scheduled count."""
runner = _make_async_output_runner(engine_output_type="latent")
runner.model.omni_pooler_payload_include_hidden = False
runner._pooler_payload_include_hidden_flag = False
runner._async_chunk = False
runner.requests = {"r1": object(), "r2": object(), "r3": object()}
@@ -790,6 +811,7 @@ def test_build_omni_output_splits_mm_by_hidden_len_when_scheduled_is_padded(monk
def test_async_snapshot_payload_omits_hidden_when_model_opts_out():
runner = _make_async_output_runner()
runner.model.omni_pooler_payload_include_hidden = False
runner._pooler_payload_include_hidden_flag = False
payload = GPUARModelRunner._build_omni_async_snapshot_payload(
runner,
@@ -819,6 +841,8 @@ def test_runner_assisted_full_attention_metadata_refresh_pads_buffers():
self.commits.append(num_reqs_padded)
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
block_table = BlockTable()
runner.input_batch = SimpleNamespace(
num_computed_tokens_cpu=torch.tensor([10, 20, 99, 99], dtype=torch.int32),
@@ -846,6 +870,8 @@ def test_runner_assisted_full_attention_metadata_refresh_pads_buffers():
@pytest.mark.parametrize("query_start_loc_attr", ["method", "tensor_attr"])
def test_sample_tokens_tail_only_prefix_cache_uses_staged_cpu_hidden_states(monkeypatch, query_start_loc_attr):
runner = object.__new__(GPUARModelRunner)
runner._omni_cache_policy = ModelCachePolicy(needs_full_hidden_states=True)
runner._pooler_payload_include_hidden_flag = True
runner.execute_model_state = ExecuteModelState(
SimpleNamespace(
total_num_scheduled_tokens=3,
@@ -908,12 +934,11 @@ def test_sample_tokens_tail_only_prefix_cache_uses_staged_cpu_hidden_states(monk
)
monkeypatch.setattr(GPUARModelRunner, "eplb_step", lambda self: None)
monkeypatch.setattr(GPUARModelRunner, "_resolve_pooler_payload_req_ids", lambda self, req_ids: ("audio", req_ids))
monkeypatch.setattr(GPUARModelRunner, "_deferred_prefix_cache_mm_keys", lambda self: set())
monkeypatch.setattr(GPUARModelRunner, "_model_needs_full_prefix_hidden_states", lambda self: False)
monkeypatch.setattr(
GPUARModelRunner,
"_maybe_get_combined_prefix_cache_tensors",
lambda *args, **kwargs: (None, None),
"_prepare_prefix_cache_pooler_payload_sources",
lambda self, **kwargs: (kwargs["staged_hidden_states_cpu"], None, None),
)
monkeypatch.setattr(GPUARModelRunner, "_process_additional_information_updates", lambda *args, **kwargs: None)
monkeypatch.setattr(GPUARModelRunner, "_should_accumulate_full_payload_output", lambda self: False)
@@ -946,8 +971,6 @@ def test_build_omni_output_falls_back_to_mm_cpu_without_prefix_merge(monkeypatch
lambda self, req_ids: ("latent", req_ids),
)
monkeypatch.setattr(GPUARModelRunner, "_model_needs_full_prefix_hidden_states", lambda self: False)
monkeypatch.setattr(GPUARModelRunner, "_deferred_prefix_cache_mm_keys", lambda self: {"codes.audio"})
monkeypatch.setattr(GPUARModelRunner, "_stage_deferred_prefix_cache_mm_outputs", lambda *args, **kwargs: None)
monkeypatch.setattr(
GPUARModelRunner,
"_prepare_prefix_cache_pooler_payload_sources",
@@ -1095,8 +1118,6 @@ def test_build_omni_output_filters_multimodal_by_partial_downstream_batch(monkey
lambda self, req_ids: ("latent", ["r1", "r3"]),
)
monkeypatch.setattr(GPUARModelRunner, "_model_needs_full_prefix_hidden_states", lambda self: False)
monkeypatch.setattr(GPUARModelRunner, "_deferred_prefix_cache_mm_keys", lambda self: {"codes.audio"})
monkeypatch.setattr(GPUARModelRunner, "_stage_deferred_prefix_cache_mm_outputs", lambda *args, **kwargs: None)
monkeypatch.setattr(
GPUARModelRunner,
"_prepare_prefix_cache_pooler_payload_sources",
@@ -1172,8 +1193,6 @@ def test_build_omni_output_uses_combined_prefix_cache_mm_payload_for_partial_dow
lambda self, req_ids: ("latent", ["r1", "r3"]),
)
monkeypatch.setattr(GPUARModelRunner, "_model_needs_full_prefix_hidden_states", lambda self: False)
monkeypatch.setattr(GPUARModelRunner, "_deferred_prefix_cache_mm_keys", lambda self: {"codes.audio"})
monkeypatch.setattr(GPUARModelRunner, "_stage_deferred_prefix_cache_mm_outputs", lambda *args, **kwargs: None)
monkeypatch.setattr(
GPUARModelRunner,
"_prepare_prefix_cache_pooler_payload_sources",
+19 -2
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Guard against out-of-vocabulary stop ids in min-tokens masking (#4962).
vLLM's input processor folds the stage tokenizer's EOS id into
@@ -19,7 +19,7 @@ from vllm.v1.sample.logits_processor import (
MinTokensLogitsProcessor,
)
from vllm_omni.worker.sampling_utils import sanitize_min_tokens_stop_ids
from vllm_omni.worker.sampling_utils import sanitize_min_tokens_stop_ids, sanitize_sampling_params_min_tokens_stop_ids
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -28,6 +28,12 @@ CODEC_EOS = 2150
TEXT_EOS = 151645
@pytest.fixture(autouse=True)
def _disable_vllm_pin_memory_for_cpu(monkeypatch: pytest.MonkeyPatch):
"""Keep the module-level H2D helper in CPU-only mode."""
monkeypatch.setattr("vllm.utils.torch_utils.PIN_MEMORY", False)
def _make_min_tokens_proc(stop_token_ids: list[int], eos_token_id: int | None) -> MinTokensLogitsProcessor:
"""Build a MinTokensLogitsProcessor with one active request, mirroring
the production path (engine folds the tokenizer EOS via
@@ -113,3 +119,14 @@ def test_guard_handles_multiple_requests():
assert out[0, CODEC_EOS] == -float("inf")
assert out[1, 7] == -float("inf")
assert int((out == -float("inf")).sum()) == 2
def test_sampling_params_guard_filters_only_unreachable_min_tokens_ids():
params = SamplingParams(min_tokens=2, stop_token_ids=[CODEC_EOS])
params.update_from_generation_config({}, TEXT_EOS)
sanitize_sampling_params_min_tokens_stop_ids(params, TALKER_VOCAB)
assert params.all_stop_token_ids == {CODEC_EOS}
assert params.stop_token_ids == [CODEC_EOS]
assert params.eos_token_id == TEXT_EOS
@@ -0,0 +1,91 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for per-request model sampler arguments.
Custom model samplers that set ``model_sampler_wants_extra_args = True``
(e.g. HunyuanImage3's per-request ``ar_task_mode``, #6088) receive one
``SamplingParams.extra_args`` entry per batch row, in ``input_batch.req_ids``
order — the same order ``_build_model_sampler_output_token_ids`` uses. Pure CPU.
"""
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.worker.sampling_utils import build_model_sampler_extra_args, call_model_sampler
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _state(extra_args):
return SimpleNamespace(sampling_params=SimpleNamespace(extra_args=extra_args))
def test_row_order_follows_req_ids():
input_batch = SimpleNamespace(req_ids=["b", "a"])
requests = {
"a": _state({"ar_task_mode": "comprehension"}),
"b": _state(None),
}
assert build_model_sampler_extra_args(input_batch, requests) == [None, {"ar_task_mode": "comprehension"}]
def test_missing_request_state_yields_none():
assert build_model_sampler_extra_args(SimpleNamespace(req_ids=["gone"]), {}) == [None]
def test_request_without_sampling_params_yields_none():
input_batch = SimpleNamespace(req_ids=["r0"])
assert build_model_sampler_extra_args(input_batch, {"r0": SimpleNamespace(sampling_params=None)}) == [None]
def test_empty_batch():
assert build_model_sampler_extra_args(SimpleNamespace(req_ids=[]), {}) == []
def test_opted_in_sampler_receives_per_request_extra_args(mocker):
model = SimpleNamespace(model_sampler_wants_extra_args=True)
model_sample = mocker.Mock(return_value="sampled")
input_batch = SimpleNamespace(req_ids=["a", "b"])
logits = torch.empty(0)
metadata = SimpleNamespace()
requests = {
"a": _state(None),
"b": _state({"ar_task_mode": "comprehension"}),
}
result = call_model_sampler(
model,
model_sample,
logits,
metadata,
input_batch=input_batch,
requests=requests,
)
assert result == "sampled"
model_sample.assert_called_once_with(
logits,
metadata,
per_req_extra_args=[None, {"ar_task_mode": "comprehension"}],
)
def test_non_opted_in_sampler_keeps_two_argument_contract(mocker):
model = SimpleNamespace(model_sampler_wants_extra_args=False)
model_sample = mocker.Mock(return_value="sampled")
logits = torch.empty(0)
metadata = SimpleNamespace()
result = call_model_sampler(
model,
model_sample,
logits,
metadata,
input_batch=SimpleNamespace(req_ids=["a"]),
requests={"a": _state({"ar_task_mode": "comprehension"})},
)
assert result == "sampled"
model_sample.assert_called_once_with(logits, metadata)
+1 -1
View File
@@ -2639,7 +2639,7 @@ _REQUIRED_DUPLEX_CALLS = {
"_sample": "_apply_duplex_sampling",
}
_MODEL_SAMPLER_CALL = "model_sample"
_MODEL_SAMPLER_CALL = "call_model_sampler"
def _repo_root():
+14 -1
View File
@@ -397,7 +397,7 @@ class TestLoadCustomFuncSelection(unittest.TestCase):
def test_skips_non_payload_stage_input_processors_for_full_payload_mode(self):
incompatible_paths = [
"vllm_omni.model_executor.stage_input_processors.mimo_audio.llm2code2wav",
"vllm_omni.model_executor.stage_input_processors.mammoth_moda2.ar2dit",
"vllm_omni.model_executor.stage_input_processors.mammoth_moda2.ar2diffusion",
"vllm_omni.model_executor.stage_input_processors.cosyvoice3.text2flow",
"vllm_omni.model_executor.stage_input_processors.glm_image.ar2diffusion",
]
@@ -1724,3 +1724,16 @@ class TestSendRetry(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
@pytest.mark.parametrize(
"fallback,external,expected", [("r", None, "mapped"), (None, "ext", "ext"), (None, None, None)]
)
def test_resolve_request_id_requires_internal_or_external_id(fallback, external, expected):
host = SimpleNamespace(_request_ids_mapping={"r": "mapped"})
request = SimpleNamespace(external_req_id=external)
if expected is None:
with pytest.raises(ValueError, match="request ID"):
OmniConnectorModelRunnerMixin._resolve_external_req_id(host, request, fallback)
else:
assert OmniConnectorModelRunnerMixin._resolve_external_req_id(host, request, fallback) == expected
+4 -2
View File
@@ -770,8 +770,10 @@ def test_accumulate_full_payload_output_keeps_all_zero_qwen3_omni_prefill_placeh
def test_full_payload_output_accumulation_hook_matrix():
assert _make_full_payload_accumulation_runner(model_stage="thinker")._should_accumulate_full_payload_output()
assert _make_full_payload_accumulation_runner(model_stage="talker")._should_accumulate_full_payload_output()
assert not _make_full_payload_accumulation_runner(
model_stage="code2wav", final_output=True
# A stage may publish a user-visible output and still feed another stage
# (Qwen3-Omni thinker publishes text while sending audio state to talker).
assert _make_full_payload_accumulation_runner(
model_stage="thinker", final_output=True
)._should_accumulate_full_payload_output()
assert not _make_full_payload_accumulation_runner(
model_stage="token2audio",
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
import pytest
from vllm.compilation.cuda_graph import CUDAGraphMode
from vllm_omni.worker_v2.omni_model_runner import OmniGPUModelRunner
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.mark.parametrize("dp_size", [1, 2])
def test_dispatch_eager_and_manager_paths(monkeypatch, dp_size):
runner = object.__new__(OmniGPUModelRunner)
runner.cudagraph_manager = None
runner.dp_size = dp_size
runner.dp_rank = 1
import vllm.v1.worker.gpu.dp_utils as dp_utils
expected_tokens = object()
sync_calls = []
def sync_padding(*args, **kwargs):
sync_calls.append((args, kwargs))
return args[1], expected_tokens
monkeypatch.setattr(
dp_utils,
"sync_cudagraph_and_dp_padding",
sync_padding,
)
batch_desc, num_tokens_across_dp = runner._dispatch_batch_descriptor(
num_reqs=1, num_toks=8, uniform_tok_count=8, num_active_loras=0, use_eager=True, max_query_len=8
)
assert batch_desc.cg_mode == CUDAGraphMode.NONE
if dp_size == 1:
assert num_tokens_across_dp is None
assert sync_calls == []
else:
assert num_tokens_across_dp is expected_tokens
# Non-eager dispatch goes through the cudagraph manager.
expected = SimpleNamespace(cg_mode=CUDAGraphMode.PIECEWISE, num_tokens=8, num_reqs=1)
dispatch_calls = []
def dispatch(*args, **kwargs):
dispatch_calls.append((args, kwargs))
return expected
runner.cudagraph_manager = SimpleNamespace(dispatch=dispatch)
runner.dp_size = 1
batch_desc, _ = runner._dispatch_batch_descriptor(
num_reqs=1, num_toks=8, uniform_tok_count=8, num_active_loras=0, use_eager=False, max_query_len=8
)
assert batch_desc is expected
assert dispatch_calls == [((1, 8, 8), {"num_active_loras": 0, "max_query_len": 8})]
+34
View File
@@ -0,0 +1,34 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
import pytest
from vllm_omni.worker import sampling_utils
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_fixed_seed_contract_separates_stage_sampler_from_talker_mtp_rng() -> None:
assert hasattr(sampling_utils, "apply_fixed_seed_to_sampling_params")
assert hasattr(sampling_utils, "get_tts_local_seed")
stage_only = SimpleNamespace(seed=None, extra_args=None)
sampling_utils.apply_fixed_seed_to_sampling_params(
stage_only,
17,
seed_talker_mtp=False,
)
assert stage_only.seed == 17
assert sampling_utils.get_tts_local_seed(stage_only) is None
talker = SimpleNamespace(seed=None, extra_args={"preserved": True})
sampling_utils.apply_fixed_seed_to_sampling_params(
talker,
23,
seed_talker_mtp=True,
)
assert talker.seed == 23
assert talker.extra_args == {"preserved": True, "tts_local_seed": 23}
assert sampling_utils.get_tts_local_seed(talker) == 23