[Bugfix][VoxCPM2]: Fix vectorized_gather OOB under concurrent prefill+decode batches (#2903)

Signed-off-by: Sy03 <1370724210@qq.com>
This commit is contained in:
Sy03 authored and GitHub committed 2026-04-19 20:39:14 +12:00
1 parent a3903810fe
commit 26edc7f2fb
4 files changed
+177 -11

No files matched your search

@@ -41,6 +41,11 @@ logger = init_logger(__name__)
_ENABLE_PROFILING = os.environ.get("VOXCPM2_PROFILE", "0") == "1"
# Lower bound for the _active_states leak-warn threshold. The effective
# threshold is max(_ACTIVE_STATE_LEAK_WARN_MIN, 4 * max_batch_size) so small
# deployments still get a usable floor instead of a tiny noisy one.
_ACTIVE_STATE_LEAK_WARN_MIN = 512
def is_cjk_char(c: str) -> bool:
"""Check if a character is a CJK ideograph."""
@@ -440,6 +445,9 @@ class VoxCPM2TalkerForConditionalGeneration(nn.Module):
self._results_queue: list[tuple[str, torch.Tensor | None]] = []
self._audio_queue: list[tuple[str, Any]] = []
self._deferred_cleanup_ids: set[str] = set()
self._active_state_warn_threshold = max(_ACTIVE_STATE_LEAK_WARN_MIN, 4 * self._max_batch_size)
# one-shot by design: fires at most once per process to avoid log spam.
self._active_state_warned = False
@property
def tts(self) -> nn.Module:
@@ -449,9 +457,20 @@ class VoxCPM2TalkerForConditionalGeneration(nn.Module):
# -------------------- request state management --------------------
def _get_or_create_state(self, request_id: str) -> _RequestState:
if request_id not in self._active_states:
self._active_states[request_id] = _RequestState(request_id=request_id)
return self._active_states[request_id]
state = self._active_states.get(request_id)
if state is None:
state = _RequestState(request_id=request_id)
self._active_states[request_id] = state
if len(self._active_states) > self._active_state_warn_threshold and not self._active_state_warned:
logger.warning(
"VoxCPM2: _active_states size=%d exceeds threshold %d "
"(max_batch_size=%d); possible cleanup path leak",
len(self._active_states),
self._active_state_warn_threshold,
self._max_batch_size,
)
self._active_state_warned = True
return state
def _switch_to_request(self, request_id: str) -> _RequestState:
if request_id != self._current_request_id:
@@ -1087,14 +1106,10 @@ class VoxCPM2TalkerForConditionalGeneration(nn.Module):
is_prefill = span_len > 1
if is_prefill:
# Evict stale states
pending_ids = {rid for rid, *_ in self._pending_requests}
pending_ids.add(req_id)
if self._current_request_id:
pending_ids.add(self._current_request_id)
for rid in [r for r, s in self._active_states.items() if r not in pending_ids and s.prefill_completed]:
self._cleanup_request(rid)
# Do not evict state here: _pending_requests is a per-step prefix,
# not the full batch. Cleanup is driven by on_requests_finished ->
# _flush_deferred_cleanup (fed by vLLM scheduler._free_request via
# gpu_ar_model_runner.py).
real = info_dict.get("text_token_ids")
token_ids = input_ids.tolist() if real is None else real[0]
# Fail-fast: unsplit multichar Chinese IDs in input_ids means the