Skip to content

Commit 7dafddb

Browse files
committed
[perf]: cache TimestepEmbedder sinusoidal frequency table per device
TimestepEmbedder rebuilt the sinusoidal frequency table on every forward via a CPU `arange` + `exp` followed by an H2D copy. The table depends only on (frequency_embedding_size, max_period, freq_dtype) — all instance- constant — and the compute device, so it is identical across every denoising step. Cache it on the module per device; only the timestep- dependent product (`args = t * freqs`) runs each call. `timestep_embedding` gains an optional precomputed `freqs` argument so the free function's behavior is unchanged for any direct caller. Same cache-on- self pattern as the Wan rotary precompute in this branch and matrixgame2 PR hao-ai-lab#1415. TimestepEmbedder is shared across the DiT stack (Wan, HunyuanVideo/HV15, hunyuangamecraft, longcat, matrixgame2/3, hyworld), so every model that uses it drops the per-forward recompute + H2D. Bit-exact — the cached tensor is the same value the prior path recomputed each call.
1 parent dba71f2 commit 7dafddb

1 file changed

Lines changed: 30 additions & 5 deletions

File tree

fastvideo/layers/visual_embedding.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,26 @@ def __init__(
125125
self.freq_dtype = freq_dtype
126126

127127
def forward(self, t: torch.Tensor, timestep_seq_len: int | None = None) -> torch.Tensor:
128+
# The sinusoidal frequency table depends only on (frequency_embedding_size,
129+
# max_period, freq_dtype) — all instance-constant — and the compute device.
130+
# The original code rebuilt it on CPU (arange + exp) and copied it H2D on
131+
# every forward (once per denoising step). Cache it per device on the module
132+
# so the recompute + H2D happens once; only the timestep-dependent product
133+
# (args = t * freqs) runs each call. Same cache-on-self pattern as the Wan
134+
# rotary precompute and matrixgame2 PR #1415. Bit-exact: the cached tensor is
135+
# the same value the prior path recomputed each call.
136+
freqs_cache = getattr(self, "_freqs_cache", None)
137+
if freqs_cache is None:
138+
freqs_cache = {}
139+
self._freqs_cache = freqs_cache
140+
freqs = freqs_cache.get(t.device)
141+
if freqs is None:
142+
half = self.frequency_embedding_size // 2
143+
freqs = torch.exp(-math.log(self.max_period) *
144+
torch.arange(start=0, end=half, dtype=self.freq_dtype) / half).to(device=t.device)
145+
freqs_cache[t.device] = freqs
128146
t_freq = timestep_embedding(t, self.frequency_embedding_size, self.max_period,
129-
dtype=self.freq_dtype).to(self.mlp.fc_in.weight.dtype)
147+
dtype=self.freq_dtype, freqs=freqs).to(self.mlp.fc_in.weight.dtype)
130148
if timestep_seq_len is not None:
131149
t_freq = t_freq.unflatten(0, (1, timestep_seq_len))
132150
# t_freq = t_freq.to(self.mlp.fc_in.weight.dtype)
@@ -137,20 +155,27 @@ def forward(self, t: torch.Tensor, timestep_seq_len: int | None = None) -> torch
137155
def timestep_embedding(t: torch.Tensor,
138156
dim: int,
139157
max_period: int = 10000,
140-
dtype: torch.dtype = torch.float32) -> torch.Tensor:
158+
dtype: torch.dtype = torch.float32,
159+
freqs: torch.Tensor | None = None) -> torch.Tensor:
141160
"""
142161
Create sinusoidal timestep embeddings.
143-
162+
144163
Args:
145164
t: Tensor of shape [B] with timesteps
146165
dim: Embedding dimension
147166
max_period: Controls the minimum frequency of the embeddings
148-
167+
freqs: Optional precomputed frequency table of shape [dim // 2]. When
168+
provided, the per-call CPU compute + H2D copy is skipped (the caller
169+
is responsible for caching it on the correct device). When None, the
170+
table is built as before — preserving the original behavior for any
171+
direct caller.
172+
149173
Returns:
150174
Tensor of shape [B, dim] with embeddings
151175
"""
152176
half = dim // 2
153-
freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=dtype) / half).to(device=t.device)
177+
if freqs is None:
178+
freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=dtype) / half).to(device=t.device)
154179
args = t[:, None].float() * freqs[None]
155180
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
156181
if dim % 2:

0 commit comments

Comments
 (0)