222 lines
7.4 KiB
Python
222 lines
7.4 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Zero-overhead-when-off activation trace mode for FastVideo pipelines.
|
|
|
|
Enable by setting FASTVIDEO_TRACE_ACTIVATIONS=1. When off, this module
|
|
adds zero overhead — no hooks are registered, no branches exist in the
|
|
production forward path. When on, registers forward hooks on modules
|
|
whose name matches FASTVIDEO_TRACE_LAYERS, computes the requested stats
|
|
(FASTVIDEO_TRACE_STATS) on each output tensor, and writes JSONL records
|
|
to FASTVIDEO_TRACE_OUTPUT.
|
|
|
|
Useful for parity debugging across model ports — log on both the
|
|
FastVideo path and the upstream reference, diff the two JSONL files
|
|
to find the first divergent layer.
|
|
|
|
Example:
|
|
|
|
FASTVIDEO_TRACE_ACTIVATIONS=1 \
|
|
FASTVIDEO_TRACE_LAYERS="^block\\.layers\\.[0-9]+$" \
|
|
FASTVIDEO_TRACE_STATS="abs_mean,max,shape" \
|
|
FASTVIDEO_TRACE_OUTPUT="/tmp/fv_trace.jsonl" \
|
|
python examples/inference/basic/basic_magi_human.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import threading
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from collections.abc import Callable, Iterator
|
|
|
|
import torch
|
|
from torch import nn
|
|
|
|
from fastvideo import envs
|
|
from fastvideo.hooks.hooks import ForwardHook, ModuleHookManager
|
|
from fastvideo.logger import init_logger
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
_TRACE_STATE = threading.local()
|
|
|
|
|
|
def current_step_idx() -> int | None:
|
|
return getattr(_TRACE_STATE, "step_idx", None)
|
|
|
|
|
|
@contextmanager
|
|
def trace_step(step_idx: int) -> Iterator[None]:
|
|
"""Context manager that sets the current denoise step for trace records."""
|
|
prev = getattr(_TRACE_STATE, "step_idx", None)
|
|
_TRACE_STATE.step_idx = step_idx
|
|
try:
|
|
yield
|
|
finally:
|
|
_TRACE_STATE.step_idx = prev
|
|
|
|
|
|
_STAT_FNS: dict[str, Callable[[torch.Tensor], Any]] = {
|
|
"abs_mean": lambda t: float(t.detach().float().abs().mean().item()),
|
|
"sum": lambda t: float(t.detach().float().sum().item()),
|
|
"min": lambda t: float(t.detach().float().min().item()),
|
|
"max": lambda t: float(t.detach().float().max().item()),
|
|
"mean": lambda t: float(t.detach().float().mean().item()),
|
|
"std": lambda t: float(t.detach().float().std().item()),
|
|
"shape": lambda t: list(t.shape),
|
|
"dtype": lambda t: str(t.dtype),
|
|
}
|
|
|
|
|
|
def _resolve_stats(spec: str) -> list[tuple[str, Callable[[torch.Tensor], Any]]]:
|
|
stats = []
|
|
for name in [s.strip() for s in spec.split(",") if s.strip()]:
|
|
stat_fn = _STAT_FNS.get(name)
|
|
if stat_fn is None:
|
|
logger.warning(
|
|
"FASTVIDEO_TRACE_STATS contains unknown stat %r; valid: %s",
|
|
name,
|
|
sorted(_STAT_FNS),
|
|
)
|
|
continue
|
|
stats.append((name, stat_fn))
|
|
return stats
|
|
|
|
|
|
def _resolve_output_path(template: str) -> Path:
|
|
return Path(template.replace("<pid>", str(os.getpid())))
|
|
|
|
|
|
def _parse_step_filter(spec: str) -> set[int] | None:
|
|
if not spec.strip():
|
|
return None
|
|
return {int(s.strip()) for s in spec.split(",") if s.strip()}
|
|
|
|
|
|
class JsonlSink:
|
|
"""Buffered JSONL writer with thread-safe append."""
|
|
|
|
def __init__(self, path: Path) -> None:
|
|
self.path = path
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._fh = open(self.path, "a", buffering=1) # noqa: SIM115
|
|
self._lock = threading.Lock()
|
|
logger.info("Activation trace JSONL sink: %s", self.path)
|
|
|
|
def write(self, record: dict[str, Any]) -> None:
|
|
line = json.dumps(record, default=str) + "\n"
|
|
with self._lock:
|
|
self._fh.write(line)
|
|
|
|
def close(self) -> None:
|
|
with self._lock:
|
|
if not self._fh.closed:
|
|
self._fh.close()
|
|
|
|
|
|
class ActivationStatHook(ForwardHook):
|
|
"""Forward hook that emits per-tensor stats to a JSONL sink."""
|
|
|
|
def __init__(
|
|
self,
|
|
module_name: str,
|
|
stats: list[tuple[str, Callable[[torch.Tensor], Any]]],
|
|
sink: JsonlSink,
|
|
step_filter: set[int] | None,
|
|
) -> None:
|
|
self.module_name = module_name
|
|
self.stats = stats
|
|
self.sink = sink
|
|
self.step_filter = step_filter
|
|
|
|
def name(self) -> str:
|
|
return "ActivationStatHook"
|
|
|
|
def post_forward(self, module: nn.Module, output: Any) -> Any:
|
|
step_idx = current_step_idx()
|
|
if self.step_filter is not None and step_idx not in self.step_filter:
|
|
return output
|
|
for tensor_label, tensor in _flatten_tensors(output):
|
|
record: dict[str, Any] = {
|
|
"module": self.module_name,
|
|
"tensor": tensor_label,
|
|
"step": step_idx,
|
|
}
|
|
for stat_name, stat_fn in self.stats:
|
|
try:
|
|
record[stat_name] = stat_fn(tensor)
|
|
except Exception as exc: # pragma: no cover - defensive logging
|
|
record[stat_name] = f"<error: {exc!r}>"
|
|
self.sink.write(record)
|
|
return output
|
|
|
|
|
|
def _flatten_tensors(obj: Any, prefix: str = "out") -> list[tuple[str, torch.Tensor]]:
|
|
"""Yield (label, tensor) pairs from arbitrarily-nested forward outputs."""
|
|
if isinstance(obj, torch.Tensor):
|
|
return [(prefix, obj)]
|
|
if isinstance(obj, tuple | list):
|
|
out = []
|
|
for idx, item in enumerate(obj):
|
|
out.extend(_flatten_tensors(item, f"{prefix}[{idx}]"))
|
|
return out
|
|
if isinstance(obj, dict):
|
|
out = []
|
|
for key, value in obj.items():
|
|
out.extend(_flatten_tensors(value, f"{prefix}.{key}"))
|
|
return out
|
|
return []
|
|
|
|
|
|
class ActivationTraceManager:
|
|
|
|
def __init__(self, managers: list[ModuleHookManager], sink: JsonlSink) -> None:
|
|
self.managers = managers
|
|
self.sink = sink
|
|
|
|
def remove_from_manager(self) -> None:
|
|
for manager in self.managers:
|
|
if manager.get_forward_hook("ActivationStatHook") is not None:
|
|
manager.remove_forward_hook("ActivationStatHook")
|
|
if not manager.forward_hooks:
|
|
ModuleHookManager.remove_from_manager(manager.module)
|
|
self.sink.close()
|
|
|
|
|
|
def attach_activation_trace(model: nn.Module | None) -> ActivationTraceManager | None:
|
|
"""Attach activation-stat hooks to model. Returns None if trace is off."""
|
|
if not envs.FASTVIDEO_TRACE_ACTIVATIONS or model is None:
|
|
return None
|
|
|
|
pattern_spec = envs.FASTVIDEO_TRACE_LAYERS
|
|
pattern = re.compile(pattern_spec) if pattern_spec else re.compile(".*")
|
|
stats = _resolve_stats(envs.FASTVIDEO_TRACE_STATS)
|
|
if not stats:
|
|
logger.warning("FASTVIDEO_TRACE_STATS yielded no valid stats; trace disabled.")
|
|
return None
|
|
|
|
sink = JsonlSink(_resolve_output_path(envs.FASTVIDEO_TRACE_OUTPUT))
|
|
step_filter = _parse_step_filter(envs.FASTVIDEO_TRACE_STEPS)
|
|
managers = []
|
|
for name, module in model.named_modules():
|
|
if not name or not pattern.search(name):
|
|
continue
|
|
manager = ModuleHookManager.get_from_or_default(module)
|
|
manager.append_forward_hook(ActivationStatHook(name, stats, sink, step_filter))
|
|
managers.append(manager)
|
|
|
|
logger.info(
|
|
"Activation trace attached to %d modules (pattern=%r, stats=%s)",
|
|
len(managers),
|
|
pattern_spec,
|
|
[stat_name for stat_name, _ in stats],
|
|
)
|
|
return ActivationTraceManager(managers, sink)
|
|
|
|
|
|
def detach_activation_trace(mgr: ActivationTraceManager | None) -> None:
|
|
if mgr is not None:
|
|
mgr.remove_from_manager()
|