[Bugfix][MiniCPM-o] Wire the duplex sampling hook into the NPU AR model runner (#5895)
Signed-off-by: psv666 <2693925048@qq.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a12ac52ee
commit
009b80d686
4 files changed
+255
-38
No files matched your search
@@ -4,7 +4,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import torch
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -103,4 +108,69 @@ class DuplexSamplingHelper:
|
||||
self.hook_active = False
|
||||
|
||||
|
||||
__all__ = ["DuplexSamplingHelper", "DuplexSamplingRow"]
|
||||
class DuplexSamplingRunnerMixin:
|
||||
"""Runner-side wiring for the experimental duplex sampling hook.
|
||||
|
||||
The GPU and NPU AR runners are siblings, not parent and child: ``GPUARModelRunner``
|
||||
derives from ``OmniGPUModelRunner`` while ``NPUARModelRunner`` derives from
|
||||
``OmniNPUModelRunner``, so neither inherits the other's ``_sample``. Keeping the
|
||||
wiring here means a runner opts in by adding this mixin and calling the four
|
||||
methods below, instead of re-deriving the hook logic and silently drifting.
|
||||
|
||||
The drift this exists to prevent already happened once: the NPU ``_sample`` was a
|
||||
line-for-line copy of the GPU one minus the ``prepare_duplex_sampling`` call, which
|
||||
left ``_minicpmo45_duplex_row_sessions`` empty on Ascend. With no row -> session
|
||||
map the MiniCPM-o 4.5 thinker could not tell that a turn was still in progress, so
|
||||
every mid-turn ``<|listen|>`` was emitted verbatim instead of being rewritten to
|
||||
``<|tts_bos|>`` and the model never spoke again after the first turn.
|
||||
"""
|
||||
|
||||
def _init_duplex_sampling_state(self) -> None:
|
||||
"""Reset hook resolution. Call from ``__init__``, before ``load_model``."""
|
||||
self._duplex_sampling_hook = None
|
||||
self._duplex_sampling_hook_resolved = False
|
||||
|
||||
def _resolve_duplex_sampling_hook(self, *, force: bool = False):
|
||||
"""Bind ``model.prepare_duplex_sampling``. Call with ``force`` after load."""
|
||||
if not force and getattr(self, "_duplex_sampling_hook_resolved", False):
|
||||
return self._duplex_sampling_hook
|
||||
candidate = getattr(getattr(self, "model", None), "prepare_duplex_sampling", None)
|
||||
self._duplex_sampling_hook = candidate if callable(candidate) else None
|
||||
self._duplex_sampling_hook_resolved = True
|
||||
if self._duplex_sampling_hook is not None and not hasattr(self, "_duplex_sampling_helper"):
|
||||
self._duplex_sampling_helper = DuplexSamplingHelper()
|
||||
return self._duplex_sampling_hook
|
||||
|
||||
def _update_duplex_sampling_states(self, scheduler_output: SchedulerOutput) -> None:
|
||||
"""Refresh which batch rows are duplex rows. Call from ``_update_states``."""
|
||||
if self._resolve_duplex_sampling_hook() is None:
|
||||
return
|
||||
helper = getattr(self, "_duplex_sampling_helper", None)
|
||||
if helper is not None:
|
||||
helper.update_states(self, scheduler_output)
|
||||
|
||||
def _apply_duplex_sampling(self, logits: torch.Tensor, prepared_sampling_metadata: SamplingMetadata) -> None:
|
||||
"""Hand the model its duplex rows. Call from ``_sample``, before the sampler.
|
||||
|
||||
``hook_active`` makes the call fire once more after the last duplex row leaves
|
||||
the batch, so the model can drop the stale row -> session mapping without the
|
||||
helper having to scan request state on every non-duplex step.
|
||||
"""
|
||||
prepare_duplex_sampling = self._resolve_duplex_sampling_hook()
|
||||
if prepare_duplex_sampling is None:
|
||||
return
|
||||
helper = getattr(self, "_duplex_sampling_helper", None)
|
||||
rows = helper.rows(self) if helper is not None and helper.active_request_ids else ()
|
||||
if rows or (helper is not None and helper.hook_active):
|
||||
prepare_duplex_sampling(logits, prepared_sampling_metadata, rows)
|
||||
if helper is not None:
|
||||
helper.hook_active = bool(rows)
|
||||
|
||||
def _clear_duplex_sampling(self) -> None:
|
||||
"""Drop tracked rows. Call from teardown paths that release device memory."""
|
||||
helper = getattr(self, "_duplex_sampling_helper", None)
|
||||
if helper is not None:
|
||||
helper.clear()
|
||||
|
||||
|
||||
__all__ = ["DuplexSamplingHelper", "DuplexSamplingRow", "DuplexSamplingRunnerMixin"]
|
||||
Reference in new issue
Block a user