fix: cosyvoice3 batch>1 inference (#3910)

Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
This commit is contained in:
Yuekai Zhang authored and GitHub committed 2026-06-04 08:58:53 +00:00
1 parent a57b8f858b
commit a1cc583665
1 file changed
+61 -5
@@ -643,14 +643,70 @@ class CosyVoice3Model(
) -> torch.Tensor:
if self.model_stage == "cosyvoice3_talker":
if is_multimodal is not None and any(is_multimodal):
# Per-request rearrange for the talker prompt layout
# [SOS_ph, TASK_ID_ph, AUDIO_ph * pstoken_len, ...text_tokens...]
# → [SOS_emb, ...text_embs..., TASK_ID_emb, AUDIO_embs].
#
# In batched prefill ``input_ids`` is the flat concatenation of
# N sequences and ``multimodal_embeddings`` is a list with one
# tensor per multimodal request. Each request's audio
# placeholders form one contiguous ``True`` group in
# ``is_multimodal``; the 2 positions immediately before each
# group are the SOS / TASK_ID placeholders. Walking the groups
# lets us locate every request's segment without needing a
# ``seq_token_counts`` kwarg (which only the generation runner
# injects, not the AR runner used here).
embed_tokens = self.model.llm.model.embed_tokens(input_ids)
sos = self.model.speech_embedding.weight[self.model.sos].reshape(1, -1)
task_id = self.model.speech_embedding.weight[self.model.task_id].reshape(1, -1)
prompt_speech_token_emb = multimodal_embeddings[0]
pstoken_len = prompt_speech_token_emb.shape[0] # Get length from tensor shape
embed_tokens = torch.cat(
[sos, embed_tokens[2 + pstoken_len :], task_id, prompt_speech_token_emb], dim=0
)
is_mm = is_multimodal.to(torch.bool).reshape(-1)
# vLLM v1's multimodal placeholder range covers the FULL
# ``[SOS_ph, TASK_ID_ph, AUDIO_ph * pstoken_len]`` block
# (length ``2 + pstoken_len``, see ``_get_prompt_updates``'s
# ``insertion_end`` which inserts ``[1] * (1 + 1 + token_len)``
# and ``mm_position.length`` matches that). So each contiguous
# ``True`` group in ``is_mm`` starts at a request's SOS
# placeholder, NOT at its first audio placeholder.
prev_false = torch.cat([torch.ones(1, dtype=torch.bool, device=is_mm.device), ~is_mm[:-1]])
group_starts = (is_mm & prev_false).nonzero(as_tuple=True)[0].tolist()
if len(group_starts) != len(multimodal_embeddings):
raise RuntimeError(
f"cosyvoice3 talker: found {len(group_starts)} placeholder "
f"blocks in is_multimodal but {len(multimodal_embeddings)} "
f"multimodal_embeddings tensors — these must match 1:1."
)
total_tokens = int(embed_tokens.shape[0])
# vLLM v1 packs cached (decode) requests first, then new
# (prefill) requests. The decode tokens at positions
# ``[0:group_starts[0]]`` get a speech_embedding lookup (the
# talker is generating codec tokens here, so input ids are
# codec ids, not text ids). Positions starting at
# ``group_starts[0]`` are the first new prefill request's
# SOS placeholder.
segments: list[torch.Tensor] = []
first_prefill = group_starts[0]
if first_prefill > 0:
decode_ids = input_ids[:first_prefill].to(dtype=torch.long)
segments.append(self.model.speech_embedding.weight[decode_ids])
for i, req_start in enumerate(group_starts):
pstoken_len_i = int(multimodal_embeddings[i].shape[0])
# Real text starts after the 2 + pstoken_len_i leading
# placeholders of this request's prompt.
text_start = req_start + 2 + pstoken_len_i
# Next request's segment starts at its own SOS placeholder,
# which is exactly the next True group's first index.
if i + 1 < len(group_starts):
req_end = group_starts[i + 1]
else:
req_end = total_tokens
segments.append(sos)
segments.append(embed_tokens[text_start:req_end])
segments.append(task_id)
segments.append(multimodal_embeddings[i])
embed_tokens = torch.cat(segments, dim=0)
else:
embed_tokens = self.model.speech_embedding.weight[input_ids]
return embed_tokens