返回 JoyAI-Echo
cache.py
1 """Bounded causal cache configuration for audio-video inference."""
2
3 from __future__ import annotations
4
5 from dataclasses import dataclass
6 from typing import TYPE_CHECKING
7
8 import torch
9
10 from .scheduling import (
11 CAUSAL_VIDEO_CHUNK_SIZE,
12 causal_audio_blocks,
13 causal_audio_frames,
14 causal_video_blocks,
15 )
16
17 if TYPE_CHECKING:
18 from .causal_wrapper import CausalModelWrapper
19
20
21 @dataclass(frozen=True)
22 class CausalCacheConfig:
23 """Video cache sizes in latent-frame units, with aligned audio sizes."""
24
25 video_local_attn_size: int = 19
26 video_sink_size: int = 7
27 video_chunk_size: int = CAUSAL_VIDEO_CHUNK_SIZE
28
29 @property
30 def audio_local_attn_size(self) -> int:
31 return causal_audio_frames(self.video_local_attn_size, self.video_chunk_size)
32
33 @property
34 def audio_sink_size(self) -> int:
35 return causal_audio_frames(self.video_sink_size, self.video_chunk_size)
36
37 def validate(self) -> None:
38 if self.video_chunk_size != CAUSAL_VIDEO_CHUNK_SIZE:
39 raise ValueError(
40 f"Echo-WM Flash requires video_chunk_size={CAUSAL_VIDEO_CHUNK_SIZE}, "
41 f"got {self.video_chunk_size}"
42 )
43 if not 0 < self.video_sink_size < self.video_local_attn_size:
44 raise ValueError("expected 0 < video_sink_size < video_local_attn_size")
45 if self.video_chunk_size > self.video_local_attn_size - self.video_sink_size:
46 raise ValueError("video_chunk_size must fit in the FIFO portion of the cache")
47 for name, size in (
48 ("video_local_attn_size", self.video_local_attn_size),
49 ("video_sink_size", self.video_sink_size),
50 ):
51 if (size - 1) % self.video_chunk_size:
52 raise ValueError(
53 f"{name} must be 1 + n * video_chunk_size for audio alignment"
54 )
55
56
57 def _position_preprocessor(value):
58 return getattr(value, "simple_preprocessor", value)
59
60
61 def _make_rope(preprocessor, positions: torch.Tensor, dtype: torch.dtype, *, inner_dim=None, max_pos=None):
62 return preprocessor._prepare_positional_embeddings(
63 positions=positions,
64 inner_dim=preprocessor.inner_dim if inner_dim is None else inner_dim,
65 max_pos=preprocessor.max_pos if max_pos is None else max_pos,
66 use_middle_indices_grid=preprocessor.use_middle_indices_grid,
67 num_attention_heads=preprocessor.num_attention_heads,
68 x_dtype=dtype,
69 )
70
71
72 def configure_bounded_caches(
73 wrapper: CausalModelWrapper,
74 caches: list[dict],
75 video_positions: torch.Tensor,
76 audio_positions: torch.Tensor,
77 action_cond: dict[str, torch.Tensor],
78 dtype: torch.dtype,
79 ) -> None:
80 """Configure bounded sink-plus-FIFO RoPE and anchor translation."""
81 cfg, ppf, model = wrapper.cache, wrapper.patches_per_frame, wrapper.model
82 video_raw = model.video_args_preprocessor
83 audio_raw = model.audio_args_preprocessor
84 video_pre = _position_preprocessor(video_raw)
85 audio_pre = _position_preprocessor(audio_raw)
86 video_tokens = cfg.video_local_attn_size * ppf
87 video_rope = _make_rope(video_pre, video_positions[:, :, :video_tokens], dtype)
88 audio_rope = _make_rope(audio_pre, audio_positions[:, :, : cfg.audio_local_attn_size], dtype)
89
90 video_cross_rope = _make_rope(
91 video_pre,
92 video_positions[:, 0:1, :video_tokens],
93 dtype,
94 inner_dim=video_raw.audio_cross_attention_dim,
95 max_pos=[video_raw.cross_pe_max_pos],
96 )
97 audio_cross_rope = _make_rope(
98 audio_pre,
99 audio_positions[:, 0:1, : cfg.audio_local_attn_size],
100 dtype,
101 inner_dim=audio_raw.audio_cross_attention_dim,
102 max_pos=[audio_raw.cross_pe_max_pos],
103 )
104
105 video_frames = video_positions.shape[2] // ppf
106 video_blocks = causal_video_blocks(video_frames, cfg.video_chunk_size)
107 audio_blocks = causal_audio_blocks(video_frames, cfg.video_chunk_size)
108 audio_to_video_slices: dict[tuple[int, int], tuple[int, int]] = {}
109 video_to_audio_slices: dict[tuple[int, int], tuple[int, int]] = {}
110 for (video_start, video_end), (audio_start, audio_end) in zip(
111 video_blocks, audio_blocks, strict=True
112 ):
113 video_query_end = min(video_end, cfg.video_local_attn_size) * ppf
114 audio_to_video_slices[(audio_start, audio_end)] = (
115 video_query_end - (video_end - video_start) * ppf,
116 video_query_end,
117 )
118 audio_query_end = min(audio_end, cfg.audio_local_attn_size)
119 video_to_audio_slices[(video_start * ppf, video_end * ppf)] = (
120 audio_query_end - (audio_end - audio_start),
121 audio_query_end,
122 )
123
124 for layer in caches:
125 layer["video_self"]["local_rope_pe"] = video_rope
126 layer["audio_self"]["local_rope_pe"] = audio_rope
127 layer["a2v"].update(
128 local_cross_q_rope_pe=video_cross_rope,
129 local_cross_k_rope_pe=audio_cross_rope,
130 local_cross_q_slices=audio_to_video_slices,
131 )
132 layer["v2a"].update(
133 local_cross_q_rope_pe=audio_cross_rope,
134 local_cross_k_rope_pe=video_cross_rope,
135 local_cross_q_slices=video_to_audio_slices,
136 )
137 ucpe = layer.get("video_ucpe")
138 if ucpe is not None:
139 ucpe.update(
140 bounded_anchor_translation=True,
141 full_ucpe_viewmats=action_cond["ucpe_viewmats"],
142 full_ucpe_Ks=action_cond["ucpe_Ks"],
143 patches_per_frame=ppf,
144 )
145
145 lines PYTHON