Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
253 lines
7.4 KiB
Python
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
|