Files
2026-10-07 01:30:11 -07:00

253 lines
7.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""CPU-only tests for modular training performance accounting."""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
import pytest
import torch
from fastvideo.forward_context import set_forward_context
from fastvideo.train.utils.performance import (
TrainingPerformanceMonitor,
_blockwise_causal_frame_pairs,
_teacher_forcing_frame_pairs,
estimate_transformer_forward,
infer_peak_bf16_tflops,
)
def _arch() -> SimpleNamespace:
return SimpleNamespace(
hidden_size=8,
ffn_dim=16,
num_layers=2,
patch_size=(1, 2, 2),
)
class _FakeTransformer(torch.nn.Module):
def __init__(self, *, causal: bool = False) -> None:
super().__init__()
self.config = SimpleNamespace(arch_config=_arch())
self.weight = torch.nn.Parameter(torch.ones(()))
if causal:
self.num_frame_per_block = 2
self.local_attn_size = 3
self.text_len = 4
def forward(
self,
*,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
**kwargs: Any,
) -> torch.Tensor:
del encoder_hidden_states, kwargs
return hidden_states * self.weight
class _UnsupportedTransformer(torch.nn.Module):
def forward(self, *, hidden_states: torch.Tensor) -> torch.Tensor:
return hidden_states
def _kwargs(*, frames: int = 4) -> dict[str, torch.Tensor]:
return {
"hidden_states": torch.ones(2, 3, frames, 4, 4),
"encoder_hidden_states": torch.ones(2, 3, 8),
}
def test_dense_forward_flops_and_backward_factor() -> None:
module = _FakeTransformer()
output = torch.ones(2, 3, 4, 4, 4, requires_grad=True)
work = estimate_transformer_forward(
module,
_kwargs(),
output,
role="student",
attention_metadata=None,
)
assert work is not None
# B=2, L=16, C=3, d=8, ffn=16, layers=2.
expected_per_layer = (
8 * 2 * 16 * 8**2
+ 4 * 2 * 16 * 8 * 16
+ 4 * 2 * 16 * 8**2
+ 4 * 2 * 3 * 8**2
+ 4 * 2 * 16 * 3 * 8
+ 4 * 2 * 16**2 * 8
)
assert work.forward_flops == expected_per_layer * 2
assert work.useful_flops == work.forward_flops * 3
assert work.query_frames == 8
assert work.query_tokens == 32
assert work.attention_pairs == 2 * 16**2
def test_causal_chunk_and_teacher_forcing_pair_counts() -> None:
assert _blockwise_causal_frame_pairs(5, 2, -1) == 17
assert _blockwise_causal_frame_pairs(5, 2, 3) == 13
assert _teacher_forcing_frame_pairs(4, 2) == 24
def test_streaming_causal_forward_uses_cache_window() -> None:
module = _FakeTransformer(causal=True)
kwargs = _kwargs(frames=2)
# Four spatial tokens/frame, starting after two frames. The three-frame
# local window gives 8 query tokens x 12 key tokens.
kwargs.update({"kv_cache": [{}], "current_start": 8})
work = estimate_transformer_forward(
module,
kwargs,
torch.ones(2, 3, 2, 4, 4),
role="student",
attention_metadata=None,
)
assert work is not None
assert work.causal_chunks == 1
assert work.attention_pairs == 2 * 8 * 12
assert work.dense_attention_pairs == 2 * 8 * 16
def test_vsa_uses_kernel_topk_density_and_overhead() -> None:
module = _FakeTransformer()
metadata = SimpleNamespace(
variable_block_sizes=torch.tensor([4, 4, 4, 4]),
VSA_sparsity=0.75,
)
work = estimate_transformer_forward(
module,
_kwargs(),
torch.ones(2, 3, 4, 4, 4),
role="student",
attention_metadata=metadata,
)
assert work is not None
assert work.attention_pairs == pytest.approx(2 * 16**2 * 0.25)
dense_work = estimate_transformer_forward(
module,
_kwargs(),
torch.ones(2, 3, 4, 4, 4),
role="student",
attention_metadata=None,
)
assert dense_work is not None
# VSA includes its gate projection and pooled dense attention overhead.
assert work.forward_flops > dense_work.forward_flops - (
dense_work.attention_pairs - work.attention_pairs) * 4 * 8 * 2
def test_streaming_cross_attention_cache_skips_kv_projections() -> None:
module = _FakeTransformer(causal=True)
kwargs = _kwargs(frames=2)
kwargs.update({"kv_cache": [{}], "current_start": 8})
output = torch.ones(2, 3, 2, 4, 4)
uncached = estimate_transformer_forward(
module,
kwargs,
output,
role="student",
attention_metadata=None,
)
cached = estimate_transformer_forward(
module,
kwargs,
output,
role="student",
attention_metadata=None,
cross_attention_cached=True,
)
assert uncached is not None and cached is not None
expected_saved = 4 * 2 * 4 * 8**2 * 2
assert uncached.forward_flops - cached.forward_flops == expected_saved
def test_monitor_counts_multiple_rollouts_and_roles_without_double_counting_accumulation() -> None:
models = {
role: SimpleNamespace(transformer=_FakeTransformer())
for role in ("student", "teacher", "critic")
}
monitor = TrainingPerformanceMonitor()
monitor.attach(models)
monitor.reset()
with set_forward_context(current_timestep=0, attn_metadata=None):
models["student"].transformer(**_kwargs())
models["student"].transformer(**_kwargs())
models["critic"].transformer(**_kwargs())
with torch.no_grad():
models["teacher"].transformer(**_kwargs())
models["teacher"].transformer(**_kwargs())
metrics = monitor.metrics(
step_time_sec=2.0,
local_batch_size=2,
grad_accum=4,
world_size=8,
sp_size=4,
peak_tflops_per_gpu=100.0,
)
monitor.close()
assert metrics["perf/model_forward_calls"] == 5
assert metrics["perf/causal_chunks"] == 0
assert metrics["perf/role/student/forward_calls"] == 2
assert metrics["perf/role/teacher/forward_calls"] == 2
assert metrics["perf/role/teacher/backward_forwards"] == 0
assert metrics["perf/role/critic/backward_forwards"] == 1
# 2 local samples x DP=2 x grad-accum=4 / 2 seconds.
assert metrics["perf/samples_per_sec"] == 8
assert metrics["perf/estimated_mfu"] > 0
def test_monitor_counts_forward_when_flop_estimator_does_not_support_model() -> None:
monitor = TrainingPerformanceMonitor()
model = SimpleNamespace(transformer=_UnsupportedTransformer())
monitor.attach({"student": model})
monitor.reset()
model.transformer(hidden_states=torch.ones(1))
metrics = monitor.metrics(
step_time_sec=1.0,
local_batch_size=1,
grad_accum=1,
world_size=1,
sp_size=1,
peak_tflops_per_gpu=None,
)
monitor.close()
assert metrics["perf/model_forward_calls"] == 1
assert metrics["perf/role/student/forward_calls"] == 1
assert "perf/estimated_tflops_per_gpu" not in metrics
@pytest.mark.parametrize(
("device_name", "expected"),
[
("NVIDIA A40", 149.7),
("NVIDIA L40S", 362.05),
("NVIDIA H100 80GB HBM3", 989.5),
("NVIDIA H200", 989.5),
("NVIDIA B200", 2250.0),
("NVIDIA GB200", 2500.0),
("Some Future GPU", None),
],
)
def test_infer_peak_bf16_tflops(device_name: str, expected: float | None) -> None:
assert infer_peak_bf16_tflops(device_name) == expected