[Model][PersonaPlex] add per-row RingKV offsets and active rows (#7670)

Signed-off-by: Mr.Dong <1042542469@qq.com>
Co-authored-by: Mr.Dong <1042542469@qq.com>
This commit is contained in:
MrDongslsandMr.Dong authored and GitHub committed 2026-09-17 01:50:40 -07:00
1 parent cb439f3e84
commit 825ab20b07
3 files changed
+483 -56

No files matched your search

@@ -0,0 +1,369 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
import pytest
import torch
import torch.nn as nn
from pytest_mock import MockerFixture
from vllm_omni.model_executor.models.personaplex.personaplex_mimi import (
PersonaPlexMimiCodec,
_MimiStreamingTransformer,
_StreamConv1d,
_StreamConvTr1d,
)
from vllm_omni.model_executor.models.personaplex.personaplex_temporal import (
PersonaPlexTemporalStreaming,
_RingKV,
)
pytestmark = pytest.mark.core_model
SEED = 1234
CUDA_DEVICE = torch.device("cuda")
def _mask(*rows: bool, device: torch.device | str = "cpu") -> torch.Tensor:
return torch.tensor(rows, dtype=torch.bool, device=device)
def _inputs(shape: tuple[int, ...], seed: int, device: torch.device) -> torch.Tensor:
generator = torch.Generator(device="cpu").manual_seed(seed)
return torch.randn(*shape, generator=generator).to(device)
def _make_mimi_transformer(
batch_size: int,
device: torch.device,
context: int = 6,
) -> _MimiStreamingTransformer:
torch.manual_seed(SEED)
transformer = _MimiStreamingTransformer(
num_layers=2,
dim=32,
num_heads=4,
context=context,
).to(device)
for parameter in transformer.parameters():
if parameter.dtype.is_floating_point:
nn.init.normal_(parameter, std=0.1)
transformer.streaming_init(batch_size)
return transformer
def _make_temporal(
batch_size: int,
device: torch.device,
context: int = 6,
) -> PersonaPlexTemporalStreaming:
torch.manual_seed(SEED)
temporal = PersonaPlexTemporalStreaming(
dim=16,
num_layers=2,
num_heads=4,
hidden=32,
context=context,
text_card=11,
).to(device)
for parameter in temporal.parameters():
if parameter.dtype.is_floating_point:
nn.init.normal_(parameter, std=0.1)
temporal.streaming_init(batch_size)
return temporal
def _assert_valid_ring_row_matches(
batched: _RingKV,
reference: _RingKV,
batched_positions: torch.Tensor,
reference_positions: torch.Tensor,
row: int,
) -> None:
assert torch.equal(batched_positions[row], reference_positions)
assert torch.equal(batched.end_offset[row], reference.end_offset[0])
# The active mask covers the physical write as well as offset advancement,
# so the complete per-row ring state remains singleton-identical.
assert torch.equal(batched.cache[:, row], reference.cache[:, 0])
def _stream_carry(stream: _StreamConv1d | _StreamConvTr1d) -> torch.Tensor:
if isinstance(stream, _StreamConv1d):
assert stream.prev is not None
return stream.prev
assert stream.partial is not None
return stream.partial
def _make_passthrough_stage(mocker: MockerFixture):
stage = mocker.Mock(spec=["reset", "reset_slot"])
stage.side_effect = lambda x, active: x
return stage
def _make_passthrough_transformer(mocker: MockerFixture):
transformer = mocker.Mock(spec=["streaming_init", "reset_streaming", "reset_slot", "step"])
transformer.step.side_effect = lambda x, active: x
return transformer
def _active_args(call_args_list) -> torch.Tensor:
return torch.stack([call.args[1] for call in call_args_list])
def _make_codec_stub(mocker: MockerFixture) -> PersonaPlexMimiCodec:
codec = PersonaPlexMimiCodec.__new__(PersonaPlexMimiCodec)
nn.Module.__init__(codec)
quantizer = mocker.Mock(spec=["encode", "decode"])
quantizer.encode.side_effect = lambda x: torch.zeros(8, x.shape[0], x.shape[-1], dtype=torch.long)
quantizer.decode.side_effect = lambda codes: torch.zeros(codes.shape[0], 1, codes.shape[-1])
codec.device = torch.device("cpu")
codec.dtype = torch.float32
codec.model = SimpleNamespace(quantizer=quantizer)
codec._enc_stages = []
codec._dec_stages = []
codec._downsample = _make_passthrough_stage(mocker)
codec._upsample = _make_passthrough_stage(mocker)
codec.encoder_transformer = _make_passthrough_transformer(mocker)
codec.decoder_transformer = _make_passthrough_transformer(mocker)
codec.streaming_init(batch_size=2)
return codec
@pytest.mark.cpu
def test_codec_entrypoints_forward_active_mask(mocker: MockerFixture) -> None:
codec = _make_codec_stub(mocker)
active = _mask(True, False)
all_active = torch.ones_like(active)
codec.encode_frame(torch.zeros(2, 1920), active)
codec.decode_frame(torch.zeros(2, 8), active)
codec.decode_frames(torch.zeros(2, 8, 3), active)
codec.decode_frames(torch.zeros(2, 8, 3), active=None)
assert torch.equal(_active_args(codec._downsample.call_args_list), active[None])
assert torch.equal(_active_args(codec._upsample.call_args_list), torch.stack((active, active, all_active)))
assert torch.equal(_active_args(codec.encoder_transformer.step.call_args_list), active[None])
assert torch.equal(
_active_args(codec.decoder_transformer.step.call_args_list),
torch.stack((active, active, all_active)),
)
@pytest.mark.cpu
def test_ring_mixed_offsets_match_singleton_streams() -> None:
batch_size, heads, head_dim, capacity, tokens = 3, 2, 3, 6, 2
batched = _RingKV(batch_size, heads, head_dim, capacity, torch.device("cpu"), torch.float32)
singletons = [_RingKV(1, heads, head_dim, capacity, torch.device("cpu"), torch.float32) for _ in range(batch_size)]
reference_positions: list[torch.Tensor | None] = [None] * batch_size
active_schedule = [
_mask(True, True, True),
_mask(True, False, False),
_mask(False, False, True),
_mask(True, True, False),
_mask(False, True, True),
_mask(True, False, True),
_mask(True, True, True),
] * 2
torch.manual_seed(SEED)
for active in active_schedule:
keys = torch.randn(batch_size, heads, tokens, head_dim)
values = torch.randn_like(keys)
_, _, positions = batched.complete(keys, values, active)
for row, is_active in enumerate(active.tolist()):
if is_active:
_, _, singleton_positions = singletons[row].complete(
keys[row : row + 1],
values[row : row + 1],
_mask(True),
)
reference_positions[row] = singleton_positions[0].clone()
assert reference_positions[row] is not None
_assert_valid_ring_row_matches(
batched,
singletons[row],
positions,
reference_positions[row],
row,
)
assert any(int(offset) > capacity for offset in batched.end_offset)
assert not torch.equal(batched.end_offset[0], batched.end_offset[1])
@pytest.mark.cpu
def test_ring_slot_recycle_masks_old_history() -> None:
ring = _RingKV(2, 1, 1, 8, torch.device("cpu"), torch.float32)
for _ in range(4):
ring.complete(torch.randn(2, 1, 2, 1), torch.randn(2, 1, 2, 1), _mask(True, True))
old_end = ring.end_offset.clone()
ring.reset_slot(1)
_, _, positions = ring.complete(
torch.randn(2, 1, 2, 1),
torch.randn(2, 1, 2, 1),
_mask(False, True),
)
visible = positions[1] >= 0
assert torch.all(positions[1][visible] >= old_end[1])
ring.reset_slot(0)
ring.bump_slot_start(0)
_, _, positions = ring.complete(
torch.randn(2, 1, 2, 1),
torch.randn(2, 1, 2, 1),
_mask(True, False),
)
visible = positions[0] >= 0
assert torch.all(positions[0][visible] >= old_end[0] + 1)
@pytest.mark.cpu
def test_temporal_streaming_rejects_invalid_active_shape() -> None:
temporal = _make_temporal(2, torch.device("cpu"))
with pytest.raises(ValueError, match=r"active must have shape \(2,\)"):
temporal.step(torch.zeros(2, 1, 16), torch.ones(1))
@pytest.mark.cuda
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
def test_mimi_transformer_mixed_offsets_match_singletons() -> None:
batch_size, tokens, dim = 3, 2, 32
batched = _make_mimi_transformer(batch_size, CUDA_DEVICE)
singletons = [_make_mimi_transformer(1, CUDA_DEVICE) for _ in range(batch_size)]
active_schedule = [
_mask(True, True, True, device=CUDA_DEVICE),
_mask(True, False, False, device=CUDA_DEVICE),
_mask(False, False, True, device=CUDA_DEVICE),
_mask(True, True, False, device=CUDA_DEVICE),
_mask(False, True, True, device=CUDA_DEVICE),
_mask(True, False, True, device=CUDA_DEVICE),
_mask(True, True, True, device=CUDA_DEVICE),
] * 2
for step, active in enumerate(active_schedule):
inputs = _inputs((batch_size, tokens, dim), SEED + step, CUDA_DEVICE)
offsets_before = batched._offset.clone()
cache_before = [kv.cache.clone() for kv in batched._kv]
output = batched.step(inputs, active)
for row, is_active in enumerate(active.tolist()):
if is_active:
expected = singletons[row].step(
inputs[row : row + 1],
_mask(True, device=CUDA_DEVICE),
)
torch.testing.assert_close(output[row : row + 1], expected, rtol=0.0, atol=0.0)
assert torch.equal(batched._offset[row], singletons[row]._offset[0])
for batched_kv, singleton_kv in zip(batched._kv, singletons[row]._kv):
assert torch.equal(batched_kv.end_offset[row], singleton_kv.end_offset[0])
torch.testing.assert_close(batched_kv.cache[:, row], singleton_kv.cache[:, 0], rtol=0.0, atol=0.0)
else:
assert torch.equal(batched._offset[row], offsets_before[row])
for kv, previous_cache in zip(batched._kv, cache_before):
assert torch.equal(kv.end_offset[row], offsets_before[row])
assert torch.equal(kv.cache[:, row], previous_cache[:, row])
@pytest.mark.cuda
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
def test_temporal_streaming_mixed_offsets_match_isolated_rows() -> None:
# Keep the reference shape at B=2 so exact comparisons use the same CUDA
# kernel shape while the target row runs independently.
batched = _make_temporal(2, CUDA_DEVICE)
references = [_make_temporal(2, CUDA_DEVICE) for _ in range(2)]
active_schedule = [
_mask(True, True, device=CUDA_DEVICE),
_mask(True, False, device=CUDA_DEVICE),
_mask(False, False, device=CUDA_DEVICE),
_mask(False, True, device=CUDA_DEVICE),
_mask(True, True, device=CUDA_DEVICE),
_mask(True, False, device=CUDA_DEVICE),
_mask(False, True, device=CUDA_DEVICE),
_mask(True, True, device=CUDA_DEVICE),
] * 2
for step, active in enumerate(active_schedule):
inputs = _inputs((2, 1, 16), SEED + 100 + step, CUDA_DEVICE)
offsets_before = batched._offset.clone()
cache_before = [kv.cache.clone() for kv in batched._kv]
output, logits = batched.step(inputs, active)
for row, (reference, is_active) in enumerate(zip(references, active.tolist())):
if is_active:
reference_inputs = torch.zeros_like(inputs)
reference_inputs[row] = inputs[row]
reference_active = torch.zeros_like(active)
reference_active[row] = True
expected_output, expected_logits = reference.step(reference_inputs, reference_active)
torch.testing.assert_close(output[row : row + 1], expected_output[row : row + 1], rtol=0.0, atol=0.0)
torch.testing.assert_close(logits[row : row + 1], expected_logits[row : row + 1], rtol=0.0, atol=0.0)
assert torch.equal(batched._offset[row], reference._offset[row])
for batched_kv, reference_kv in zip(batched._kv, reference._kv):
assert torch.equal(batched_kv.end_offset[row], reference_kv.end_offset[row])
torch.testing.assert_close(batched_kv.cache[:, row], reference_kv.cache[:, row], rtol=0.0, atol=0.0)
else:
assert torch.equal(batched._offset[row], offsets_before[row])
for kv, previous_cache in zip(batched._kv, cache_before):
assert torch.equal(kv.end_offset[row], offsets_before[row])
assert torch.equal(kv.cache[:, row], previous_cache[:, row])
@pytest.mark.parametrize("kind", ["conv", "convtr"])
@pytest.mark.cuda
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
def test_mimi_conv_carries_preserve_inactive_rows(kind: str) -> None:
if kind == "conv":
conv = nn.Conv1d(1, 2, kernel_size=3, stride=2, device=CUDA_DEVICE)
batched = _StreamConv1d(conv, pad_mode="replicate")
row0 = _StreamConv1d(conv, pad_mode="replicate")
row1 = _StreamConv1d(conv, pad_mode="replicate")
samples = 4
else:
conv = nn.ConvTranspose1d(1, 2, kernel_size=4, stride=2, device=CUDA_DEVICE)
batched = _StreamConvTr1d(conv)
row0 = _StreamConvTr1d(conv)
row1 = _StreamConvTr1d(conv)
samples = 2
for stream, batch_size in ((batched, 2), (row0, 1), (row1, 1)):
stream.reset(batch_size, CUDA_DEVICE, torch.float32)
singletons = [row0, row1]
active_schedule = [
_mask(False, False, device=CUDA_DEVICE),
_mask(True, False, device=CUDA_DEVICE),
_mask(True, True, device=CUDA_DEVICE),
_mask(False, True, device=CUDA_DEVICE),
_mask(True, True, device=CUDA_DEVICE),
]
for step, active in enumerate(active_schedule):
inputs = _inputs((2, 1, samples), SEED + 200 + step, CUDA_DEVICE)
carry_before = _stream_carry(batched).clone()
fresh_before = batched._fresh.clone()
output = PersonaPlexMimiCodec._run_stages(inputs, [(kind, batched)], active)
for row, is_active in enumerate(active.tolist()):
if is_active:
expected = PersonaPlexMimiCodec._run_stages(
inputs[row : row + 1],
[(kind, singletons[row])],
_mask(True, device=CUDA_DEVICE),
)
torch.testing.assert_close(output[row : row + 1], expected, rtol=0.0, atol=0.0)
torch.testing.assert_close(
_stream_carry(batched)[row], _stream_carry(singletons[row])[0], rtol=0.0, atol=0.0
)
assert torch.equal(batched._fresh[row], singletons[row]._fresh[0])
else:
assert torch.equal(_stream_carry(batched)[row], carry_before[row])
assert torch.equal(batched._fresh[row], fresh_before[row])
@@ -43,6 +43,19 @@ FRAME_SIZE = 1920
CODEBOOKS = 8
def _normalize_active(active: torch.Tensor | None, all_active: torch.Tensor) -> torch.Tensor:
# Stage 0 and Code2Wav currently use one B=1 codec per session and omit
# `active`, so preserve that API by treating None as all rows active. When
# they switch to shared B>1 codecs, their batch builders must pass bool[B]:
# True advances that row's streaming state; False keeps an absent or padded
# row's offsets and convolution carries unchanged.
if active is None:
return all_active
if active.shape != all_active.shape:
raise ValueError(f"active must have shape {tuple(all_active.shape)}, got {tuple(active.shape)}")
return active.to(device=all_active.device, dtype=torch.bool)
def _map_moshi_codec_weights(
state_dict: dict[str, torch.Tensor],
) -> dict[str, torch.Tensor]:
@@ -107,23 +120,24 @@ class _StreamConv1d:
def reset(self, batch_size: int, device, dtype) -> None:
pad = self.kernel - self.stride
self.prev = torch.zeros(batch_size, self.conv.in_channels, pad, device=device, dtype=dtype)
self._fresh = torch.ones(batch_size, dtype=torch.bool)
self._fresh = torch.ones(batch_size, dtype=torch.bool, device=device)
def reset_slot(self, b: int) -> None:
self.prev[b].zero_()
self._fresh[b] = True
def __call__(self, x: torch.Tensor) -> torch.Tensor:
if self.pad_mode == "replicate" and bool(self._fresh.any()):
def __call__(self, x: torch.Tensor, active: torch.Tensor) -> torch.Tensor:
if self.pad_mode == "replicate":
pad = self.prev.shape[-1]
edge = x[..., 0:1].expand(-1, -1, pad)
fresh = self._fresh.to(x.device).view(-1, 1, 1)
fresh = (self._fresh & active).view(-1, 1, 1)
self.prev = torch.where(fresh, edge.to(self.prev.dtype), self.prev)
self._fresh[:] = False
self._fresh[active] = False
x = torch.cat([self.prev, x], dim=-1)
t = x.shape[-1]
num_frames = max(0, (t - self.kernel) // self.stride + 1)
self.prev = x[..., num_frames * self.stride :]
prev = x[..., num_frames * self.stride :]
self.prev = torch.where(active.view(-1, 1, 1), prev, self.prev)
if num_frames == 0:
return x.new_zeros(x.shape[0], self.conv.out_channels, 0)
return self.conv(x[..., : (num_frames - 1) * self.stride + self.kernel])
@@ -137,18 +151,19 @@ class _StreamConvTr1d:
self.kernel = conv.kernel_size[0]
self.stride = conv.stride[0]
self.partial: torch.Tensor | None = None
self._fresh: torch.Tensor | None = None
def reset(self, batch_size: int, device, dtype) -> None:
self.partial = torch.zeros(
batch_size, self.conv.out_channels, self.kernel - self.stride, device=device, dtype=dtype
)
self._fresh = torch.ones(batch_size, dtype=torch.bool)
self._fresh = torch.ones(batch_size, dtype=torch.bool, device=device)
def reset_slot(self, b: int) -> None:
self.partial[b].zero_()
self._fresh[b] = True
def __call__(self, x: torch.Tensor) -> torch.Tensor:
def __call__(self, x: torch.Tensor, active: torch.Tensor) -> torch.Tensor:
out = self.conv(x)
length = out.shape[-1]
tail = self.kernel - self.stride
@@ -159,10 +174,11 @@ class _StreamConvTr1d:
# it again, so subtract one copy -- except on a row's very first
# frame, where the carry is zeros by construction.
merge = merge - self.conv.bias[:, None]
merge[self._fresh] = 0.0
self._fresh[:] = False
merge[self._fresh & active] = 0.0
self._fresh[active] = False
out[..., :pt] += merge
self.partial = out[..., length - tail :].clone()
partial = out[..., length - tail :].clone()
self.partial = torch.where(active.view(-1, 1, 1), partial, self.partial)
return out[..., : length - tail]
@@ -183,16 +199,23 @@ class _MimiTransformerLayer(nn.Module):
self.scale1 = nn.Parameter(torch.empty(dim))
self.scale2 = nn.Parameter(torch.empty(dim))
def forward(self, x: torch.Tensor, kv: _RingKV, offset: torch.Tensor, context: int) -> torch.Tensor:
def forward(
self,
x: torch.Tensor,
kv: _RingKV,
offset: torch.Tensor,
context: int,
active: torch.Tensor,
) -> torch.Tensor:
B, T, _ = x.shape
h = self.norm1(x)
qkv = F.linear(h, self.in_proj_weight)
qkv = qkv.view(B, T, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2]
q, k = _apply_rope(q, k, offset)
keys, values, pos_k = kv.complete(k, v)
keys, values, pos_k = kv.complete(k, v, active=active)
pos_k = pos_k.view(pos_k.shape[0], 1, pos_k.shape[1])
pos_q = offset + torch.arange(T, device=q.device, dtype=torch.long).view(1, -1, 1)
pos_q = offset.view(-1, 1, 1) + torch.arange(T, device=q.device, dtype=torch.long).view(1, -1, 1)
delta = pos_q - pos_k
attn_bias = (pos_k >= 0) & (delta >= 0) & (delta < context)
attn = F.scaled_dot_product_attention(q, keys, values, attn_bias.unsqueeze(1), dropout_p=0.0)
@@ -218,7 +241,7 @@ class _MimiStreamingTransformer(nn.Module):
heads = self.layers[0].num_heads
hd = self.layers[0].head_dim
self._kv = [_RingKV(batch_size, heads, hd, self.context, p.device, p.dtype) for _ in self.layers]
self._offset = torch.zeros(1, device=p.device, dtype=torch.long)
self._offset = torch.zeros(batch_size, device=p.device, dtype=torch.long)
def reset_streaming(self) -> None:
for kv in self._kv:
@@ -229,11 +252,11 @@ class _MimiStreamingTransformer(nn.Module):
for kv in self._kv:
kv.reset_slot(b)
def step(self, x: torch.Tensor) -> torch.Tensor:
def step(self, x: torch.Tensor, active: torch.Tensor) -> torch.Tensor:
"""``x`` is ``[B, T, dim]`` (T = positions this frame, typically 2)."""
for layer, kv in zip(self.layers, self._kv):
x = layer(x, kv, self._offset, self.context)
self._offset.add_(x.shape[1])
x = layer(x, kv, self._offset, self.context, active)
self._offset.add_(x.shape[1] * active.to(self._offset.dtype))
return x
def load_weights(self, state_dict: dict[str, torch.Tensor], prefix: str) -> int:
@@ -339,6 +362,7 @@ class PersonaPlexMimiCodec(nn.Module):
self._upsample = _StreamConvTr1d(m.upsample.conv)
self._dec_stages = _walk_seanet(m.decoder.layers)
self._batch_size: int | None = None
self._all_active: torch.Tensor
# -- streaming state ------------------------------------------------------
@@ -354,6 +378,7 @@ class PersonaPlexMimiCodec(nn.Module):
def streaming_init(self, batch_size: int) -> None:
self._batch_size = batch_size
self._all_active = torch.ones(batch_size, dtype=torch.bool, device=self.device)
for s in self._conv_states():
s.reset(batch_size, self.device, self.dtype)
self.encoder_transformer.streaming_init(batch_size)
@@ -376,39 +401,42 @@ class PersonaPlexMimiCodec(nn.Module):
# -- per-frame codec -------------------------------------------------------
@staticmethod
def _run_stages(x: torch.Tensor, stages) -> torch.Tensor:
def _run_stages(x: torch.Tensor, stages, active: torch.Tensor) -> torch.Tensor:
for kind, stage in stages:
if kind == "res":
act0, conv1, act2, conv3 = stage
x = x + conv3(act2(conv1(act0(x))))
x = x + conv3(act2(conv1(act0(x), active)), active)
else:
x = stage(x)
x = stage(x, active) if kind in {"conv", "convtr"} else stage(x)
return x
@torch.no_grad()
def encode_frame(self, pcm: torch.Tensor) -> torch.Tensor:
def encode_frame(self, pcm: torch.Tensor, active: torch.Tensor | None = None) -> torch.Tensor:
"""``[B, frame_size]`` float PCM -> ``[B, 8]`` codes."""
x = pcm.to(self.device, self.dtype).view(-1, 1, FRAME_SIZE)
x = self._run_stages(x, self._enc_stages)
x = self.encoder_transformer.step(x.transpose(1, 2)).transpose(1, 2)
x = self._downsample(x)
active = _normalize_active(active, self._all_active)
x = self._run_stages(x, self._enc_stages, active)
x = self.encoder_transformer.step(x.transpose(1, 2), active).transpose(1, 2)
x = self._downsample(x, active)
codes = self.model.quantizer.encode(x) # [Q, B, T]
return codes[:CODEBOOKS, :, 0].transpose(0, 1).contiguous()
@torch.no_grad()
def decode_frame(self, codes: torch.Tensor) -> torch.Tensor:
def decode_frame(self, codes: torch.Tensor, active: torch.Tensor | None = None) -> torch.Tensor:
"""``[B, 8]`` codes -> ``[B, frame_size]`` float PCM."""
emb = self.model.quantizer.decode(codes.to(self.device).view(-1, CODEBOOKS, 1))
emb = self._upsample(emb)
emb = self.decoder_transformer.step(emb.transpose(1, 2)).transpose(1, 2)
x = self._run_stages(emb, self._dec_stages)
active = _normalize_active(active, self._all_active)
emb = self._upsample(emb, active)
emb = self.decoder_transformer.step(emb.transpose(1, 2), active).transpose(1, 2)
x = self._run_stages(emb, self._dec_stages, active)
return x[:, 0, :]
@torch.no_grad()
def decode_frames(self, codes: torch.Tensor) -> torch.Tensor:
def decode_frames(self, codes: torch.Tensor, active: torch.Tensor | None = None) -> torch.Tensor:
"""``[B, 8, F]`` codes -> ``[B, F * frame_size]`` float PCM."""
emb = self.model.quantizer.decode(codes.to(self.device))
emb = self._upsample(emb)
emb = self.decoder_transformer.step(emb.transpose(1, 2)).transpose(1, 2)
x = self._run_stages(emb, self._dec_stages)
active = _normalize_active(active, self._all_active)
emb = self._upsample(emb, active)
emb = self.decoder_transformer.step(emb.transpose(1, 2), active).transpose(1, 2)
x = self._run_stages(emb, self._dec_stages, active)
return x[:, 0, :]
@@ -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
"""Streaming Helium temporal transformer for PersonaPlex (moshi-free, plain torch).
Frame-clocked duplex needs a stateful per-frame forward: one 80 ms step consumes
@@ -49,8 +49,8 @@ def _apply_rope(q: torch.Tensor, k: torch.Tensor, offset: torch.Tensor, max_peri
B, H, T, D = q.shape
ds = torch.arange(D // 2, device=q.device, dtype=torch.float32)
freqs = torch.exp(ds * (-math.log(max_period) * 2 / D))
ts = offset.float() + torch.arange(T, device=q.device, dtype=torch.float32)
ts = ts.view(1, -1, 1)
ts = offset.float().view(-1, 1) + torch.arange(T, device=q.device, dtype=torch.float32)
ts = ts.view(-1, 1, T, 1)
dims = q.shape[:-1]
q = q.view(*dims, D // 2, 2)
@@ -80,7 +80,7 @@ class _RingKV:
def __init__(self, batch_size: int, num_heads: int, dim_per_head: int, capacity: int, device, dtype):
self.capacity = capacity
self.cache = torch.zeros((2, batch_size, num_heads, capacity, dim_per_head), device=device, dtype=dtype)
self.end_offset = torch.zeros(1, device=device, dtype=torch.long)
self.end_offset = torch.zeros(batch_size, device=device, dtype=torch.long)
self.start_offset = torch.zeros(batch_size, device=device, dtype=torch.long)
def reset(self) -> None:
@@ -90,23 +90,36 @@ class _RingKV:
def reset_slot(self, b: int) -> None:
# Mask everything written so far for row b; the row's next write is its
# first visible entry. (LM sacrifice-tick +1 is applied by the caller.)
self.start_offset[b] = self.end_offset.clone()
self.start_offset[b] = self.end_offset[b]
def bump_slot_start(self, b: int) -> None:
self.start_offset[b] += 1
def complete(self, k: torch.Tensor, v: torch.Tensor):
def complete(self, k: torch.Tensor, v: torch.Tensor, active: torch.Tensor):
B, H, T, D = k.shape
indexes = torch.arange(T, device=self.end_offset.device, dtype=self.end_offset.dtype) + self.end_offset
indexes = indexes % self.capacity
self.cache[0].index_copy_(2, indexes, k)
self.cache[1].index_copy_(2, indexes, v)
self.end_offset.add_(T)
indexes = (
torch.arange(T, device=self.end_offset.device, dtype=self.end_offset.dtype).view(1, -1)
+ self.end_offset.view(-1, 1)
) % self.capacity
idx4 = indexes.view(B, 1, T, 1).expand(-1, H, -1, D)
# Keep inactive rows completely inert. Once the ring is full, the
# physical future-write slot is also addressable by the position mask;
# writing it for an inactive row would therefore leak padded data into
# its next attention step.
active_view = active.view(B, 1, 1, 1)
old_k = self.cache[0].gather(2, idx4)
old_v = self.cache[1].gather(2, idx4)
k = torch.where(active_view, k, old_k)
v = torch.where(active_view, v, old_v)
self.cache[0].scatter_(2, idx4, k)
self.cache[1].scatter_(2, idx4, v)
self.end_offset.add_(T * active.to(self.end_offset.dtype))
idx = torch.arange(self.capacity, device=self.end_offset.device, dtype=torch.long)
invalid = idx >= self.end_offset
end_index = self.end_offset % self.capacity
delta = idx - end_index
end_offset = self.end_offset.view(-1, 1)
invalid = idx.view(1, -1) >= end_offset
end_index = end_offset % self.capacity
delta = idx.view(1, -1) - end_index
# `delta <= 0` (not `< 0`) is moshi's exact convention (transformer.py
# RingKVCache.complete). It labels the just-past-newest slot as the future
# write position, so once the ring has wrapped the single oldest in-window
@@ -114,14 +127,19 @@ class _RingKV:
# verbatim from the reference and only shows after the window fills (Helium
# ~3000 frames / 240 s); it costs one frame out of thousands. Do NOT change
# this to `< 0`: it would diverge from moshi and break greedy bit-parity.
positions = torch.where(delta <= 0, self.end_offset + delta, self.end_offset + delta - self.capacity)
positions = torch.where(delta <= 0, end_offset + delta, end_offset + delta - self.capacity)
positions = torch.where(invalid, torch.full_like(positions, -1), positions)
positions = positions.view(1, -1) # [1, capacity]
below = positions < self.start_offset.view(-1, 1) # [B, capacity]
positions = torch.where(below, torch.full_like(positions, -1), positions)
return self.cache[0], self.cache[1], positions
def _normalize_temporal_active(active: torch.Tensor, reference: torch.Tensor) -> torch.Tensor:
if active.shape != reference.shape:
raise ValueError(f"active must have shape {tuple(reference.shape)}, got {tuple(active.shape)}")
return active.to(device=reference.device, dtype=torch.bool)
class _TemporalLayer(nn.Module):
"""One Helium decoder layer in the checkpoint's fused layout."""
@@ -137,7 +155,14 @@ class _TemporalLayer(nn.Module):
self.norm1_alpha = nn.Parameter(torch.ones(1, 1, dim))
self.norm2_alpha = nn.Parameter(torch.ones(1, 1, dim))
def forward(self, x: torch.Tensor, kv: _RingKV, offset: torch.Tensor, context: int) -> torch.Tensor:
def forward(
self,
x: torch.Tensor,
kv: _RingKV,
offset: torch.Tensor,
context: int,
active: torch.Tensor,
) -> torch.Tensor:
B, T, _ = x.shape
h = _rms_norm_f32(x, self.norm1_alpha, 1e-8)
qkv = F.linear(h, self.in_proj_weight)
@@ -147,9 +172,9 @@ class _TemporalLayer(nn.Module):
q, k, v = qkv[0], qkv[1], qkv[2]
q, k = _apply_rope(q, k, offset)
keys, values, pos_k = kv.complete(k, v)
keys, values, pos_k = kv.complete(k, v, active)
pos_k = pos_k.view(pos_k.shape[0], 1, pos_k.shape[1]) # [B, 1, cap]
pos_q = offset + torch.arange(T, device=q.device, dtype=torch.long).view(1, -1, 1)
pos_q = offset.view(-1, 1, 1) + torch.arange(T, device=q.device, dtype=torch.long).view(1, -1, 1)
delta = pos_q - pos_k
attn_bias = (pos_k >= 0) & (delta >= 0) & (delta < context)
attn_bias = attn_bias.unsqueeze(1) # [B, 1, T, cap]
@@ -199,7 +224,7 @@ class PersonaPlexTemporalStreaming(nn.Module):
head_dim = self.layers[0].head_dim
# Capacity = context window (the mask truncates at `context` anyway).
self._kv = [_RingKV(batch_size, heads, head_dim, self.context, p.device, p.dtype) for _ in self.layers]
self._offset = torch.zeros(1, device=p.device, dtype=torch.long)
self._offset = torch.zeros(batch_size, device=p.device, dtype=torch.long)
def reset_streaming(self) -> None:
assert self._kv is not None, "call streaming_init first"
@@ -221,12 +246,17 @@ class PersonaPlexTemporalStreaming(nn.Module):
# -- per-frame step -------------------------------------------------------
@torch.no_grad()
def step(self, frame_embedding: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
def step(
self,
frame_embedding: torch.Tensor,
active: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
assert self._kv is not None, "call streaming_init first"
active = _normalize_temporal_active(active, self._offset)
x = frame_embedding
for layer, kv in zip(self.layers, self._kv):
x = layer(x, kv, self._offset, self.context)
self._offset.add_(x.shape[1])
x = layer(x, kv, self._offset, self.context, active)
self._offset.add_(x.shape[1] * active.to(self._offset.dtype))
out = _rms_norm_f32(x, self.out_norm_alpha, 1e-8)
text_logits = F.linear(out, self.text_linear)
return out, text_logits[:, None]