Merge remote-tracking branch 'origin/main' into feat/kaiqin/mg_solaris
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# Agent Infrastructure — Status Dashboard
|
||||
|
||||
Developer-maintained overview of all agent components and their maturity.
|
||||
Use this to understand what exists, how complete it is, and how much to trust it.
|
||||
|
||||
_Last synced: 2026-03-02_
|
||||
|
||||
> To resync this dashboard, use the workflow: `.agents/workflows/sync-dashboard.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Total | ✅ Ready | 🟡 Draft | 🔴 Stub | Trust |
|
||||
|----------|-------|---------|---------|---------|-------|
|
||||
| Skills | 8 | 0 | 8 | 0 | Low — newly created, untested |
|
||||
| Workflows (SOPs) | 4 | 0 | 4 | 0 | Low — newly created, untested |
|
||||
| Memory files | 4 | 1 | 3 | 0 | Medium — codebase_map is solid |
|
||||
| Lessons | 0 | — | — | — | N/A — empty |
|
||||
| Exploration logs | 0 | — | — | — | N/A — empty |
|
||||
|
||||
---
|
||||
|
||||
## Skills (`.agents/skills/`)
|
||||
|
||||
| Skill | File | Status | Trust | Tested | Notes |
|
||||
|-------|------|--------|-------|--------|-------|
|
||||
| Launch Experiment | `launch-experiment.md` | 🟡 Draft | Low | ❌ | Needs dry-run validation |
|
||||
| Monitor Experiment | `monitor-experiment.md` | 🟡 Draft | Low | ❌ | Requires W&B API access to test |
|
||||
| Summarize Run | `summarize-run.md` | 🟡 Draft | Low | ❌ | Pattern from existing test infra |
|
||||
| Log Experiment | `log-experiment.md` | 🟡 Draft | Low | ❌ | Journal formatting only |
|
||||
| Evaluate Video Quality | `evaluate-video-quality.md` | 🟡 Draft | Low | ❌ | SSIM section most mature |
|
||||
| Index Related Work | `index-related-work.md` | 🟡 Draft | Low | ❌ | Schema defined, no entries yet |
|
||||
| Search Related Work | `search-related-work.md` | 🟡 Draft | Low | ❌ | Depends on indexed entries |
|
||||
| Skill Template | `SKILL_TEMPLATE.md` | ✅ Ready | High | ✅ | Meta-template, stable |
|
||||
|
||||
### Trust Level Definitions
|
||||
- **High**: Tested in production, validated against real experiments
|
||||
- **Medium**: Logic is sound, partially tested or based on existing patterns
|
||||
- **Low**: Newly written, not yet validated
|
||||
- **None**: Placeholder only
|
||||
|
||||
---
|
||||
|
||||
## Workflows / SOPs (`.agents/workflows/`)
|
||||
|
||||
| Workflow | File | Status | Trust | Tested | Notes |
|
||||
|----------|------|--------|-------|--------|-------|
|
||||
| Experiment Lifecycle | `experiment-lifecycle.md` | 🟡 Draft | Low | ❌ | End-to-end flow, untested |
|
||||
| Evaluation Development | `evaluation-development.md` | 🟡 Draft | Low | ❌ | Metric dev process |
|
||||
| Experiment Journaling | `experiment-journaling.md` | 🟡 Draft | Low | ❌ | Journaling cadence |
|
||||
| Lesson Capture | `lesson-capture.md` | 🟡 Draft | Low | ❌ | Post-experiment reflection |
|
||||
| Sync Dashboard | `sync-dashboard.md` | 🟡 Draft | Low | ❌ | This dashboard's updater |
|
||||
|
||||
---
|
||||
|
||||
## Memory (`.agents/memory/`)
|
||||
|
||||
| File | Status | Trust | Notes |
|
||||
|------|--------|-------|-------|
|
||||
| `codebase_map.md` | ✅ Ready | High | Synthesized from full repo research |
|
||||
| `experiment_journal.md` | 🟡 Draft | Medium | Schema defined, no entries yet |
|
||||
| `evaluation_registry.md` | 🟡 Draft | Medium | SSIM/loss metrics documented |
|
||||
| `related_work/README.md` | 🟡 Draft | Medium | Schema defined, no entries yet |
|
||||
|
||||
---
|
||||
|
||||
## Lessons (`.agents/lessons/`)
|
||||
|
||||
| File | Category | Severity | Notes |
|
||||
|------|----------|----------|-------|
|
||||
|
||||
_No lessons captured yet._
|
||||
|
||||
---
|
||||
|
||||
## Exploration Logs (`.agents/exploration/`)
|
||||
|
||||
| File | Status | Topic | Notes |
|
||||
|------|--------|-------|-------|
|
||||
|
||||
_No exploration logs yet._
|
||||
|
||||
---
|
||||
|
||||
## What to Do Next
|
||||
|
||||
1. **Validate skills**: Run a minimal training experiment using the
|
||||
`experiment-lifecycle` SOP to test `launch-experiment` → `monitor-experiment`
|
||||
→ `summarize-run` end-to-end.
|
||||
2. **Index first related work**: Use `index-related-work` to add at least one
|
||||
paper (e.g., the Self-Forcing paper used in the codebase).
|
||||
3. **Capture first lesson**: After the validation run, capture any findings.
|
||||
4. **Promote to Ready**: As each skill/SOP is tested, update its status here.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Exploration Logs
|
||||
|
||||
This directory holds draft procedures and investigation notes for tasks that
|
||||
don't yet have a standardized skill or SOP. Each exploration should follow this
|
||||
template.
|
||||
|
||||
## When to Create an Exploration Log
|
||||
|
||||
- You are working on a task with no existing skill or workflow.
|
||||
- You are experimenting with a new metric, training technique, or tool.
|
||||
- You want to document findings before they are promoted to a standard.
|
||||
|
||||
## File Naming
|
||||
|
||||
`<topic-slug>.md` — e.g., `fvd-metric-investigation.md`
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
# Exploration Log: <Topic>
|
||||
|
||||
## Status: draft | under_review | promoted | abandoned
|
||||
|
||||
## Context
|
||||
<Why this exploration is needed — link to experiment or task if applicable.>
|
||||
|
||||
## Progress
|
||||
- [ ] Step 1: ...
|
||||
- [ ] Step 2: ...
|
||||
|
||||
## Findings
|
||||
<What you have learned so far.>
|
||||
|
||||
## Mistakes / Dead Ends
|
||||
<What didn't work and why — these become lessons.>
|
||||
|
||||
## Proposed Standardization
|
||||
<If this works, describe the skill/SOP/workflow to create.>
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
1. **Create** during exploration mode.
|
||||
2. **Update** as you make progress.
|
||||
3. **Promote**: If findings are solid, create a skill in `.agents/skills/` or an SOP in `.agents/workflows/`.
|
||||
4. **Archive mistakes**: Move failures into `.agents/lessons/`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Lessons Learned Database
|
||||
|
||||
This directory stores documented mistakes, unexpected behaviors, and their fixes.
|
||||
Each lesson is a permanent record that helps agents and humans avoid repeating
|
||||
past errors.
|
||||
|
||||
## When to Create a Lesson
|
||||
|
||||
- An experiment failed for a non-obvious reason.
|
||||
- A configuration or hyperparameter choice led to wasted compute.
|
||||
- A porting, data, or infrastructure issue was discovered and resolved.
|
||||
- A workaround was needed for a known framework/library bug.
|
||||
|
||||
## File Naming
|
||||
|
||||
`<YYYY-MM-DD>_<short-slug>.md` — e.g., `2026-03-02_lr-too-high-for-lora.md`
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: <ISO-8601>
|
||||
experiment: <reference to experiment_journal.md entry, if applicable>
|
||||
category: hyperparameter | data | infrastructure | evaluation | porting | other
|
||||
severity: critical | important | minor
|
||||
---
|
||||
|
||||
# <Short Descriptive Title>
|
||||
|
||||
## What Happened
|
||||
<Description of the problem and its symptoms.>
|
||||
|
||||
## Root Cause
|
||||
<Analysis of why it happened.>
|
||||
|
||||
## Fix / Workaround
|
||||
<What resolved the issue.>
|
||||
|
||||
## Prevention
|
||||
<How to avoid this in the future — updated skills, SOPs, or checks.>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
- Before starting a task, **search this directory** for relevant lessons.
|
||||
- After completing or failing a task, **check if a new lesson should be created**.
|
||||
- Periodically review lessons for **patterns** — recurring themes may warrant
|
||||
a new skill, SOP, or codebase fix.
|
||||
@@ -0,0 +1,129 @@
|
||||
# FastVideo-WorldModel — Codebase Map
|
||||
|
||||
High-level structural index for agent orientation. Updated 2026-03-08.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```
|
||||
FastVideo-WorldModel/
|
||||
├── fastvideo/ # Core Python package
|
||||
│ ├── models/ # Model implementations
|
||||
│ │ ├── dits/ # DiT transformers (wanvideo, ltx2, ...)
|
||||
│ │ ├── vaes/ # VAE models
|
||||
│ │ ├── encoders/ # Text/image encoders (T5, CLIP)
|
||||
│ │ ├── schedulers/ # Noise schedulers
|
||||
│ │ ├── upsamplers/ # Super-resolution models
|
||||
│ │ ├── audio/ # Audio models
|
||||
│ │ └── loader/ # Component loaders for HF repos
|
||||
│ ├── configs/ # Configuration system
|
||||
│ │ ├── models/ # Arch configs + param_names_mapping
|
||||
│ │ ├── pipelines/ # Pipeline wiring
|
||||
│ │ └── sample/ # Default sampling parameters
|
||||
│ ├── pipelines/ # End-to-end pipelines
|
||||
│ │ ├── basic/ # Per-model pipelines (wan/, ltx2/, ...)
|
||||
│ │ └── stages/ # Reusable pipeline stages
|
||||
│ ├── train/ # Refactored training framework (YAML-driven, preferred)
|
||||
│ │ ├── trainer.py # Main training loop coordinator
|
||||
│ │ ├── entrypoint/ # Training entrypoint (train.py) + checkpoint conversion
|
||||
│ │ ├── methods/ # Training algorithms (FineTune, DFSFT, DMD2, SelfForcing)
|
||||
│ │ │ ├── base.py # TrainingMethod ABC
|
||||
│ │ │ ├── fine_tuning/ # FineTuneMethod, DiffusionForcingSFTMethod
|
||||
│ │ │ └── distribution_matching/ # DMD2Method, SelfForcingMethod
|
||||
│ │ ├── models/ # Per-role model wrappers (ModelBase, CausalModelBase)
|
||||
│ │ │ └── wan/ # WanModel, WanCausalModel
|
||||
│ │ ├── callbacks/ # Composable hooks (grad_clip, ema, validation)
|
||||
│ │ └── utils/ # Config, builder, checkpoint, optimizer, tracking
|
||||
│ ├── training/ # Legacy training infrastructure (being phased out)
|
||||
│ │ ├── trackers.py # W&B tracker (BaseTracker → WandbTracker)
|
||||
│ │ ├── training_utils.py # Checkpointing, grad clipping, state dicts
|
||||
│ │ ├── training_pipeline.py # Base training pipeline
|
||||
│ │ ├── wan_training_pipeline.py # Wan T2V training
|
||||
│ │ ├── wan_i2v_training_pipeline.py # Wan I2V training
|
||||
│ │ ├── distillation_pipeline.py # Distillation base
|
||||
│ │ ├── wan_distillation_pipeline.py # Wan distillation
|
||||
│ │ ├── self_forcing_distillation_pipeline.py # Self-forcing distill
|
||||
│ │ ├── ltx2_training_pipeline.py # LTX-2 training
|
||||
│ │ └── matrixgame_training_pipeline.py # MatrixGame training
|
||||
│ ├── attention/ # Attention backends
|
||||
│ ├── distributed/ # Sequence/tensor parallel utilities
|
||||
│ ├── layers/ # Tensor-parallel layers
|
||||
│ ├── tests/ # Package-level tests
|
||||
│ │ ├── training/ # Training regression tests (W&B summary comparison)
|
||||
│ │ ├── ssim/ # SSIM visual regression tests
|
||||
│ │ ├── encoders/ # Encoder parity tests
|
||||
│ │ └── modal/ # Modal CI test runner
|
||||
│ └── registry.py # Unified config registry
|
||||
├── fastvideo-kernel/ # CUDA/custom kernels (separate build: ./build.sh)
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── distill/ # Distillation launch scripts
|
||||
│ ├── inference/ # Inference scripts
|
||||
│ ├── checkpoint_conversion/ # Weight conversion tools
|
||||
│ ├── finetune/ # Finetune scripts
|
||||
│ └── preprocess/ # Data preprocessing
|
||||
├── examples/ # Ready-to-run examples
|
||||
│ ├── training/ # Training examples (finetune/, consistency_finetune/)
|
||||
│ ├── distill/ # Distillation examples
|
||||
│ ├── inference/ # Inference examples
|
||||
│ └── dataset/ # Dataset examples
|
||||
├── docs/ # MkDocs documentation source
|
||||
│ ├── design/overview.md # Architecture overview
|
||||
│ ├── training/ # Training guides
|
||||
│ └── contributing/ # Contributor guides + coding_agents.md
|
||||
├── tests/ # Top-level tests (local_tests/)
|
||||
├── AGENTS.md # Agent coding guidelines
|
||||
└── .agents/ # Agent infrastructure (you are here)
|
||||
```
|
||||
|
||||
## Key Training Entrypoints
|
||||
|
||||
### New framework (`fastvideo/train/`) — preferred
|
||||
|
||||
| Method | Config Example | Launch Pattern |
|
||||
|--------|---------------|----------------|
|
||||
| FineTune (Wan) | `examples/train/finetune_wan2.1_t2v_1.3B_vsa_*.yaml` | `torchrun -m fastvideo.train.entrypoint.train --config <yaml>` |
|
||||
| DFSFT (Wan causal) | `examples/train/dfsft_wan_causal_t2v_1.3B.yaml` | `torchrun -m fastvideo.train.entrypoint.train --config <yaml>` |
|
||||
| DMD2 distillation | `examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml` | `torchrun -m fastvideo.train.entrypoint.train --config <yaml>` |
|
||||
| Self-Forcing | `examples/train/self_forcing_wan_causal_t2v_1.3B.yaml` | `torchrun -m fastvideo.train.entrypoint.train --config <yaml>` |
|
||||
|
||||
### Legacy pipelines (`fastvideo/training/`) — being phased out
|
||||
|
||||
| Pipeline | Entrypoint | Launch Pattern |
|
||||
|----------|-----------|----------------|
|
||||
| Wan T2V finetune | `fastvideo/training/wan_training_pipeline.py` | `torchrun --nproc_per_node N` |
|
||||
| Wan I2V finetune | `fastvideo/training/wan_i2v_training_pipeline.py` | `torchrun --nproc_per_node N` |
|
||||
| Wan distillation (DMD) | `fastvideo/training/wan_distillation_pipeline.py` | `torchrun --nproc_per_node N` |
|
||||
| Self-forcing distill | `fastvideo/training/wan_self_forcing_distillation_pipeline.py` | `torchrun --nproc_per_node N` |
|
||||
| LTX-2 finetune | `fastvideo/training/ltx2_training_pipeline.py` | `torchrun --nproc_per_node N` |
|
||||
| MatrixGame | `fastvideo/training/matrixgame_training_pipeline.py` | `torchrun --nproc_per_node N` |
|
||||
|
||||
## W&B Integration
|
||||
|
||||
- **Tracker classes**: `fastvideo/training/trackers.py`
|
||||
- `WandbTracker` — logs metrics, videos, timing
|
||||
- `SequentialTracker` — fan-out to multiple trackers
|
||||
- `DummyTracker` — no-op for offline/test
|
||||
- **Run summary location**: `<output_dir>/tracker/wandb/latest-run/files/wandb-summary.json`
|
||||
- **Reference summaries**: `fastvideo/tests/training/*/` (e.g., `a40_reference_wandb_summary.json`)
|
||||
- **Environment**: `WANDB_API_KEY`, `WANDB_BASE_URL`, `WANDB_MODE`
|
||||
|
||||
## Critical Environment Variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `WANDB_API_KEY` | W&B authentication |
|
||||
| `WANDB_MODE` | `online` / `offline` |
|
||||
| `FASTVIDEO_ATTENTION_BACKEND` | `FLASH_ATTN` / `TORCH_SDPA` |
|
||||
| `TOKENIZERS_PARALLELISM` | Set `false` to avoid fork warnings |
|
||||
| `HF_HOME` | HuggingFace cache directory |
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
uv pip install -e .[dev] # Editable install
|
||||
pre-commit run --all-files # Lint/format/spell
|
||||
pytest tests/ # Top-level tests
|
||||
pytest fastvideo/tests/ -v # Package tests
|
||||
pytest fastvideo/tests/training/Vanilla -srP # Training loss regression
|
||||
pytest fastvideo/tests/ssim/ -vs # SSIM visual regression
|
||||
cd fastvideo-kernel && ./build.sh # Build kernels
|
||||
```
|
||||
@@ -0,0 +1,327 @@
|
||||
# Evaluation Metrics Registry
|
||||
|
||||
Living catalog of all evaluation metrics for FastVideo-WorldModel video quality
|
||||
assessment. Each metric includes a detailed explanation, implementation status,
|
||||
usage instructions, and interpretation guide.
|
||||
|
||||
_Last updated: 2026-03-02_
|
||||
|
||||
---
|
||||
|
||||
## Metric Summary
|
||||
|
||||
| Metric | Category | Status | Location | Trust |
|
||||
|--------|----------|--------|----------|-------|
|
||||
| **FVD** | Distribution | ✅ Implemented | `benchmarks/fvd/` | High |
|
||||
| **SSIM** | Reference | ✅ Implemented | `fastvideo/tests/ssim/` | High |
|
||||
| **LPIPS** | Perceptual | ✅ Implemented | `scripts/lora_extraction/` | Medium |
|
||||
| **Loss trajectory** | Training signal | ✅ Implemented | W&B `train_loss` | Medium |
|
||||
| **Grad norm stability** | Training signal | ✅ Implemented | W&B `grad_norm` | Medium |
|
||||
| **GameWorld Score** | Multi-dim benchmark | 🟡 External | Matrix-Game repo | Low |
|
||||
| **Human preference** | Gold standard | 🔴 Manual | N/A | Highest |
|
||||
|
||||
---
|
||||
|
||||
## Implemented Metrics
|
||||
|
||||
### FVD — Fréchet Video Distance
|
||||
|
||||
**Category**: Distribution-level quality metric
|
||||
**Status**: ✅ Fully implemented in `benchmarks/fvd/`
|
||||
**Trust**: High — standard protocol, I3D feature extractor
|
||||
|
||||
#### What It Measures
|
||||
FVD measures the distance between the **distribution** of generated videos and
|
||||
a distribution of real/reference videos. It works by:
|
||||
1. Extracting spatiotemporal features from both real and generated video sets
|
||||
using a pretrained **I3D** (Inflated 3D ConvNet) model.
|
||||
2. Modeling each set of features as a multivariate Gaussian (mean + covariance).
|
||||
3. Computing the **Fréchet distance** between the two Gaussians.
|
||||
|
||||
Lower FVD = generated videos are more statistically similar to real videos.
|
||||
|
||||
#### Why It Matters
|
||||
- FVD is the **de facto standard** for benchmarking video generation models.
|
||||
- It captures both **visual quality** (are individual frames realistic?) and
|
||||
**temporal coherence** (do frames flow naturally?).
|
||||
- Matrix-Game 2.0, Open-Sora, and most video generation papers report FVD.
|
||||
|
||||
#### Limitations
|
||||
- Requires a **large sample set** (standard protocol uses 2048 videos) to
|
||||
produce stable statistics. Small sample sizes yield noisy results.
|
||||
- Measures **distributional similarity**, not per-video quality. A model could
|
||||
have low FVD by generating a diverse set of "roughly okay" videos.
|
||||
- The I3D model was trained on Kinetics-400 (human actions). It may be less
|
||||
sensitive to domain-specific artifacts in non-human-action videos (e.g.,
|
||||
driving, game environments).
|
||||
- Does not directly measure text-video alignment or action controllability.
|
||||
|
||||
#### How to Use
|
||||
|
||||
```python
|
||||
# Programmatic
|
||||
from benchmarks.fvd import compute_fvd_with_config, FVDConfig
|
||||
|
||||
config = FVDConfig.fvd2048_16f() # Standard: 2048 videos, 16 frames
|
||||
results = compute_fvd_with_config('data/real/', 'outputs/gen/', config)
|
||||
print(f"FVD: {results['fvd']:.2f}")
|
||||
```
|
||||
|
||||
```bash
|
||||
# CLI
|
||||
python -m benchmarks.fvd.cli \
|
||||
--real-path data/real/ \
|
||||
--gen-path outputs/gen/ \
|
||||
--protocol fvd2048_16f
|
||||
```
|
||||
|
||||
**Preset protocols**:
|
||||
| Protocol | Videos | Frames | Use Case |
|
||||
|----------|--------|--------|----------|
|
||||
| `fvd2048_16f` | 2048 | 16 | Standard benchmark (papers) |
|
||||
| `fvd2048_128f` | 2048 | 128 | Long video evaluation |
|
||||
| `quick_test` | 100 | 16 | Fast dev iteration |
|
||||
|
||||
**Feature extractors**: `i3d` (default, standard), `clip`, `videomae`
|
||||
|
||||
#### Interpretation
|
||||
| FVD Range | Interpretation |
|
||||
|-----------|---------------|
|
||||
| < 100 | Excellent — near-real quality |
|
||||
| 100–300 | Good — competitive with SOTA |
|
||||
| 300–600 | Fair — noticeable gap from real |
|
||||
| > 600 | Poor — significant quality issues |
|
||||
|
||||
> FVD values are dataset-dependent. Always compare against baselines evaluated
|
||||
> on the same real video distribution.
|
||||
|
||||
---
|
||||
|
||||
### SSIM — Structural Similarity Index
|
||||
|
||||
**Category**: Per-frame reference comparison
|
||||
**Status**: ✅ Implemented in `fastvideo/tests/ssim/`
|
||||
**Trust**: High — used in CI regression tests
|
||||
|
||||
#### What It Measures
|
||||
SSIM compares two images (or video frames) based on three components:
|
||||
1. **Luminance**: brightness similarity
|
||||
2. **Contrast**: dynamic range similarity
|
||||
3. **Structure**: spatial pattern similarity
|
||||
|
||||
The final score is a value in [0, 1] where 1.0 = identical.
|
||||
|
||||
#### Why It Matters
|
||||
- Used as a **regression guard** in CI: ensures model updates don't degrade
|
||||
visual output below a threshold.
|
||||
- More perceptually meaningful than raw pixel MSE.
|
||||
- Fast to compute — suitable for automated testing.
|
||||
|
||||
#### Limitations
|
||||
- Requires a **pixel-aligned reference** video. Cannot compare videos with
|
||||
different seeds, prompts, or angles.
|
||||
- Operates **per-frame** — does not capture temporal coherence.
|
||||
- Insensitive to some perceptual artifacts (color shifts, high-frequency noise).
|
||||
|
||||
#### How to Use
|
||||
|
||||
```bash
|
||||
pytest fastvideo/tests/ssim/ -vs
|
||||
```
|
||||
|
||||
#### Interpretation
|
||||
| SSIM Range | Quality |
|
||||
|------------|---------|
|
||||
| > 0.90 | Excellent — very close to reference |
|
||||
| 0.80–0.90 | Good — acceptable for most uses |
|
||||
| 0.70–0.80 | Fair — noticeable differences |
|
||||
| < 0.70 | Poor — significant divergence |
|
||||
|
||||
---
|
||||
|
||||
### LPIPS — Learned Perceptual Image Patch Similarity
|
||||
|
||||
**Category**: Per-frame perceptual distance
|
||||
**Status**: ✅ Implemented in `scripts/lora_extraction/lora_inference_comparison.py`
|
||||
**Trust**: Medium — available but only used for LoRA comparison currently
|
||||
|
||||
#### What It Measures
|
||||
LPIPS uses a pretrained neural network (AlexNet by default) to extract
|
||||
deep features from two images and computes the distance between them in
|
||||
feature space. Unlike SSIM, LPIPS correlates much more strongly with
|
||||
**human perceptual judgments**.
|
||||
|
||||
Lower LPIPS = more perceptually similar.
|
||||
|
||||
#### Why It Matters
|
||||
- Best available automated proxy for **human visual judgments** at the frame
|
||||
level.
|
||||
- Captures semantic and structural differences that SSIM misses (e.g., texture
|
||||
changes, minor recoloring).
|
||||
- Used for validating LoRA merge quality.
|
||||
|
||||
#### Limitations
|
||||
- Per-frame metric — no temporal awareness.
|
||||
- Requires reference video (paired comparison only).
|
||||
- Slightly slower than SSIM due to neural network forward pass.
|
||||
|
||||
#### How to Use
|
||||
|
||||
```bash
|
||||
python scripts/lora_extraction/lora_inference_comparison.py \
|
||||
--base merged_model \
|
||||
--ft path/to/finetuned \
|
||||
--adapter NONE \
|
||||
--output-dir results \
|
||||
--prompt "A cat" \
|
||||
--compute-lpips
|
||||
```
|
||||
|
||||
#### Interpretation
|
||||
| LPIPS Range | Quality |
|
||||
|-------------|---------|
|
||||
| < 0.10 | Excellent — nearly indistinguishable |
|
||||
| 0.10–0.20 | Good — minor perceptual differences |
|
||||
| 0.20–0.40 | Fair — noticeable differences |
|
||||
| > 0.40 | Poor — clearly different |
|
||||
|
||||
---
|
||||
|
||||
### Loss Trajectory
|
||||
|
||||
**Category**: Training signal proxy
|
||||
**Status**: ✅ Active (from W&B `train_loss`)
|
||||
**Trust**: Medium — proxy, not direct quality measure
|
||||
|
||||
#### What It Measures
|
||||
Tracks the training loss over time. A healthy training run shows:
|
||||
- **Decreasing loss** over the first hundreds of steps.
|
||||
- **Stable gradient norms** (no wild spikes).
|
||||
- **Consistent step times** (no infrastructure issues).
|
||||
|
||||
#### Why It Matters
|
||||
- Cheapest evaluation signal — available in real-time from W&B.
|
||||
- Critical for the **30-minute quality check** workflow.
|
||||
- At later training stages (when loss becomes meaningful), trajectory shape
|
||||
can predict final model quality.
|
||||
|
||||
#### Context: How This Evolves
|
||||
The team's experience shows evaluation signals change during a project:
|
||||
- **Early stage**: Loss may be flat or meaningless → focus on SSIM & visual
|
||||
inspection instead.
|
||||
- **Mid stage**: Loss starts decreasing → trajectory shape becomes useful.
|
||||
- **Late stage**: Loss is meaningful → can compare trajectories across runs.
|
||||
|
||||
This dynamic is a key insight from the team's workflow: don't over-rely on
|
||||
loss early; don't ignore it late.
|
||||
|
||||
---
|
||||
|
||||
### Grad Norm Stability
|
||||
|
||||
**Category**: Training health diagnostic
|
||||
**Status**: ✅ Active (from W&B `grad_norm`)
|
||||
**Trust**: Medium — diagnostic, not quality metric
|
||||
|
||||
#### What It Measures
|
||||
The magnitude of gradients during training. Stable grad norms indicate
|
||||
healthy optimization. Spikes or NaN values indicate training instability.
|
||||
|
||||
#### Alert Thresholds
|
||||
| Condition | Meaning |
|
||||
|-----------|---------|
|
||||
| Stable ~0.3–0.5 | Normal training |
|
||||
| Single spike > 3× average | Possible bad batch, monitor |
|
||||
| NaN or Inf | 🔴 Training has diverged — stop run |
|
||||
| Increasing trend | Learning rate may be too high |
|
||||
|
||||
---
|
||||
|
||||
## External Benchmarks
|
||||
|
||||
### GameWorld Score Benchmark (Matrix-Game)
|
||||
|
||||
**Category**: Multi-dimensional evaluation framework for interactive world models
|
||||
**Status**: 🟡 External — not implemented in-repo
|
||||
**Source**: [Matrix-Game 1.0 benchmark](https://github.com/SkyworkAI/Matrix-Game), used in [Matrix-Game 2.0 paper](https://arxiv.org/abs/2508.13009)
|
||||
|
||||
#### What It Measures
|
||||
A comprehensive benchmark examining **four critical capabilities**:
|
||||
|
||||
| Dimension | What It Evaluates | Example Signals |
|
||||
|-----------|-------------------|-----------------|
|
||||
| **Visual quality** | Frame-level realism, absence of artifacts | Color fidelity, sharpness, coherence |
|
||||
| **Temporal quality** | Smoothness across frames, motion consistency | Jitter, flickering, temporal aliasing |
|
||||
| **Action controllability** | Response to input actions (keyboard/mouse) | Action delay, correctness, smoothness |
|
||||
| **Physical rule understanding** | Adherence to physics (gravity, collision) | Object persistence, plausible motion |
|
||||
|
||||
#### Context from Matrix-Game 2.0
|
||||
- Evaluation uses **597-frame composite action sequences** over 32 Minecraft
|
||||
scenes and 16 wild scenes.
|
||||
- Action controllability assessment is **Minecraft-specific** — cannot be
|
||||
directly applied to wild/general scenes.
|
||||
- The paper notes that models that "collapse" to static frames can
|
||||
paradoxically score higher on consistency metrics — beware of this confound.
|
||||
|
||||
#### Relevance to FastVideo
|
||||
- Matrix-Game 2.0 is built on SkyReels-V2/Wan2.1 architecture — **same model
|
||||
family as FastVideo**.
|
||||
- Their distillation uses DMD-based Self-Forcing — **same technique** as our
|
||||
`self_forcing_distillation_pipeline.py`.
|
||||
- GameWorld Score dimensions are a useful framework for thinking about world
|
||||
model quality even outside gaming contexts.
|
||||
|
||||
---
|
||||
|
||||
## Human Preference Evaluation
|
||||
|
||||
**Category**: Gold-standard quality assessment
|
||||
**Status**: 🔴 Manual process — no automated implementation
|
||||
**Priority**: **Highest** — this is the most important evaluation signal
|
||||
**Trust**: Highest — but expensive
|
||||
|
||||
### What It Measures
|
||||
Human evaluators compare generated videos and rate them on dimensions like:
|
||||
- Overall quality and realism
|
||||
- Temporal coherence and smoothness
|
||||
- Prompt adherence / action correctness
|
||||
- Absence of artifacts
|
||||
|
||||
#### Why It's the Most Important Metric
|
||||
All automated metrics are **proxies** for human judgment. They can be gamed
|
||||
or may miss artifacts that humans easily notice. Human preference is the
|
||||
ultimate ground truth for video generation quality.
|
||||
|
||||
#### Cost & Practicality
|
||||
| Approach | Cost | Scale | When to Use |
|
||||
|----------|------|-------|-------------|
|
||||
| Internal team review | Low | ~10–50 videos | Every major checkpoint |
|
||||
| Crowdsource (MTurk, Scale) | Medium | 100+ videos | Pre-release validation |
|
||||
| A/B preference test | Medium | Pairs | Comparing two model versions |
|
||||
|
||||
#### Recommended Protocol
|
||||
1. Sample 10–20 videos from the model at a checkpoint.
|
||||
2. Include diverse prompts (easy + hard, short + long).
|
||||
3. Have 2–3 evaluators score each video 1–5 on: quality, coherence, fidelity.
|
||||
4. Record scores in the experiment journal.
|
||||
|
||||
---
|
||||
|
||||
## Metrics NOT Used
|
||||
|
||||
| Metric | Reason |
|
||||
|--------|--------|
|
||||
| ~~CLIP-Score~~ | Not used by the team. Measures text-image alignment using CLIP embeddings, but not well-suited for video temporal quality. |
|
||||
| Inception Score (IS) | Less informative than FVD for video; primarily an image metric. |
|
||||
| PSNR | Pixel-level metric; less perceptually meaningful than SSIM/LPIPS. |
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Metric
|
||||
|
||||
Follow the SOP: `.agents/workflows/evaluation-development.md`
|
||||
|
||||
1. Prototype in `.agents/exploration/`
|
||||
2. Validate on known-good and known-bad samples
|
||||
3. Add to this registry
|
||||
4. Update the `evaluate-video-quality` skill
|
||||
@@ -0,0 +1,21 @@
|
||||
# Experiment Journal
|
||||
|
||||
Living log of all experiments. Each entry captures what was tried, the result,
|
||||
and any insights. Newest entries go at the top.
|
||||
|
||||
_No experiments logged yet. Use the `log-experiment` skill to add entries._
|
||||
|
||||
<!-- TEMPLATE — copy and fill for each new experiment:
|
||||
|
||||
## [YYYY-MM-DD] Experiment: <name>
|
||||
- **Hypothesis**: <what you expected to learn>
|
||||
- **Config**: model=..., lr=..., sp_size=..., gpus=..., script=...
|
||||
- **W&B run**: <run_id or URL>
|
||||
- **Duration**: <total wall time>
|
||||
- **Key metrics**: loss=..., step_time=..., grad_norm=...
|
||||
- **Checkpoint**: <path>
|
||||
- **Insight**: <what was learned>
|
||||
- **Status**: running | completed | failed | abandoned
|
||||
- **Related lessons**: `.agents/lessons/<filename>.md`
|
||||
|
||||
-->
|
||||
@@ -0,0 +1,4 @@
|
||||
{"name": "codebase-map", "description": "High-level structural index of the FastVideo-WorldModel repository", "path": "codebase-map/README.md", "status": "ready", "trust": "high"}
|
||||
{"name": "evaluation-registry", "description": "Catalog of all evaluation metrics with detailed explanations, implementation status, and usage guides", "path": "evaluation-registry/README.md", "status": "draft", "trust": "medium"}
|
||||
{"name": "experiment-journal", "description": "Living log of all experiments with hypotheses, configs, metrics, and insights", "path": "experiment-journal/README.md", "status": "draft", "trust": "medium"}
|
||||
{"name": "related-work", "description": "Index of related papers, repos, and blog posts with structured comparisons to FastVideo", "path": "related-work/README.md", "status": "draft", "trust": "low"}
|
||||
@@ -0,0 +1,34 @@
|
||||
# Related Work Index
|
||||
|
||||
Each file in this directory is a structured summary of a related paper, repo,
|
||||
or blog post relevant to FastVideo-WorldModel training.
|
||||
|
||||
## File Format
|
||||
|
||||
Each file is named `<slug>.md` and follows this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: <paper/repo title>
|
||||
source: <URL or citation>
|
||||
type: paper | repo | blog
|
||||
date_indexed: <ISO-8601>
|
||||
tags: [world-model, distillation, evaluation, reward-shaping, ...]
|
||||
---
|
||||
|
||||
## Summary
|
||||
<1-2 paragraph summary of the work.>
|
||||
|
||||
## Key Differences from FastVideo
|
||||
- <Bullet points comparing their approach to ours.>
|
||||
|
||||
## Actionable Insights
|
||||
- <What we could adopt or adapt.>
|
||||
```
|
||||
|
||||
## How to Add New Entries
|
||||
|
||||
Use the `index-related-work` skill, or manually create a file following the
|
||||
template above.
|
||||
|
||||
_No related work indexed yet._
|
||||
@@ -0,0 +1,76 @@
|
||||
# Agent Onboarding — FastVideo-WorldModel
|
||||
|
||||
Welcome, agent. This is the **master onboarding** guide. Follow the steps below,
|
||||
then check if a **domain-specific onboarding** exists for your task.
|
||||
|
||||
## Domain-Specific Onboarding
|
||||
|
||||
If your task falls into one of these areas, read the specialized guide **after**
|
||||
completing the general steps below:
|
||||
|
||||
| Domain | Guide | When to Use |
|
||||
|--------|-------|-------------|
|
||||
| **WorldModel Training** | `worldmodel-training/README.md` | Training, finetuning, distillation, experiment management |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Understand the Codebase
|
||||
|
||||
Read these files to build your context:
|
||||
|
||||
| Priority | File | What you learn |
|
||||
|----------|------|----------------|
|
||||
| 1 | `AGENTS.md` | Coding guidelines, build/test commands, PR conventions |
|
||||
| 2 | `docs/design/overview.md` | Architecture: models, pipelines, configs, registry |
|
||||
| 3 | `fastvideo/train/` | Refactored training framework (YAML-driven, modular methods/models/callbacks) |
|
||||
| 4 | `docs/training/overview.md` | Training data flow and preprocessing |
|
||||
| 5 | `docs/training/finetune.md` | Training arguments, parallelism, LoRA, validation |
|
||||
| 6 | `docs/contributing/coding_agents.md` | How to add model pipelines with agent assistance |
|
||||
|
||||
## Step 2: Discover Available Resources
|
||||
|
||||
Read these two index files to see what skills and memory modules exist:
|
||||
|
||||
- **`.agents/skills/index.jsonl`** — catalog of all agent skills (name + description)
|
||||
- **`.agents/memory/index.jsonl`** — catalog of all memory modules (name + description)
|
||||
|
||||
Each entry has a `path` field pointing to the full content. Only load the
|
||||
full README.md for modules relevant to your current task.
|
||||
|
||||
## Step 3: Check for Existing Skills & SOPs
|
||||
|
||||
Before writing new code or procedures:
|
||||
|
||||
1. **Skills**: Read `.agents/skills/index.jsonl` — find a matching skill by description.
|
||||
2. **Workflows/SOPs**: Browse `.agents/workflows/` — step-by-step procedures for common tasks.
|
||||
3. **Lessons**: Browse `.agents/lessons/` — known pitfalls and their fixes.
|
||||
|
||||
If a skill or SOP exists for your task, **use it**. If not, you are in **exploration mode** — see Step 4.
|
||||
|
||||
## Step 4: Exploration Mode
|
||||
|
||||
If no existing skill/SOP covers your task:
|
||||
|
||||
1. Document your progress in `.agents/exploration/<topic>.md` using the template in `.agents/exploration/README.md`.
|
||||
2. At the end of your session, reflect:
|
||||
- **What worked** → propose a new skill or SOP in the exploration log.
|
||||
- **What failed** → create a lesson in `.agents/lessons/`.
|
||||
3. Flag the exploration log for human review.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
.agents/
|
||||
├── ONBOARDING.md ← you are here
|
||||
├── STATUS.md ← dashboard: completeness & trust of all components
|
||||
├── skills/ ← reusable agent skills
|
||||
├── workflows/ ← SOPs and procedures
|
||||
├── memory/ ← persistent context (folder per topic + index.jsonl)
|
||||
│ ├── index.jsonl
|
||||
│ ├── codebase-map/
|
||||
│ ├── experiment-journal/
|
||||
│ ├── evaluation-registry/
|
||||
│ └── related-work/
|
||||
├── lessons/ ← mistakes and fixes
|
||||
└── exploration/ ← draft procedures
|
||||
```
|
||||
@@ -0,0 +1,302 @@
|
||||
# WorldModel Training — Agent Onboarding
|
||||
|
||||
Specialized onboarding for agents working on FastVideo-WorldModel training,
|
||||
distillation, and evaluation. Read the master onboarding (`.agents/onboarding/README.md`)
|
||||
first, then come here.
|
||||
|
||||
---
|
||||
|
||||
## Domain Context
|
||||
|
||||
FastVideo-WorldModel trains **interactive world models** — video generation systems
|
||||
that respond to user actions (keyboard/mouse) in real-time. The architecture is
|
||||
based on **Wan2.1** (SkyReels-V2) DiT models with causal attention for
|
||||
auto-regressive streaming generation.
|
||||
|
||||
**Key techniques you will work with:**
|
||||
- Full finetuning and LoRA on Wan / LTX-2 / MatrixGame models
|
||||
- DMD-based distillation (few-step generation)
|
||||
- Self-Forcing distillation (causal streaming)
|
||||
- Diffusion-Forcing SFT (DFSFT) for causal models
|
||||
- VSA (Variable Sparsity Acceleration) for efficient training
|
||||
|
||||
---
|
||||
|
||||
## Training Code: Two Generations
|
||||
|
||||
### New modular framework: `fastvideo/train/` (preferred)
|
||||
|
||||
The refactored training code uses a **YAML-only config-driven** architecture
|
||||
with composable methods, per-role models, and a callback system. All new
|
||||
training work should use this framework.
|
||||
|
||||
### Legacy pipelines: `fastvideo/training/` (deprecated)
|
||||
|
||||
The old monolithic pipeline classes (`WanTrainingPipeline`,
|
||||
`DistillationPipeline`, etc.) still exist but are being phased out. The new
|
||||
framework imports select utilities from `fastvideo/training/` for backward
|
||||
compatibility (EMA, gradient clipping, checkpoint wrappers).
|
||||
|
||||
---
|
||||
|
||||
## Essential Reading (Training-Specific)
|
||||
|
||||
Read these **in order** before touching any training code:
|
||||
|
||||
| # | File | What You Learn |
|
||||
|---|------|----------------|
|
||||
| 1 | `docs/training/overview.md` | Training data flow: raw video → text embeddings + video latents → training |
|
||||
| 2 | `docs/training/finetune.md` | Training arguments, parallelism (SP/TP), LoRA, validation settings |
|
||||
| 3 | `docs/training/data_preprocess.md` | How to preprocess datasets into the expected format |
|
||||
| 4 | `docs/design/overview.md` | Architecture: models, pipelines, configs, registry |
|
||||
|
||||
---
|
||||
|
||||
## New Training Framework (`fastvideo/train/`)
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
fastvideo/train/
|
||||
├── __init__.py → exports Trainer
|
||||
├── trainer.py → main training loop coordinator
|
||||
├── entrypoint/
|
||||
│ ├── train.py → YAML-only training entrypoint
|
||||
│ └── dcp_to_diffusers.py → checkpoint conversion utility
|
||||
├── methods/ → training algorithms (TrainingMethod ABC)
|
||||
│ ├── base.py → TrainingMethod base class
|
||||
│ ├── fine_tuning/
|
||||
│ │ ├── finetune.py → FineTuneMethod (supervised finetuning)
|
||||
│ │ └── dfsft.py → DiffusionForcingSFTMethod (causal)
|
||||
│ ├── distribution_matching/
|
||||
│ │ ├── dmd2.py → DMD2Method (distribution matching distill)
|
||||
│ │ └── self_forcing.py → SelfForcingMethod (causal streaming)
|
||||
│ ├── knowledge_distillation/ → (stub, not yet implemented)
|
||||
│ └── consistency_model/ → (stub, not yet implemented)
|
||||
├── models/ → per-role model instances
|
||||
│ ├── base.py → ModelBase & CausalModelBase (ABC)
|
||||
│ └── wan/
|
||||
│ ├── wan.py → WanModel (non-causal)
|
||||
│ └── wan_causal.py → WanCausalModel (causal streaming)
|
||||
├── callbacks/ → training hooks & monitoring
|
||||
│ ├── callback.py → Callback base class + CallbackDict
|
||||
│ ├── grad_clip.py → GradNormClipCallback
|
||||
│ ├── ema.py → EMACallback (shadow weights)
|
||||
│ └── validation.py → ValidationCallback (sampling + eval)
|
||||
└── utils/ → configuration, building, checkpointing
|
||||
├── builder.py → build_from_config() (config → runtime)
|
||||
├── checkpoint.py → CheckpointManager (DCP-based)
|
||||
├── config.py → load_run_config() (YAML → RunConfig)
|
||||
├── training_config.py → TypedConfig dataclasses
|
||||
├── optimizer.py → build_optimizer_and_scheduler()
|
||||
├── instantiate.py → resolve_target() + instantiate()
|
||||
├── tracking.py → build_tracker() (W&B, etc.)
|
||||
├── dataloader.py → dataloader utilities
|
||||
├── module_state.py → apply_trainable()
|
||||
└── moduleloader.py → load_module_from_path()
|
||||
```
|
||||
|
||||
### Key Concepts
|
||||
|
||||
**TrainingMethod** (`methods/base.py`): Abstract base class for all training
|
||||
algorithms. Owns role models (student, teacher, critic), manages checkpoint
|
||||
state, and defines the training step interface.
|
||||
|
||||
**ModelBase** (`models/base.py`): Per-role model wrapper. Each role (student,
|
||||
teacher, critic) gets its own `ModelBase` instance owning a `transformer` and
|
||||
`noise_scheduler`. `CausalModelBase` extends this for streaming models.
|
||||
|
||||
**Callback system** (`callbacks/`): Composable hooks for gradient clipping,
|
||||
EMA, validation, etc. Configured via YAML, dispatched by `CallbackDict`.
|
||||
|
||||
**Config system** (`utils/config.py`, `utils/training_config.py`): YAML files
|
||||
are parsed into typed `RunConfig` dataclass trees. Models and methods use
|
||||
`_target_` fields for instantiation (similar to Hydra).
|
||||
|
||||
### Training Flow
|
||||
|
||||
```
|
||||
run_training_from_config(config_path)
|
||||
→ load_run_config() # YAML → RunConfig
|
||||
→ init_distributed() # TP/SP setup
|
||||
→ build_from_config() # instantiate models, method, dataloader
|
||||
→ Trainer.run() # main loop:
|
||||
├─ callbacks.on_train_start()
|
||||
├─ checkpoint_manager.maybe_resume()
|
||||
├─ for step in range(max_steps):
|
||||
│ ├─ method.single_train_step(batch)
|
||||
│ ├─ method.backward()
|
||||
│ ├─ callbacks.on_before_optimizer_step()
|
||||
│ ├─ method.optimizers_schedulers_step()
|
||||
│ ├─ tracker.log(metrics, step)
|
||||
│ ├─ callbacks.on_training_step_end()
|
||||
│ └─ checkpoint_manager.maybe_save(step)
|
||||
├─ callbacks.on_train_end()
|
||||
└─ checkpoint_manager.save_final()
|
||||
```
|
||||
|
||||
### Training Methods
|
||||
|
||||
| Method | Class | Use Case |
|
||||
|--------|-------|----------|
|
||||
| **FineTune** | `FineTuneMethod` | Single-role supervised finetuning |
|
||||
| **DFSFT** | `DiffusionForcingSFTMethod` | Diffusion-forcing SFT with inhomogeneous timesteps |
|
||||
| **DMD2** | `DMD2Method` | Multi-role distribution matching distillation (student + teacher + critic) |
|
||||
| **Self-Forcing** | `SelfForcingMethod` | Extends DMD2 for causal student rollouts |
|
||||
|
||||
### Launching Training (New Framework)
|
||||
|
||||
Training is launched via `torchrun` with a single YAML config:
|
||||
|
||||
```bash
|
||||
torchrun --nproc_per_node <N_GPUS> \
|
||||
-m fastvideo.train.entrypoint.train \
|
||||
--config examples/train/<config>.yaml
|
||||
```
|
||||
|
||||
### Example YAML Configs
|
||||
|
||||
| Config | Method | Description |
|
||||
|--------|--------|-------------|
|
||||
| `examples/train/finetune_wan2.1_t2v_1.3B_vsa_phase3.4_0.9sparsity.yaml` | FineTune | Wan 1.3B finetuning with VSA sparsity |
|
||||
| `examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml` | DMD2 | Wan 1.3B distillation (student + teacher + critic) |
|
||||
| `examples/train/dfsft_wan_causal_t2v_1.3B.yaml` | DFSFT | Causal Wan 1.3B diffusion-forcing SFT |
|
||||
| `examples/train/self_forcing_wan_causal_t2v_1.3B.yaml` | Self-Forcing | Causal streaming distillation |
|
||||
|
||||
### Checkpointing (New Framework)
|
||||
|
||||
**CheckpointManager** (`utils/checkpoint.py`) saves via `torch.distributed.checkpoint`:
|
||||
|
||||
```
|
||||
output_dir/
|
||||
└─ checkpoint-{step}/
|
||||
├─ dcp/ # DCP state dict
|
||||
├─ config.json # resolved training config
|
||||
└─ .fastvideo_metadata.json
|
||||
```
|
||||
|
||||
Checkpoint state includes: role model weights, per-role optimizers/schedulers,
|
||||
CUDA RNG state, and callback state (e.g., EMA shadow weights).
|
||||
|
||||
### Config Structure
|
||||
|
||||
A YAML config defines the full training pipeline:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
model_path: ...
|
||||
trainable: true
|
||||
teacher: # optional, for distillation
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
model_path: ...
|
||||
trainable: false
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.fine_tuning.FineTuneMethod
|
||||
# method-specific params...
|
||||
|
||||
training:
|
||||
distributed: { num_gpus: 8, tp_size: 1, sp_size: 8 }
|
||||
data: { data_path: ..., batch_size: 1 }
|
||||
optimizer: { lr: 1e-5, lr_scheduler: constant_with_warmup }
|
||||
loop: { max_train_steps: 1000 }
|
||||
checkpoint: { output_dir: ./outputs }
|
||||
tracker: { trackers: [wandb], project_name: ... }
|
||||
|
||||
callbacks:
|
||||
grad_clip:
|
||||
_target_: fastvideo.train.callbacks.GradNormClipCallback
|
||||
max_grad_norm: 1.0
|
||||
validation:
|
||||
_target_: fastvideo.train.callbacks.ValidationCallback
|
||||
validation_steps: 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Legacy Training Pipelines (`fastvideo/training/`)
|
||||
|
||||
> **Note:** Use the new `fastvideo/train/` framework for new work. This section
|
||||
> is retained for reference on existing pipelines not yet migrated.
|
||||
|
||||
| Pipeline | Entrypoint | Use Case |
|
||||
|----------|-----------|----------|
|
||||
| Wan T2V finetune | `fastvideo/training/wan_training_pipeline.py` | Standard text-to-video finetune / LoRA |
|
||||
| Wan I2V finetune | `fastvideo/training/wan_i2v_training_pipeline.py` | Image-to-video (first frame conditioned) |
|
||||
| MatrixGame finetune | `fastvideo/training/matrixgame_training_pipeline.py` | Action-conditioned world model |
|
||||
| LTX-2 finetune | `fastvideo/training/ltx2_training_pipeline.py` | LTX-2 architecture finetuning |
|
||||
| Wan DMD distillation | `fastvideo/training/wan_distillation_pipeline.py` | Few-step distillation via DMD |
|
||||
| Self-Forcing distill | `fastvideo/training/wan_self_forcing_distillation_pipeline.py` | Causal streaming distillation |
|
||||
|
||||
---
|
||||
|
||||
## Key Infrastructure
|
||||
|
||||
### W&B Integration
|
||||
- **Tracker**: `fastvideo/training/trackers.py` — `WandbTracker` class
|
||||
- **New framework tracker**: `fastvideo/train/utils/tracking.py` — `build_tracker()`
|
||||
- **Env vars**: `WANDB_API_KEY`, `WANDB_BASE_URL`, `WANDB_MODE`
|
||||
|
||||
### Parallelism
|
||||
- **SP** (Sequence Parallel): splits video frames across GPUs — `sp_size: N`
|
||||
- **TP** (Tensor Parallel): splits model layers across GPUs — `tp_size: N`
|
||||
- Typical configs: SP=2–8, TP=1–2
|
||||
|
||||
---
|
||||
|
||||
## Evaluation (for training runs)
|
||||
|
||||
Read `.agents/memory/evaluation-registry/README.md` for the full metric catalog.
|
||||
|
||||
**Quick summary for training agents:**
|
||||
| Metric | When to Use | Trust |
|
||||
|--------|-------------|-------|
|
||||
| **Loss trajectory** | Every run, real-time from W&B | Medium |
|
||||
| **SSIM** | When comparing against reference outputs | High |
|
||||
| **FVD** | For benchmarking model quality (`benchmarks/fvd/`) | High |
|
||||
| **LPIPS** | LoRA merge validation | Medium |
|
||||
| **Human preference** | Major checkpoints | Highest |
|
||||
|
||||
---
|
||||
|
||||
## Common Workflows
|
||||
|
||||
| Task | Skill / SOP |
|
||||
|------|-------------|
|
||||
| Launch a training run | `.agents/skills/launch-experiment/SKILL.md` |
|
||||
| Monitor a running experiment | `.agents/skills/monitor-experiment/SKILL.md` |
|
||||
| Summarize final results | `.agents/skills/summarize-run/SKILL.md` |
|
||||
| Full experiment lifecycle | `.agents/workflows/experiment-lifecycle.md` |
|
||||
| Capture lessons from failures | `.agents/workflows/lesson-capture.md` |
|
||||
|
||||
---
|
||||
|
||||
## World Model–Specific Concepts
|
||||
|
||||
### Action Injection (MatrixGame)
|
||||
The MatrixGame pipeline adds **action modules** to each DiT block, enabling
|
||||
frame-level mouse/keyboard input conditioning. The action sequence is injected
|
||||
per-frame alongside the latent video tokens.
|
||||
|
||||
### Causal Architecture
|
||||
For streaming generation, the model uses **causal attention** (each frame only
|
||||
attends to previous frames). This enables auto-regressive chunk-by-chunk
|
||||
generation — critical for real-time interactive world models.
|
||||
|
||||
### Self-Forcing Distillation
|
||||
A **data-free** distillation method where the student model is trained to
|
||||
generate coherent video sequences by being forced to use its own previous
|
||||
outputs (rather than ground-truth) as context. This produces models robust to
|
||||
their own error accumulation during long auto-regressive generation.
|
||||
|
||||
### DMD Distillation (Distribution Matching Distillation)
|
||||
Reduces inference steps from ~50 to 3–4 by training a student model to match
|
||||
the output distribution of the teacher model. Uses a critic network to estimate
|
||||
distribution divergence.
|
||||
|
||||
### Diffusion-Forcing SFT (DFSFT)
|
||||
Supervised finetuning with **inhomogeneous timesteps** across chunks — each
|
||||
chunk in a causal sequence can have a different noise level, training the model
|
||||
to handle mixed-fidelity contexts.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: <skill-name>
|
||||
description: <one-line description — Codex uses this for implicit invocation matching>
|
||||
---
|
||||
|
||||
# <Skill Name>
|
||||
|
||||
## Purpose
|
||||
<Why this skill exists and when to use it.>
|
||||
|
||||
## Prerequisites
|
||||
- <What must be true before using this skill>
|
||||
|
||||
## Inputs
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `param1` | Yes | ... |
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Step 1 title**
|
||||
- Detail...
|
||||
|
||||
2. **Step 2 title**
|
||||
- Detail...
|
||||
|
||||
## Outputs
|
||||
- <What this skill produces>
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
<Example invocation or prompt snippet>
|
||||
```
|
||||
|
||||
## References
|
||||
- <Links to relevant files in the codebase>
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
|
||||
Each skill lives in its own directory under `.agents/skills/`:
|
||||
|
||||
```
|
||||
.agents/skills/<skill-name>/
|
||||
├── SKILL.md # Required: instructions + metadata (this file)
|
||||
├── scripts/ # Optional: executable helper scripts
|
||||
├── references/ # Optional: documentation, papers
|
||||
└── assets/ # Optional: templates, resources
|
||||
```
|
||||
|
||||
After creating a new skill, add an entry to `.agents/skills/index.jsonl`:
|
||||
|
||||
```json
|
||||
{"name": "<skill-name>", "description": "<description>", "path": "<skill-name>/SKILL.md", "status": "draft", "trust": "low"}
|
||||
```
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
name: evaluate-video-quality
|
||||
description: Evaluate generated video quality using available metrics (SSIM, loss trajectory, caption consistency)
|
||||
---
|
||||
|
||||
# Evaluate Video Quality
|
||||
|
||||
## Purpose
|
||||
Assess the quality of videos generated by a training run. Combines multiple
|
||||
signals to give a holistic quality assessment. This skill is **evolving** —
|
||||
new metrics will be added as they are developed.
|
||||
|
||||
## Prerequisites
|
||||
- Generated videos available locally or via W&B artifacts.
|
||||
- For SSIM: reference videos from official implementations.
|
||||
- For caption consistency: LLM access (optional, stub for now).
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `video_paths` | Yes | List of paths to generated videos |
|
||||
| `reference_paths` | No | Paths to reference videos (for SSIM) |
|
||||
| `prompts` | No | Prompts used to generate videos (for caption check) |
|
||||
| `loss_summary` | No | Path to W&B summary JSON (for loss trajectory) |
|
||||
| `metrics` | No | Which metrics to run (default: all available) |
|
||||
|
||||
## Available Metrics
|
||||
|
||||
Check `.agents/memory/evaluation-registry/README.md` for the current catalog.
|
||||
|
||||
### SSIM (Active)
|
||||
|
||||
Leverages the existing infrastructure in `fastvideo/tests/ssim/`.
|
||||
|
||||
```bash
|
||||
pytest fastvideo/tests/ssim/ -vs --video-path <generated> --reference-path <reference>
|
||||
```
|
||||
|
||||
Or use the SSIM utility directly:
|
||||
|
||||
```python
|
||||
from fastvideo.tests.ssim.ssim_utils import compute_ssim
|
||||
score = compute_ssim(generated_video, reference_video)
|
||||
# score > 0.85 is typically "acceptable"
|
||||
```
|
||||
|
||||
**Interpretation**:
|
||||
| SSIM Range | Quality |
|
||||
|------------|---------|
|
||||
| > 0.90 | Excellent — very close to reference |
|
||||
| 0.80–0.90 | Good — acceptable for most uses |
|
||||
| 0.70–0.80 | Fair — noticeable differences |
|
||||
| < 0.70 | Poor — significant quality issues |
|
||||
|
||||
### Loss Trajectory (Active)
|
||||
|
||||
Analyze the loss curve shape from W&B summary:
|
||||
|
||||
```python
|
||||
import json
|
||||
with open(loss_summary_path) as f:
|
||||
summary = json.load(f)
|
||||
|
||||
final_loss = summary["train_loss"]
|
||||
runtime = summary["_runtime"]
|
||||
steps = summary["_step"]
|
||||
```
|
||||
|
||||
**Early-stage heuristics** (first 500 steps):
|
||||
- Loss should be decreasing (even slightly).
|
||||
- Grad norm should be stable (no wild oscillations).
|
||||
- If loss is flat or increasing, flag for review.
|
||||
|
||||
### Caption Consistency (Draft — Not Yet Calibrated)
|
||||
|
||||
Use an LLM to evaluate whether the video content matches the input prompt.
|
||||
|
||||
```
|
||||
Prompt: "A golden retriever playing in the snow"
|
||||
Video: <path>
|
||||
|
||||
Score the video on:
|
||||
1. Object presence (is there a golden retriever?)
|
||||
2. Action accuracy (is it playing?)
|
||||
3. Environment match (is there snow?)
|
||||
4. Overall coherence (does it look natural?)
|
||||
|
||||
Each 1-5, total /20.
|
||||
```
|
||||
|
||||
> ⚠️ This metric is in **draft** status. Results should not be treated as
|
||||
> ground truth until calibrated against human judgments.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Identify available metrics** — Check `.agents/memory/evaluation-registry/README.md`.
|
||||
2. **Run each metric** — Collect scores.
|
||||
3. **Aggregate** — Produce a combined quality report.
|
||||
4. **Log** — Update the experiment journal with quality results.
|
||||
|
||||
## Outputs
|
||||
|
||||
```markdown
|
||||
## Video Quality Report: <experiment_name>
|
||||
|
||||
| Metric | Score | Threshold | Status |
|
||||
|--------|-------|-----------|--------|
|
||||
| SSIM (avg) | 0.87 | > 0.80 | ✅ Pass |
|
||||
| Loss trajectory | decreasing | decreasing | ✅ Pass |
|
||||
| Caption consistency | 16/20 | > 14/20 | ✅ Pass |
|
||||
|
||||
### Per-Video Scores
|
||||
| Video | SSIM | Caption |
|
||||
|-------|------|---------|
|
||||
| video_001.mp4 | 0.89 | 17/20 |
|
||||
| video_002.mp4 | 0.85 | 15/20 |
|
||||
```
|
||||
|
||||
## References
|
||||
- `fastvideo/tests/ssim/` — SSIM test infrastructure
|
||||
- `fastvideo/tests/training/Vanilla/test_training_loss.py` — loss comparison
|
||||
- `.agents/memory/evaluation-registry/README.md` — metric catalog
|
||||
|
||||
## Changelog
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-02 | Initial version with SSIM, loss trajectory, caption consistency stub |
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: index-related-work
|
||||
description: Ingest a paper or repository into the related work index
|
||||
---
|
||||
|
||||
# Index Related Work
|
||||
|
||||
## Purpose
|
||||
Create a structured summary of a related paper, repository, or blog post and
|
||||
add it to `.agents/memory/related-work/` for future reference. This builds the
|
||||
agent's knowledge base for making informed decisions about training, evaluation,
|
||||
and architecture choices.
|
||||
|
||||
## Prerequisites
|
||||
- Access to the paper/repo (URL, PDF, or local clone).
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `source` | Yes | URL, citation, or local path |
|
||||
| `type` | Yes | `paper`, `repo`, or `blog` |
|
||||
| `tags` | No | List of tags (default: inferred from content) |
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Extract key information
|
||||
|
||||
For **papers**: Read abstract, method section, experimental setup, and results.
|
||||
For **repos**: Read README, key source files, and training scripts.
|
||||
For **blogs**: Read the full post.
|
||||
|
||||
Focus on:
|
||||
- What problem does it solve?
|
||||
- What architecture/technique is used?
|
||||
- How does it relate to FastVideo's approach?
|
||||
|
||||
### 2. Create the index entry
|
||||
|
||||
Write to `.agents/memory/related-work/<slug>.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: <title>
|
||||
source: <URL or citation>
|
||||
type: paper | repo | blog
|
||||
date_indexed: <ISO-8601>
|
||||
tags: [world-model, distillation, evaluation, ...]
|
||||
---
|
||||
|
||||
## Summary
|
||||
<1-2 paragraph summary.>
|
||||
|
||||
## Key Differences from FastVideo
|
||||
- <comparison points>
|
||||
|
||||
## Actionable Insights
|
||||
- <what we could adopt or adapt>
|
||||
```
|
||||
|
||||
### 3. Update the catalog
|
||||
|
||||
If `.agents/memory/related-work/_catalog.md` exists, append the new entry.
|
||||
If not, create it:
|
||||
|
||||
```markdown
|
||||
# Related Work Catalog
|
||||
|
||||
| Slug | Title | Type | Tags | Date |
|
||||
|------|-------|------|------|------|
|
||||
| <slug> | <title> | <type> | <tags> | <date> |
|
||||
```
|
||||
|
||||
## Outputs
|
||||
- New file in `.agents/memory/related-work/<slug>.md`.
|
||||
- Updated catalog.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Index the Self-Forcing paper:
|
||||
|
||||
source: https://arxiv.org/abs/2406.xxxxx
|
||||
type: paper
|
||||
tags: [world-model, self-forcing, distillation]
|
||||
```
|
||||
|
||||
## References
|
||||
- `.agents/memory/related-work/README.md` — schema documentation
|
||||
|
||||
## Changelog
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-02 | Initial version |
|
||||
@@ -0,0 +1,7 @@
|
||||
{"name": "launch-experiment", "description": "Generate and execute a training launch command for FastVideo models", "path": "launch-experiment/SKILL.md", "status": "draft", "trust": "low"}
|
||||
{"name": "monitor-experiment", "description": "Poll a running W&B training run for progress and emit structured alerts", "path": "monitor-experiment/SKILL.md", "status": "draft", "trust": "low"}
|
||||
{"name": "summarize-run", "description": "Extract a W&B run summary into a structured experiment report", "path": "summarize-run/SKILL.md", "status": "draft", "trust": "low"}
|
||||
{"name": "log-experiment", "description": "Append or update an experiment entry in the experiment journal", "path": "log-experiment/SKILL.md", "status": "draft", "trust": "low"}
|
||||
{"name": "evaluate-video-quality", "description": "Evaluate generated video quality using available metrics (SSIM, loss trajectory, caption consistency)", "path": "evaluate-video-quality/SKILL.md", "status": "draft", "trust": "low"}
|
||||
{"name": "index-related-work", "description": "Ingest a paper or repository into the related work index", "path": "index-related-work/SKILL.md", "status": "draft", "trust": "low"}
|
||||
{"name": "search-related-work", "description": "Query the related work index for relevant papers, repos, or comparisons", "path": "search-related-work/SKILL.md", "status": "draft", "trust": "low"}
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: launch-experiment
|
||||
description: Generate and execute a training launch command for FastVideo models
|
||||
---
|
||||
|
||||
# Launch Experiment
|
||||
|
||||
## Purpose
|
||||
Construct a fully-specified `torchrun` training command for a FastVideo model
|
||||
given a target pipeline, dataset, and hyperparameter overrides. This skill
|
||||
automates the boilerplate of setting environment variables, picking the right
|
||||
entrypoint, and applying defaults from the closest example script.
|
||||
|
||||
## Prerequisites
|
||||
- The repo is cloned and `fastvideo` is installed (`uv pip install -e .[dev]`).
|
||||
- Dataset is preprocessed (see `docs/training/data_preprocess.md`).
|
||||
- `WANDB_API_KEY` is set in the environment (or `WANDB_MODE=offline` for local).
|
||||
- GPU resources are available (multi-GPU requires NCCL).
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `pipeline` | Yes | Training pipeline type: `finetune`, `distill-dmd`, `self-forcing`, `lora`, `consistency` |
|
||||
| `model` | Yes | Model family: `wan-t2v-1.3B`, `wan-i2v-14B`, `ltx2`, `matrixgame` |
|
||||
| `data_path` | Yes | Path to preprocessed dataset (parquet) |
|
||||
| `num_gpus` | Yes | Number of GPUs |
|
||||
| `overrides` | No | Dict of hyperparameter overrides (any CLI arg) |
|
||||
| `output_dir` | No | Output directory (default: `outputs/<model>_<pipeline>`) |
|
||||
| `run_name` | No | W&B run name (default: auto-generated) |
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the training entrypoint
|
||||
|
||||
| Pipeline | Entrypoint |
|
||||
|----------|-----------|
|
||||
| `finetune` (Wan T2V) | `fastvideo/training/wan_training_pipeline.py` |
|
||||
| `finetune` (Wan I2V) | `fastvideo/training/wan_i2v_training_pipeline.py` |
|
||||
| `finetune` (LTX-2) | `fastvideo/training/ltx2_training_pipeline.py` |
|
||||
| `finetune` (MatrixGame) | `fastvideo/training/matrixgame_training_pipeline.py` |
|
||||
| `distill-dmd` | `fastvideo/training/wan_distillation_pipeline.py` |
|
||||
| `self-forcing` | `fastvideo/training/wan_self_forcing_distillation_pipeline.py` |
|
||||
|
||||
### 2. Resolve default hyperparameters
|
||||
|
||||
Find the closest example script in `examples/training/` for the model:
|
||||
|
||||
| Model | Example Script Directory |
|
||||
|-------|-------------------------|
|
||||
| `wan-t2v-1.3B` | `examples/training/finetune/wan_t2v_1.3B/crush_smol/` |
|
||||
| `wan-i2v-14B` | `examples/training/finetune/wan_i2v_14B_480p/crush_smol/` |
|
||||
| `ltx2` | `examples/training/finetune/ltx2/` |
|
||||
| `matrixgame` | `examples/training/finetune/MatrixGame2.0/` |
|
||||
| `distill-dmd` | `scripts/distill/v1_distill_dmd_wan.sh` |
|
||||
|
||||
Read the script to extract default values for:
|
||||
- `--learning_rate`, `--train_batch_size`, `--sp_size`, `--tp_size`
|
||||
- `--num_latent_t`, `--num_height`, `--num_width`, `--num_frames`
|
||||
- `--gradient_accumulation_steps`, `--max_train_steps`
|
||||
- `--mixed_precision`, `--weight_decay`, `--max_grad_norm`
|
||||
- `--validation_steps`, `--validation_sampling_steps`
|
||||
|
||||
### 3. Set environment variables
|
||||
|
||||
```bash
|
||||
export WANDB_API_KEY="${WANDB_API_KEY}"
|
||||
export WANDB_BASE_URL="https://api.wandb.ai"
|
||||
export FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN
|
||||
export TOKENIZERS_PARALLELISM=false
|
||||
export TRITON_CACHE_DIR=/tmp/triton_cache
|
||||
```
|
||||
|
||||
### 4. Construct the torchrun command
|
||||
|
||||
```bash
|
||||
torchrun --nnodes 1 --nproc_per_node <num_gpus> \
|
||||
<entrypoint> \
|
||||
--pretrained_model_name_or_path <model_hf_id> \
|
||||
--data_path "<data_path>" \
|
||||
--output_dir "<output_dir>" \
|
||||
--wandb_run_name "<run_name>" \
|
||||
--tracker_project_name "<project_name>" \
|
||||
--log_validation \
|
||||
<...all hyperparameters...>
|
||||
```
|
||||
|
||||
### 5. Log to experiment journal
|
||||
|
||||
After launching, append an entry to `.agents/memory/experiment-journal/README.md`:
|
||||
|
||||
```markdown
|
||||
## [YYYY-MM-DD] Experiment: <run_name>
|
||||
- **Hypothesis**: <user-provided or auto-generated>
|
||||
- **Config**: model=<model>, lr=<lr>, sp_size=<sp>, gpus=<n>, script=<entrypoint>
|
||||
- **W&B run**: <pending — will be updated by monitor skill>
|
||||
- **Status**: running
|
||||
```
|
||||
|
||||
## Outputs
|
||||
- A ready-to-execute shell command.
|
||||
- An experiment journal entry.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Launch a Wan T2V 1.3B finetune on 4 GPUs with lr=5e-5 and max_train_steps=1000:
|
||||
|
||||
pipeline: finetune
|
||||
model: wan-t2v-1.3B
|
||||
data_path: data/crush_smol_preprocessed/
|
||||
num_gpus: 4
|
||||
overrides:
|
||||
learning_rate: 5e-5
|
||||
max_train_steps: 1000
|
||||
```
|
||||
|
||||
## References
|
||||
- `examples/training/finetune/wan_t2v_1.3B/crush_smol/finetune_t2v.sh`
|
||||
- `scripts/distill/v1_distill_dmd_wan.sh`
|
||||
- `docs/training/finetune.md` (training arguments table)
|
||||
- `fastvideo/training/trackers.py` (tracker initialization)
|
||||
|
||||
## Changelog
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-02 | Initial version |
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: log-experiment
|
||||
description: Append or update an experiment entry in the experiment journal
|
||||
---
|
||||
|
||||
# Log Experiment
|
||||
|
||||
## Purpose
|
||||
Create or update an entry in `.agents/memory/experiment-journal/README.md` to maintain
|
||||
a living record of all experiments and their outcomes.
|
||||
|
||||
## Prerequisites
|
||||
- `.agents/memory/experiment-journal/README.md` exists.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `name` | Yes | Experiment name / identifier |
|
||||
| `hypothesis` | No | What you expected to learn |
|
||||
| `config` | Yes | Key config: model, lr, sp_size, gpus, script |
|
||||
| `wandb_run` | No | W&B run ID or URL |
|
||||
| `duration` | No | Total wall time |
|
||||
| `metrics` | No | Key metrics dict (loss, step_time, grad_norm) |
|
||||
| `checkpoint` | No | Path to checkpoint |
|
||||
| `insight` | No | What was learned |
|
||||
| `status` | Yes | `running`, `completed`, `failed`, `abandoned` |
|
||||
| `lessons` | No | Paths to related lesson files |
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Check for existing entry
|
||||
|
||||
Search `.agents/memory/experiment-journal/README.md` for an entry with the same name.
|
||||
If found, update it instead of creating a duplicate.
|
||||
|
||||
### 2. Format the entry
|
||||
|
||||
```markdown
|
||||
## [YYYY-MM-DD] Experiment: <name>
|
||||
- **Hypothesis**: <hypothesis or "N/A">
|
||||
- **Config**: model=<model>, lr=<lr>, sp_size=<sp>, gpus=<n>, script=<script>
|
||||
- **W&B run**: <wandb_run or "pending">
|
||||
- **Duration**: <duration or "in progress">
|
||||
- **Key metrics**: loss=<loss>, step_time=<step_time>, grad_norm=<grad_norm>
|
||||
- **Checkpoint**: <checkpoint or "N/A">
|
||||
- **Insight**: <insight or "pending">
|
||||
- **Status**: <status>
|
||||
- **Related lessons**: <lessons or "none">
|
||||
```
|
||||
|
||||
### 3. Insert at the top of the journal
|
||||
|
||||
New entries go at the top of the file (after the header), so the most recent
|
||||
experiments are always visible first.
|
||||
|
||||
### 4. Warn on duplicates
|
||||
|
||||
If a similar experiment name exists with `status: completed`, warn that this
|
||||
may be a repeat. If it's `status: running`, assume this is an update.
|
||||
|
||||
## Outputs
|
||||
- Updated `.agents/memory/experiment-journal/README.md`.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Log a completed experiment:
|
||||
|
||||
name: wan-t2v-finetune-lr5e5-sp4
|
||||
config: model=wan-t2v-1.3B, lr=5e-5, sp_size=4, gpus=4
|
||||
wandb_run: fastvideo/training/run_abc123
|
||||
duration: 2h 15m
|
||||
metrics: {loss: 0.065, step_time: 2.3, grad_norm: 0.35}
|
||||
checkpoint: outputs/wan_finetune/checkpoint-1000
|
||||
insight: LR 5e-5 converges 30% faster than 1e-5 with no quality loss
|
||||
status: completed
|
||||
```
|
||||
|
||||
## References
|
||||
- `.agents/memory/experiment-journal/README.md` — journal file
|
||||
- `.agents/workflows/experiment-lifecycle.md` — when to log
|
||||
|
||||
## Changelog
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-02 | Initial version |
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: monitor-experiment
|
||||
description: Poll a running W&B training run for progress and emit structured alerts
|
||||
---
|
||||
|
||||
# Monitor Experiment
|
||||
|
||||
## Purpose
|
||||
Continuously (or on-demand) check a running experiment's W&B metrics and emit
|
||||
alerts for anomalies. Supports the "30-minute quality check" paradigm: after
|
||||
the first 30 minutes of a long training run, produce a checkpoint quality
|
||||
report before committing more resources.
|
||||
|
||||
## Prerequisites
|
||||
- `WANDB_API_KEY` is set in the environment.
|
||||
- The experiment is actively logging to W&B (not in `WANDB_MODE=offline`).
|
||||
- For offline mode: read from local `wandb-summary.json` instead.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `run_id` | Yes* | W&B run ID (e.g., `entity/project/run_id`) |
|
||||
| `output_dir` | Yes* | Local output directory (for offline mode fallback) |
|
||||
| `poll_interval` | No | Seconds between polls (default: 60) |
|
||||
| `alert_on` | No | List of alert conditions to enable (default: all) |
|
||||
|
||||
\* One of `run_id` or `output_dir` is required.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Connect to the run
|
||||
|
||||
**Online mode** (preferred):
|
||||
|
||||
```python
|
||||
import wandb
|
||||
api = wandb.Api()
|
||||
run = api.run("<run_id>")
|
||||
```
|
||||
|
||||
**Offline fallback**:
|
||||
|
||||
```python
|
||||
import json
|
||||
summary_path = f"{output_dir}/tracker/wandb/latest-run/files/wandb-summary.json"
|
||||
with open(summary_path) as f:
|
||||
summary = json.load(f)
|
||||
```
|
||||
|
||||
### 2. Track key metrics
|
||||
|
||||
| Metric | W&B Key | Description |
|
||||
|--------|---------|-------------|
|
||||
| Training loss | `train_loss` | Primary training loss |
|
||||
| Gradient norm | `grad_norm` | Gradient magnitude |
|
||||
| Step time | `step_time` | Wall-clock seconds per step |
|
||||
| Learning rate | `learning_rate` | Current LR |
|
||||
| Avg step time | `avg_step_time` | Running average step time |
|
||||
| Validation videos | `validation_videos_*` | Generated validation samples |
|
||||
|
||||
### 3. Evaluate alert conditions
|
||||
|
||||
| Alert | Condition | Severity |
|
||||
|-------|-----------|----------|
|
||||
| **Loss spike** | `current_loss > 3 × rolling_avg_loss` | 🔴 Critical |
|
||||
| **NaN/Inf gradient** | `grad_norm` is NaN or Inf | 🔴 Critical |
|
||||
| **Step time regression** | `step_time > 2 × baseline_step_time` | 🟡 Warning |
|
||||
| **No progress** | No new W&B logs for > 10 minutes | 🟡 Warning |
|
||||
| **Loss plateau** | Loss change < 1% over last 100 steps | 🟢 Info |
|
||||
|
||||
### 4. Emit structured status
|
||||
|
||||
Output format (agent-consumable):
|
||||
|
||||
```json
|
||||
{
|
||||
"run_id": "...",
|
||||
"step": 500,
|
||||
"metrics": {
|
||||
"train_loss": 0.078,
|
||||
"grad_norm": 0.41,
|
||||
"step_time": 2.5,
|
||||
"learning_rate": 1e-6
|
||||
},
|
||||
"alerts": [
|
||||
{"type": "loss_spike", "severity": "critical", "message": "Loss jumped to 0.45 (avg: 0.08)"}
|
||||
],
|
||||
"status": "running"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 30-Minute Quality Check
|
||||
|
||||
After the first 30 minutes of wall-clock time:
|
||||
1. Summarize the loss curve shape (decreasing? at what rate?).
|
||||
2. Check if validation videos have been generated.
|
||||
3. Report step count, loss at start vs. current, and estimated time to completion.
|
||||
4. Produce a go/no-go recommendation.
|
||||
|
||||
```markdown
|
||||
## 30-Minute Check: <run_name>
|
||||
- **Steps completed**: 150
|
||||
- **Loss**: 0.12 → 0.08 (↓ 33%)
|
||||
- **Grad norm**: stable at ~0.4
|
||||
- **Step time**: 2.5s/step (consistent)
|
||||
- **Validation videos**: 5 generated at step 100
|
||||
- **Recommendation**: ✅ Continue — loss is decreasing normally
|
||||
```
|
||||
|
||||
## Outputs
|
||||
- Structured JSON status updates.
|
||||
- Alert messages for anomalous conditions.
|
||||
- 30-minute checkpoint quality report.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Monitor W&B run "fastvideo/Wan_distillation/abc123":
|
||||
|
||||
run_id: fastvideo/Wan_distillation/abc123
|
||||
poll_interval: 120
|
||||
alert_on: [loss_spike, nan_gradient, step_time_regression]
|
||||
```
|
||||
|
||||
## References
|
||||
- `fastvideo/training/trackers.py` — `WandbTracker` implementation
|
||||
- `fastvideo/tests/training/Vanilla/test_training_loss.py` — how summaries are compared
|
||||
- `fastvideo/tests/training/Vanilla/a40_reference_wandb_summary.json` — reference summary format
|
||||
|
||||
## Changelog
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-02 | Initial version |
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: search-related-work
|
||||
description: Query the related work index for relevant papers, repos, or comparisons
|
||||
---
|
||||
|
||||
# Search Related Work
|
||||
|
||||
## Purpose
|
||||
Search through `.agents/memory/related-work/` to find indexed papers, repos,
|
||||
or blog posts relevant to a query. Use this when you need to understand how
|
||||
other work compares to FastVideo's approach, or when looking for techniques
|
||||
to adopt.
|
||||
|
||||
## Prerequisites
|
||||
- The related work index has entries (`.agents/memory/related-work/*.md`).
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `query` | Yes | Natural language query |
|
||||
| `tags` | No | Filter by tags (e.g., `[distillation, evaluation]`) |
|
||||
| `type` | No | Filter by type (`paper`, `repo`, `blog`) |
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Search the index
|
||||
|
||||
Use grep-based search through `.agents/memory/related-work/`:
|
||||
|
||||
```bash
|
||||
# Search by content
|
||||
grep -rl "<query>" .agents/memory/related-work/
|
||||
|
||||
# Search by tags (in frontmatter)
|
||||
grep -l "tags:.*<tag>" .agents/memory/related-work/*.md
|
||||
```
|
||||
|
||||
### 2. Rank results
|
||||
|
||||
For each matching file:
|
||||
1. Read the file.
|
||||
2. Score relevance to the query based on:
|
||||
- Title match
|
||||
- Tag match
|
||||
- Content match (summary, differences, insights)
|
||||
3. Return top results.
|
||||
|
||||
### 3. Format output
|
||||
|
||||
```markdown
|
||||
## Related Work Search: "<query>"
|
||||
|
||||
### 1. <Title> (relevance: high)
|
||||
- **Source**: <URL>
|
||||
- **Tags**: <tags>
|
||||
- **Key insight**: <most relevant excerpt>
|
||||
- **File**: `.agents/memory/related-work/<slug>.md`
|
||||
|
||||
### 2. <Title> (relevance: medium)
|
||||
...
|
||||
```
|
||||
|
||||
## Outputs
|
||||
- Ranked list of relevant related work entries with excerpts.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Search for work related to video quality evaluation metrics:
|
||||
|
||||
query: "video generation quality evaluation metrics"
|
||||
tags: [evaluation]
|
||||
```
|
||||
|
||||
## References
|
||||
- `.agents/memory/related-work/README.md` — index schema
|
||||
|
||||
## Changelog
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-02 | Initial version |
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
name: summarize-run
|
||||
description: Extract a W&B run summary into a structured experiment report
|
||||
---
|
||||
|
||||
# Summarize Run
|
||||
|
||||
## Purpose
|
||||
After a training run completes (or at any checkpoint), extract key metrics from
|
||||
the W&B run summary and produce a structured markdown report. Supports both
|
||||
online (W&B API) and offline (local `wandb-summary.json`) modes.
|
||||
|
||||
## Prerequisites
|
||||
- Run has completed or reached a checkpoint with a saved summary.
|
||||
- For online: `WANDB_API_KEY` set in environment.
|
||||
- For offline: access to `<output_dir>/tracker/wandb/latest-run/files/wandb-summary.json`.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `run_id` | Yes* | W&B run ID for online access |
|
||||
| `output_dir` | Yes* | Local output dir for offline access |
|
||||
| `reference_run` | No | Path to reference `wandb-summary.json` for comparison |
|
||||
| `experiment_name` | No | Name for the journal entry (default: from W&B) |
|
||||
|
||||
\* One of `run_id` or `output_dir` is required.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load run summary
|
||||
|
||||
**Online**:
|
||||
|
||||
```python
|
||||
import wandb
|
||||
api = wandb.Api()
|
||||
run = api.run("<run_id>")
|
||||
summary = dict(run.summary)
|
||||
config = dict(run.config)
|
||||
```
|
||||
|
||||
**Offline** (existing codebase pattern from `fastvideo/tests/training/`):
|
||||
|
||||
```python
|
||||
import json
|
||||
summary_path = f"{output_dir}/tracker/wandb/latest-run/files/wandb-summary.json"
|
||||
with open(summary_path) as f:
|
||||
summary = json.load(f)
|
||||
```
|
||||
|
||||
### 2. Extract key fields
|
||||
|
||||
| Field | Source | Description |
|
||||
|-------|--------|-------------|
|
||||
| `train_loss` | `summary["train_loss"]` | Final training loss |
|
||||
| `avg_step_time` | `summary["avg_step_time"]` | Average seconds per step |
|
||||
| `step_time` | `summary["step_time"]` | Last step time |
|
||||
| `grad_norm` | `summary["grad_norm"]` | Final gradient norm |
|
||||
| `learning_rate` | `summary["learning_rate"]` | Final LR |
|
||||
| `_step` | `summary["_step"]` | Total steps completed |
|
||||
| `_runtime` | `summary["_runtime"]` | Total wall-clock seconds |
|
||||
| `validation_videos_*` | `summary[key]` | Validation video artifacts |
|
||||
|
||||
### 3. Compare against reference (optional)
|
||||
|
||||
Follow the pattern in `fastvideo/tests/training/Vanilla/test_training_loss.py`:
|
||||
|
||||
```python
|
||||
# Fields to compare
|
||||
compare_fields = ["train_loss", "grad_norm", "avg_step_time"]
|
||||
tolerance = 0.05 # 5% relative tolerance
|
||||
|
||||
for field in compare_fields:
|
||||
ref_val = reference_summary[field]
|
||||
cur_val = summary[field]
|
||||
diff_pct = abs(cur_val - ref_val) / abs(ref_val) * 100
|
||||
status = "✅" if diff_pct < tolerance * 100 else "⚠️"
|
||||
print(f"{status} {field}: {cur_val:.4f} (ref: {ref_val:.4f}, diff: {diff_pct:.1f}%)")
|
||||
```
|
||||
|
||||
### 4. Generate report
|
||||
|
||||
```markdown
|
||||
# Run Summary: <experiment_name>
|
||||
|
||||
| Metric | Value | Reference | Diff |
|
||||
|--------|-------|-----------|------|
|
||||
| Train Loss | 0.0788 | 0.0800 | -1.5% ✅ |
|
||||
| Avg Step Time | 2.81s | 2.80s | +0.4% ✅ |
|
||||
| Grad Norm | 0.408 | 0.410 | -0.5% ✅ |
|
||||
| Total Steps | 500 | — | — |
|
||||
| Wall Time | 23m 30s | — | — |
|
||||
|
||||
## Configuration
|
||||
- Model: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
- Learning Rate: 1e-6
|
||||
- Batch Size: 1
|
||||
- GPUs: 8 × (SP=1, TP=1)
|
||||
- Mixed Precision: bf16
|
||||
|
||||
## Validation Videos
|
||||
<list of validation video paths if available>
|
||||
|
||||
## Notes
|
||||
<any observations or anomalies>
|
||||
```
|
||||
|
||||
### 5. Update experiment journal
|
||||
|
||||
Append or update the experiment's entry in `.agents/memory/experiment-journal/README.md`
|
||||
with the final metrics and status.
|
||||
|
||||
## Outputs
|
||||
- Structured markdown report.
|
||||
- Updated experiment journal entry.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Summarize the run in output directory "outputs/wan_finetune":
|
||||
|
||||
output_dir: outputs/wan_finetune
|
||||
reference_run: fastvideo/tests/training/Vanilla/a40_reference_wandb_summary.json
|
||||
experiment_name: wan-t2v-finetune-lr1e6
|
||||
```
|
||||
|
||||
## References
|
||||
- `fastvideo/tests/training/Vanilla/test_training_loss.py` — reference comparison pattern
|
||||
- `fastvideo/tests/training/Vanilla/a40_reference_wandb_summary.json` — example summary
|
||||
- `fastvideo/tests/training/lora/test_lora_training.py` — LoRA summary comparison
|
||||
- `fastvideo/training/trackers.py` — tracker summary generation
|
||||
|
||||
## Changelog
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-02 | Initial version |
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: How to develop, validate, and register a new evaluation metric
|
||||
---
|
||||
|
||||
# Evaluation Development SOP
|
||||
|
||||
Standard procedure for adding new video quality evaluation metrics to the
|
||||
FastVideo agent toolkit.
|
||||
|
||||
## When to Use
|
||||
|
||||
- You need a metric that doesn't exist in `.agents/memory/evaluation-registry/README.md`.
|
||||
- An existing metric needs significant changes to its methodology.
|
||||
- You're exploring a new evaluation approach.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Research
|
||||
|
||||
- Search `.agents/memory/related-work/` for existing evaluation approaches.
|
||||
- Check the `evaluation_registry.md` for current metrics and their limitations.
|
||||
- Review literature: FVD, CLIP-Score, human preference, etc.
|
||||
|
||||
### 2. Prototype
|
||||
|
||||
- Write a standalone script in `.agents/exploration/<metric-name>.md`.
|
||||
- Keep it simple: one script, minimal dependencies.
|
||||
- Test on a few known-good and known-bad video samples.
|
||||
|
||||
### 3. Validate
|
||||
|
||||
- **Known-good test**: Metric should score high on reference-quality videos.
|
||||
- **Known-bad test**: Metric should score low on degraded/unrelated videos.
|
||||
- **Sensitivity test**: Small quality differences should produce meaningful
|
||||
score differences.
|
||||
- Document thresholds and their justification.
|
||||
|
||||
### 4. Register
|
||||
|
||||
Update `.agents/memory/evaluation-registry/README.md`:
|
||||
- Add the metric with status `Active`.
|
||||
- Document location, thresholds, and trust level.
|
||||
|
||||
### 5. Integrate
|
||||
|
||||
Update `.agents/skills/evaluate-video-quality.md`:
|
||||
- Add the new metric as a section.
|
||||
- Include code examples and interpretation guide.
|
||||
|
||||
### 6. Document
|
||||
|
||||
- Move the exploration log content into the skill.
|
||||
- Clean up the exploration file or mark it as `promoted`.
|
||||
- If anything went wrong during development, create a lesson.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
description: When and how to log experiments in the experiment journal
|
||||
---
|
||||
|
||||
# Experiment Journaling SOP
|
||||
|
||||
Ensures every experiment is properly recorded with context and outcomes.
|
||||
|
||||
## When to Log
|
||||
|
||||
**Always.** Every experiment — even quick tests — should be journaled.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Before Launch — Create Draft Entry
|
||||
|
||||
Use the `log-experiment` skill with `status: running`:
|
||||
- Include hypothesis and config.
|
||||
- Leave metrics, duration, and insight blank.
|
||||
|
||||
### 2. After 30-Minute Check — Update with Initial Metrics
|
||||
|
||||
Update the entry with:
|
||||
- Current loss and its trajectory direction.
|
||||
- Step time.
|
||||
- Number of validation videos generated.
|
||||
- Preliminary go/no-go assessment.
|
||||
|
||||
### 3. On Completion — Fill Final Entry
|
||||
|
||||
Update the entry with `status: completed`:
|
||||
- Final loss, grad norm, avg step time.
|
||||
- Total duration and steps.
|
||||
- Checkpoint path.
|
||||
- Key insight.
|
||||
|
||||
### 4. On Failure — Document Failure Mode
|
||||
|
||||
Update the entry with `status: failed`:
|
||||
- What went wrong (OOM, NaN, crash, etc.).
|
||||
- At what step the failure occurred.
|
||||
- Create a lesson in `.agents/lessons/` for non-trivial failures.
|
||||
|
||||
### 5. Cross-Reference
|
||||
|
||||
- Link related lessons: `**Related lessons**: .agents/lessons/<filename>.md`
|
||||
- Link related experiments: if this is a follow-up, reference the prior entry.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
description: End-to-end experiment lifecycle from hypothesis to lessons learned
|
||||
---
|
||||
|
||||
# Experiment Lifecycle SOP
|
||||
|
||||
Standard operating procedure for running ML training experiments on
|
||||
FastVideo-WorldModel. Every experiment should follow this flow.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
Plan → Launch → Monitor → Summarize → Journal → Reflect
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Plan the Experiment
|
||||
|
||||
Before launching:
|
||||
- [ ] Define a clear **hypothesis** (what you expect to learn).
|
||||
- [ ] Select the **model** and **pipeline** type (finetune, distill, lora, etc.).
|
||||
- [ ] Prepare the **dataset** (preprocessed into parquet format).
|
||||
- [ ] Review existing experiments in `.agents/memory/experiment-journal/README.md` for related work.
|
||||
- [ ] Check `.agents/lessons/` for known pitfalls with this configuration.
|
||||
- [ ] Document the plan in the experiment journal as a draft entry.
|
||||
|
||||
### 2. Launch the Experiment
|
||||
|
||||
Use the `launch-experiment` skill:
|
||||
- Provide: pipeline, model, data_path, num_gpus, and any hyperparameter overrides.
|
||||
- The skill generates the `torchrun` command and creates a journal entry.
|
||||
- Verify the command looks correct before executing.
|
||||
|
||||
Reference: `.agents/skills/launch-experiment.md`
|
||||
|
||||
### 3. Monitor the Experiment
|
||||
|
||||
Use the `monitor-experiment` skill:
|
||||
- Provide the W&B run ID (or output_dir for offline).
|
||||
- Monitor alerts: loss spikes, NaN gradients, step time regressions.
|
||||
- At the **30-minute mark**: perform the quality check.
|
||||
- Is loss decreasing?
|
||||
- Are validation videos reasonable?
|
||||
- Is step time consistent?
|
||||
- **Decision point**: Continue or abort based on the 30-min check.
|
||||
|
||||
Reference: `.agents/skills/monitor-experiment.md`
|
||||
|
||||
### 4. Summarize the Run
|
||||
|
||||
After completion (or at any checkpoint), use the `summarize-run` skill:
|
||||
- Extract final metrics from W&B summary.
|
||||
- Compare against reference runs if available.
|
||||
- Generate a structured report.
|
||||
|
||||
Reference: `.agents/skills/summarize-run.md`
|
||||
|
||||
### 5. Update the Experiment Journal
|
||||
|
||||
Use the `log-experiment` skill to update the journal entry:
|
||||
- Fill in final metrics, duration, checkpoint paths.
|
||||
- Record the key insight learned.
|
||||
- Set status to `completed`, `failed`, or `abandoned`.
|
||||
|
||||
Reference: `.agents/skills/log-experiment.md`
|
||||
|
||||
### 6. Reflect and Capture Lessons
|
||||
|
||||
After every experiment:
|
||||
- **What went right?** → Note in the journal insight field.
|
||||
- **What went wrong?** → Create a lesson in `.agents/lessons/`:
|
||||
- Use the template in `.agents/lessons/README.md`.
|
||||
- Cross-reference the experiment journal entry.
|
||||
- **What was surprising?** → Consider creating an exploration log if this
|
||||
warrants further investigation.
|
||||
|
||||
Reference: `.agents/workflows/lesson-capture.md`
|
||||
|
||||
## Validation Criteria
|
||||
|
||||
This SOP is validated when an agent can:
|
||||
1. Follow steps 1–6 end-to-end for a minimal training run
|
||||
(e.g., `examples/training/finetune/wan_t2v_1.3B/crush_smol/finetune_t2v.sh`
|
||||
with `--max_train_steps 5`).
|
||||
2. Produce a complete experiment journal entry.
|
||||
3. Generate a run summary report.
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
description: Post-experiment reflection to capture lessons learned
|
||||
---
|
||||
|
||||
# Lesson Capture SOP
|
||||
|
||||
Systematic procedure for turning experiment outcomes into persistent knowledge.
|
||||
|
||||
## When to Use
|
||||
|
||||
After **every** completed or failed experiment. Even successful experiments
|
||||
can yield lessons (e.g., "LR 5e-5 works better than 1e-5 for LoRA").
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Review the Experiment
|
||||
|
||||
Read the experiment journal entry. Ask:
|
||||
- Did anything go wrong?
|
||||
- Was anything surprising?
|
||||
- Did anything take longer than expected?
|
||||
- Was a workaround needed?
|
||||
|
||||
### 2. Decide: Lesson or Not?
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Something broke | Create a lesson (category: `infrastructure` or `data`) |
|
||||
| Hyperparameter choice mattered | Create a lesson (category: `hyperparameter`) |
|
||||
| Porting issue found | Create a lesson (category: `porting`) |
|
||||
| Evaluation metric was misleading | Create a lesson (category: `evaluation`) |
|
||||
| Everything went smoothly | No lesson needed, but note in the journal insight |
|
||||
|
||||
### 3. Create the Lesson File
|
||||
|
||||
In `.agents/lessons/`, create `<YYYY-MM-DD>_<short-slug>.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: <ISO-8601>
|
||||
experiment: <journal entry reference>
|
||||
category: hyperparameter | data | infrastructure | evaluation | porting
|
||||
severity: critical | important | minor
|
||||
---
|
||||
|
||||
# <Short Descriptive Title>
|
||||
|
||||
## What Happened
|
||||
<description>
|
||||
|
||||
## Root Cause
|
||||
<analysis>
|
||||
|
||||
## Fix / Workaround
|
||||
<resolution>
|
||||
|
||||
## Prevention
|
||||
<how to avoid in future>
|
||||
```
|
||||
|
||||
### 4. Cross-Reference
|
||||
|
||||
- Update the experiment journal entry with a link to the lesson file.
|
||||
- If a similar lesson already exists, add a reference or update it.
|
||||
|
||||
### 5. Periodic Pattern Review
|
||||
|
||||
Every ~10 lessons, scan for patterns:
|
||||
- Multiple lessons in the same category → consider a new skill or SOP.
|
||||
- Repeated mistakes → strengthen the relevant SOP with a checklist item.
|
||||
- Infrastructure issues → propose a codebase fix.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: Synchronize the STATUS.md dashboard by scanning .agents/ directories
|
||||
---
|
||||
|
||||
# Sync Dashboard
|
||||
|
||||
Updates `.agents/STATUS.md` by scanning the skills, workflows, memory, lessons,
|
||||
and exploration directories to reflect what actually exists on disk.
|
||||
|
||||
## When to Use
|
||||
|
||||
- After adding, removing, or renaming any file in `.agents/`.
|
||||
- Periodically (e.g., at end of each conversation session).
|
||||
- When the dashboard feels out of date.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Scan directories
|
||||
|
||||
List all files in each directory:
|
||||
|
||||
```bash
|
||||
echo "=== Skills ==="
|
||||
ls -1 .agents/skills/*.md 2>/dev/null | grep -v SKILL_TEMPLATE
|
||||
|
||||
echo "=== Workflows ==="
|
||||
ls -1 .agents/workflows/*.md 2>/dev/null
|
||||
|
||||
echo "=== Memory ==="
|
||||
ls -1 .agents/memory/*.md 2>/dev/null
|
||||
ls -1 .agents/memory/related-work/*.md 2>/dev/null | grep -v README
|
||||
|
||||
echo "=== Lessons ==="
|
||||
ls -1 .agents/lessons/*.md 2>/dev/null | grep -v README
|
||||
|
||||
echo "=== Exploration ==="
|
||||
ls -1 .agents/exploration/*.md 2>/dev/null | grep -v README
|
||||
```
|
||||
|
||||
### 2. Compare with STATUS.md
|
||||
|
||||
For each file found:
|
||||
- If it's in STATUS.md → leave it (preserve status/trust/tested fields).
|
||||
- If it's NOT in STATUS.md → add it with status `🔴 Stub`, trust `None`, tested `❌`.
|
||||
|
||||
For each entry in STATUS.md:
|
||||
- If the file no longer exists → mark it as `❌ Removed` or delete the row.
|
||||
|
||||
### 3. Update counts
|
||||
|
||||
Recalculate the summary table at the top:
|
||||
- Count files per category.
|
||||
- Count by status (Ready, Draft, Stub).
|
||||
|
||||
### 4. Update timestamp
|
||||
|
||||
Set `_Last synced: <current date>_` at the top of STATUS.md.
|
||||
|
||||
### 5. Review
|
||||
|
||||
Read through the updated STATUS.md for accuracy. Flag anything that looks wrong.
|
||||
|
||||
## Notes
|
||||
|
||||
- Do NOT change trust levels during sync — those are set manually after testing.
|
||||
- Do NOT change status during sync — status changes require actual validation.
|
||||
- This workflow only handles structural sync (file existence), not content review.
|
||||
@@ -40,3 +40,17 @@
|
||||
- test evidence (`pytest`/SSIM outputs or rationale if skipped),
|
||||
- linked issue/PR context,
|
||||
- screenshots or sample outputs for UI/demo/docs changes.
|
||||
|
||||
## Agent Infrastructure
|
||||
|
||||
This repository is agent-friendly. Before doing any work, read:
|
||||
|
||||
1. `.agents/onboarding/README.md` — full onboarding guide with step-by-step instructions.
|
||||
2. `.agents/memory/codebase-map/README.md` — structural index of the entire repository.
|
||||
3. `.agents/skills/` — available agent skills (check if one exists before writing code).
|
||||
4. `.agents/workflows/` — SOPs for common procedures (experiment lifecycle, evaluation, etc.).
|
||||
5. `.agents/lessons/` — known pitfalls and their documented fixes.
|
||||
|
||||
If you are exploring a new procedure that has no existing SOP, document your
|
||||
progress in `.agents/exploration/` and flag it for review at the end of your
|
||||
session.
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
# Training Architecture
|
||||
|
||||
!!! warning "Work in Progress"
|
||||
This training architecture (`fastvideo/train/`) is under active development
|
||||
and is replacing the older `fastvideo/training/` module. APIs, config
|
||||
formats, and supported methods may change. See the
|
||||
[Current Status](#current-status) section for what is implemented so far.
|
||||
|
||||
FastVideo's training framework (`fastvideo/train/`) is built around a
|
||||
**pluggable, YAML-driven architecture** that cleanly separates **models**,
|
||||
**training methods**, and **infrastructure** into independent, composable
|
||||
layers. A single YAML config file is all that is needed to train any supported
|
||||
model with any supported algorithm — no code changes required to mix and match.
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
Training video diffusion models involves a tangle of concerns: model loading,
|
||||
noise scheduling, distillation algorithms, distributed strategies,
|
||||
checkpointing, and validation. Existing training scripts tend to hard-wire
|
||||
these together, making it painful to:
|
||||
|
||||
1. **Try a new distillation algorithm** on an existing model (requires forking
|
||||
the training loop).
|
||||
2. **Add a new model** to an existing algorithm (requires re-implementing
|
||||
boilerplate).
|
||||
3. **Switch distributed strategies** (FSDP, TP, SP) without touching algorithm
|
||||
code.
|
||||
4. **Resume, checkpoint, and validate** uniformly across all combinations.
|
||||
|
||||
The training framework solves this by making each axis of variation an
|
||||
independent plugin.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
YAML Config
|
||||
|
|
||||
v
|
||||
+------------------+ +---------------------+ +------------------+
|
||||
| Models Layer | | Methods Layer | | Infrastructure |
|
||||
| (per-role) | | (algorithm) | | Layer |
|
||||
| | | | | |
|
||||
| - ModelBase |<----| - TrainingMethod |---->| - Trainer |
|
||||
| - CausalModelBase| | - single_train_step| | - Callbacks |
|
||||
| | | - backward | | - Checkpoint |
|
||||
| Roles: | | - optimizers | | - Tracker (W&B) |
|
||||
| student | | | | - Dataloader |
|
||||
| teacher | | Algorithms: | | |
|
||||
| critic | | DMD2, SelfForcing, | | Distributed: |
|
||||
| | | SFT, DFSFT | | HSDP, TP, SP |
|
||||
+------------------+ +---------------------+ +------------------+
|
||||
```
|
||||
|
||||
### Three Layers
|
||||
|
||||
| Layer | Responsibility | Extension point |
|
||||
|-------|---------------|-----------------|
|
||||
| **Models** (`fastvideo/train/models/`) | Load transformer + scheduler, define `predict_noise`, `predict_x0`, `add_noise`, `backward`. Each training role (student/teacher/critic) is an independent instance. | Subclass `ModelBase` (or `CausalModelBase` for streaming). |
|
||||
| **Methods** (`fastvideo/train/methods/`) | Implement the training algorithm: own role models, define `single_train_step` + `backward`, manage optimizers/schedulers. | Subclass `TrainingMethod`. |
|
||||
| **Infrastructure** (`fastvideo/train/trainer.py`, `utils/`, `callbacks/`) | Training loop, gradient accumulation, distributed setup, checkpointing (DCP), W&B tracking, validation, EMA, grad clipping. | Add callbacks; everything else is shared. |
|
||||
|
||||
---
|
||||
|
||||
## YAML-Driven Configuration
|
||||
|
||||
Everything is configured declaratively. The `_target_` field selects the Python
|
||||
class to instantiate:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
teacher:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: false
|
||||
critic:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.distribution_matching.dmd2.DMD2Method
|
||||
rollout_mode: simulate
|
||||
dmd_denoising_steps: [1000, 850, 700, 550, 350, 275, 200, 125]
|
||||
generator_update_interval: 5
|
||||
real_score_guidance_scale: 3.5
|
||||
# ...
|
||||
|
||||
training:
|
||||
distributed: { num_gpus: 8, sp_size: 1, tp_size: 1 }
|
||||
data: { data_path: ..., num_latent_t: 20, num_frames: 77 }
|
||||
optimizer: { learning_rate: 2.0e-6, betas: [0.0, 0.999] }
|
||||
loop: { max_train_steps: 4000 }
|
||||
checkpoint: { output_dir: outputs/my_run }
|
||||
|
||||
callbacks:
|
||||
grad_clip: { max_grad_norm: 1.0 }
|
||||
validation: { pipeline_target: ..., every_steps: 100 }
|
||||
```
|
||||
|
||||
To switch from DMD2 to SFT, change the `method._target_` and remove the
|
||||
teacher/critic — no code changes needed.
|
||||
|
||||
---
|
||||
|
||||
## Model Abstraction
|
||||
|
||||
### `ModelBase` — Standard (Bidirectional) Models
|
||||
|
||||
Every role gets its own `ModelBase` instance owning a `transformer` and
|
||||
`noise_scheduler`. The base class defines:
|
||||
|
||||
- **`prepare_batch()`** — Convert raw dataloader output into forward-ready
|
||||
`TrainingBatch`.
|
||||
- **`add_noise()`** — Apply forward-process noise at a given timestep.
|
||||
- **`predict_noise()` / `predict_x0()`** — Run the transformer and return
|
||||
predictions.
|
||||
- **`backward()`** — Backward pass that restores forward context (attention
|
||||
metadata, timesteps).
|
||||
- **`init_preprocessors()`** — Lazy-load VAE, build dataloader (called only on
|
||||
the student).
|
||||
|
||||
### `CausalModelBase` — Streaming / Causal Models
|
||||
|
||||
Extends `ModelBase` with streaming inference primitives for causal video
|
||||
generation:
|
||||
|
||||
```python
|
||||
class CausalModelBase(ModelBase):
|
||||
def clear_caches(self, *, cache_tag: str = "pos") -> None: ...
|
||||
def predict_noise_streaming(
|
||||
self, ..., cache_tag, store_kv, cur_start_frame
|
||||
) -> Tensor | None: ...
|
||||
def predict_x0_streaming(
|
||||
self, ..., cache_tag, store_kv, cur_start_frame
|
||||
) -> Tensor | None: ...
|
||||
```
|
||||
|
||||
KV caches are **internal** to the model instance, keyed by `cache_tag`. The
|
||||
method controls when to store (`store_kv=True`) vs. read-only
|
||||
(`store_kv=False`), enabling block-by-block causal rollout during training.
|
||||
|
||||
---
|
||||
|
||||
## Training Methods
|
||||
|
||||
### DMD2 (Distribution Matching Distillation)
|
||||
|
||||
**Roles:** student (trainable) + teacher (frozen) + critic (trainable)
|
||||
|
||||
The student learns to generate clean video in few steps by matching the
|
||||
teacher's score function, with a critic network providing a learned fake-score
|
||||
baseline.
|
||||
|
||||
- **Rollout modes:**
|
||||
- `simulate` — Student starts from pure noise and iteratively denoises
|
||||
through the full step schedule.
|
||||
- `data_latent` — Student denoises from a single randomly-noised data
|
||||
sample.
|
||||
- **Losses:** Generator loss (DMD gradient) + critic flow-matching loss, with
|
||||
alternating updates (`generator_update_interval`).
|
||||
|
||||
### Self-Forcing (Causal DMD)
|
||||
|
||||
**Roles:** student (causal, trainable) + teacher (frozen) + critic (trainable)
|
||||
|
||||
Extends DMD2 for **streaming/causal video generation**. The key idea: during
|
||||
training, the student processes video in temporal chunks, using its own
|
||||
previously-denoised outputs as context for future chunks — simulating online
|
||||
autoregressive rollout.
|
||||
|
||||
- Video is split into blocks of `chunk_size` latent frames.
|
||||
- Each block is denoised through the student's step schedule; a random
|
||||
early-exit step is sampled per block.
|
||||
- After denoising a block, its output is fed back (with optional
|
||||
`context_noise`) as KV cache context for subsequent blocks via
|
||||
`predict_noise_streaming(store_kv=True)`.
|
||||
- Supports SDE and ODE sampling during rollout.
|
||||
- Selective gradient control: `enable_gradient_in_rollout`,
|
||||
`start_gradient_frame`.
|
||||
|
||||
### Supervised Fine-Tuning (SFT)
|
||||
|
||||
**Roles:** student only
|
||||
|
||||
Standard flow-matching loss between predicted and ground-truth noise/x0.
|
||||
|
||||
### Diffusion-Forcing SFT (DFSFT)
|
||||
|
||||
**Roles:** student only
|
||||
|
||||
SFT with **inhomogeneous (per-chunk) timesteps** — each temporal chunk in a
|
||||
video gets a different noise level. This trains the model to handle mixed-noise
|
||||
inputs, which is a prerequisite for causal/streaming inference where earlier
|
||||
frames are cleaner than later ones.
|
||||
|
||||
---
|
||||
|
||||
## Training Loop
|
||||
|
||||
The `Trainer` runs a standard loop with pluggable method and callbacks:
|
||||
|
||||
```
|
||||
for step in range(start_step, max_steps):
|
||||
for accum_iter in range(grad_accum_steps):
|
||||
batch <- dataloader
|
||||
loss_map, outputs, metrics <- method.single_train_step(batch, step)
|
||||
method.backward(loss_map, outputs)
|
||||
|
||||
callbacks.on_before_optimizer_step() # grad clipping
|
||||
method.optimizers_schedulers_step()
|
||||
method.optimizers_zero_grad()
|
||||
callbacks.on_training_step_end() # logging
|
||||
checkpoint_manager.maybe_save(step)
|
||||
callbacks.on_validation_begin() # periodic inference
|
||||
```
|
||||
|
||||
### Callbacks
|
||||
|
||||
- **GradNormClipCallback** — Per-module gradient norm logging + global
|
||||
clipping.
|
||||
- **ValidationCallback** — Periodic inference sampling with configurable
|
||||
pipeline, sampling steps, and guidance scale.
|
||||
- **EMACallback** — Exponential moving average of student weights.
|
||||
|
||||
### Checkpointing
|
||||
|
||||
- DCP (Distributed Checkpoint) format, compatible with FSDP/HSDP.
|
||||
- Saves: model weights, optimizer states, scheduler states, RNG states (per
|
||||
role).
|
||||
- Full resume support: auto-restores step counter and all RNG states.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
# Install
|
||||
uv pip install -e .[dev]
|
||||
|
||||
# Run DMD2 distillation on Wan 2.1
|
||||
torchrun --nproc_per_node=8 -m fastvideo.train.entrypoint.train \
|
||||
--config examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml
|
||||
|
||||
# Run SFT fine-tuning
|
||||
torchrun --nproc_per_node=8 -m fastvideo.train.entrypoint.train \
|
||||
--config examples/train/finetune_wan2.1_t2v_1.3B_vsa_phase3.4_0.9sparsity.yaml
|
||||
```
|
||||
|
||||
Example configs are in `examples/train/`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
fastvideo/train/
|
||||
trainer.py # Training loop
|
||||
models/
|
||||
base.py # ModelBase, CausalModelBase ABCs
|
||||
wan/wan.py # Wan 2.1 T2V model plugin
|
||||
wangame/wangame.py # WanGame 2.1 I2V model plugin
|
||||
wangame/wangame_causal.py # WanGame causal (streaming) plugin
|
||||
methods/
|
||||
base.py # TrainingMethod ABC
|
||||
distribution_matching/
|
||||
dmd2.py # DMD2 distillation
|
||||
self_forcing.py # Self-Forcing (causal DMD)
|
||||
fine_tuning/
|
||||
finetune.py # Supervised fine-tuning
|
||||
dfsft.py # Diffusion-forcing SFT
|
||||
callbacks/
|
||||
grad_clip.py # Gradient clipping + norm logging
|
||||
validation.py # Periodic inference validation
|
||||
ema.py # EMA weight averaging
|
||||
entrypoint/
|
||||
train.py # CLI entrypoint (torchrun)
|
||||
utils/
|
||||
config.py # YAML parser -> RunConfig
|
||||
builder.py # build_from_config: model/method instantiation
|
||||
training_config.py # TrainingConfig dataclass
|
||||
dataloader.py # Dataset/dataloader construction
|
||||
optimizer.py # Optimizer/scheduler construction
|
||||
checkpoint.py # DCP save/resume
|
||||
tracking.py # W&B tracker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Core framework (trainer, config, callbacks) | Implemented and tested |
|
||||
| `WanModel` (Wan 2.1 T2V) | Implemented and tested |
|
||||
| `WanGameModel` (WanGame 2.1 I2V) | Implemented and tested |
|
||||
| `WanGameCausalModel` (streaming) | Implemented and tested |
|
||||
| `WanCausalModel` (Wan T2V causal) | In progress |
|
||||
| DMD2 method | Implemented and tested |
|
||||
| Self-Forcing method | Implemented and tested |
|
||||
| SFT method | Implemented and tested |
|
||||
| DFSFT method | Implemented and tested |
|
||||
| DCP checkpointing + resume | Implemented and tested |
|
||||
| EMA callback | Implemented |
|
||||
| Validation callback | Implemented and tested |
|
||||
| Causal DMD inference pipeline | Implemented |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
We welcome community feedback on the following topics:
|
||||
|
||||
### Model Plugin API
|
||||
|
||||
The current `ModelBase` interface requires implementing 6 methods. Is this the
|
||||
right granularity?
|
||||
|
||||
- Should `prepare_batch` be split into separate concerns (noise sampling,
|
||||
timestep sampling, attention metadata)?
|
||||
- Should `backward` be lifted out of the model and into the method/trainer?
|
||||
|
||||
### Causal Streaming Interface
|
||||
|
||||
`CausalModelBase` adds `predict_noise_streaming` / `predict_x0_streaming` with
|
||||
cache management. Alternatives considered:
|
||||
|
||||
- **(a) Current:** Cache is internal to the model, keyed by `cache_tag`.
|
||||
Simple but couples cache lifecycle to model.
|
||||
- **(b) External cache:** Method owns the cache dict, passes it into predict
|
||||
calls. More explicit but verbose.
|
||||
- **(c) Context manager:** `with model.streaming_context(tag) as ctx: ...` —
|
||||
cleaner lifecycle but harder to compose.
|
||||
|
||||
### Method Composition
|
||||
|
||||
Currently, methods are monolithic classes. Should we support composing methods
|
||||
(e.g., DFSFT pre-training followed by Self-Forcing distillation) within a
|
||||
single config? Or is sequential training with checkpoint handoff sufficient?
|
||||
|
||||
### New Models and Methods
|
||||
|
||||
What models and training methods should we prioritize next?
|
||||
|
||||
- **Models:** HunyuanVideo, CogVideoX, other Wan variants?
|
||||
- **Methods:** Consistency models, progressive distillation, reward-based
|
||||
fine-tuning?
|
||||
|
||||
### Distributed Strategy
|
||||
|
||||
Currently supports HSDP (hybrid sharded data parallel) + TP + SP. Are there
|
||||
scenarios where the current distributed setup is insufficient? Should we add
|
||||
pipeline parallelism for very large models?
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Self-Forcing paper](https://arxiv.org/abs/2406.05477) — Chen et al., 2024.
|
||||
- [DMD2 paper](https://arxiv.org/abs/2405.14867) — Yin et al., 2024.
|
||||
- [Diffusion Forcing paper](https://arxiv.org/abs/2407.01392) — Chen et al.,
|
||||
2024.
|
||||
@@ -0,0 +1,595 @@
|
||||
# Training Infrastructure
|
||||
|
||||
FastVideo's training infrastructure (`fastvideo/train/`) is a YAML-driven
|
||||
framework for training and distilling video diffusion models. A single config
|
||||
file controls everything — models, algorithms, distributed strategy,
|
||||
checkpointing, and validation — with no code changes needed to mix and match.
|
||||
|
||||
!!! note "Relationship to legacy training"
|
||||
This system replaces the older script-based training in `fastvideo/training/`.
|
||||
The legacy scripts still work for basic fine-tuning, but new development
|
||||
should use the config-driven system documented here.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Launch with the helper script
|
||||
|
||||
```bash
|
||||
bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml
|
||||
```
|
||||
|
||||
The script auto-detects available GPUs and sets up `torchrun`. Override with
|
||||
environment variables:
|
||||
|
||||
```bash
|
||||
NUM_GPUS=4 NNODES=2 NODE_RANK=0 \
|
||||
MASTER_ADDR=10.0.0.1 MASTER_PORT=29501 \
|
||||
bash examples/train/run.sh my_config.yaml
|
||||
```
|
||||
|
||||
### Launch directly with torchrun
|
||||
|
||||
```bash
|
||||
torchrun --nproc_per_node=8 \
|
||||
fastvideo/train/entrypoint/train.py \
|
||||
--config examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml
|
||||
```
|
||||
|
||||
### CLI flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--config` | Path to YAML config file (required) |
|
||||
| `--resume-from-checkpoint` | Path to a DCP checkpoint directory to resume from |
|
||||
| `--override-output-dir` | Override `training.checkpoint.output_dir` |
|
||||
| `--dry-run` | Validate config and exit without training |
|
||||
|
||||
---
|
||||
|
||||
## Config Format
|
||||
|
||||
Every run is defined by a single YAML file with five top-level sections.
|
||||
See `examples/train/example.yaml` for a fully-commented reference.
|
||||
|
||||
### `models` — Role-based model instances
|
||||
|
||||
Each entry defines a model role. The `_target_` field specifies the Python class
|
||||
to instantiate:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
teacher:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: false
|
||||
disable_custom_init_weights: true
|
||||
```
|
||||
|
||||
Common model parameters:
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `_target_` | *(required)* | Python class path for the model |
|
||||
| `init_from` | *(required)* | HuggingFace repo ID or local checkpoint path |
|
||||
| `trainable` | `true` | Whether the model's parameters require gradients |
|
||||
| `disable_custom_init_weights` | `false` | Skip custom weight initialization (use for teacher/critic) |
|
||||
| `flow_shift` | `3.0` | Timestep shifting factor |
|
||||
| `enable_gradient_checkpointing_type` | `null` | Gradient checkpointing (`"full"` or `null`) |
|
||||
|
||||
Which roles are needed depends on the training method:
|
||||
|
||||
| Method | Required roles |
|
||||
|--------|---------------|
|
||||
| Fine-tune (SFT) | `student` |
|
||||
| Diffusion-Forcing SFT | `student` |
|
||||
| DMD2 | `student`, `teacher`, `critic` |
|
||||
| Self-Forcing | `student` (causal), `teacher`, `critic` |
|
||||
|
||||
### `method` — Training algorithm
|
||||
|
||||
Selects and configures the training algorithm:
|
||||
|
||||
```yaml
|
||||
method:
|
||||
_target_: fastvideo.train.methods.distribution_matching.dmd2.DMD2Method
|
||||
rollout_mode: simulate
|
||||
dmd_denoising_steps: [1000, 750, 500, 250]
|
||||
generator_update_interval: 5
|
||||
```
|
||||
|
||||
To switch algorithms, change `_target_` and adjust the method-specific keys.
|
||||
See [Training Methods](#training-methods) for details on each algorithm.
|
||||
|
||||
### `training` — Typed infrastructure config
|
||||
|
||||
This section maps to typed dataclasses with defaults and validation:
|
||||
|
||||
```yaml
|
||||
training:
|
||||
distributed:
|
||||
num_gpus: 8
|
||||
sp_size: 1 # sequence parallelism
|
||||
tp_size: 1 # tensor parallelism
|
||||
hsdp_replicate_dim: 1 # HSDP replication dimension
|
||||
hsdp_shard_dim: 8 # HSDP sharding dimension
|
||||
|
||||
data:
|
||||
data_path: data/my_dataset
|
||||
train_batch_size: 1
|
||||
dataloader_num_workers: 4
|
||||
training_cfg_rate: 0.1 # classifier-free guidance dropout rate
|
||||
seed: 1000
|
||||
num_latent_t: 20
|
||||
num_height: 448
|
||||
num_width: 832
|
||||
num_frames: 77
|
||||
|
||||
optimizer:
|
||||
learning_rate: 2.0e-6
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
lr_scheduler: constant # constant, linear, cosine, polynomial
|
||||
lr_warmup_steps: 0
|
||||
|
||||
loop:
|
||||
max_train_steps: 4000
|
||||
gradient_accumulation_steps: 1
|
||||
|
||||
checkpoint:
|
||||
output_dir: outputs/my_run
|
||||
training_state_checkpointing_steps: 1000 # 0 = disabled
|
||||
checkpoints_total_limit: 3 # 0 = keep all
|
||||
|
||||
tracker:
|
||||
project_name: my_project
|
||||
run_name: my_run
|
||||
|
||||
model:
|
||||
weighting_scheme: uniform # uniform, logit_normal, mode
|
||||
precondition_outputs: false
|
||||
enable_gradient_checkpointing_type: full
|
||||
|
||||
vsa:
|
||||
sparsity: 0.0 # 0.0 = disabled
|
||||
decay_rate: 0.0
|
||||
decay_interval_steps: 0
|
||||
```
|
||||
|
||||
### `callbacks` — Pluggable hooks
|
||||
|
||||
Callbacks run at specific points in the training loop (before/after optimizer
|
||||
steps, at validation time, etc.):
|
||||
|
||||
```yaml
|
||||
callbacks:
|
||||
grad_clip:
|
||||
max_grad_norm: 1.0
|
||||
|
||||
ema:
|
||||
_target_: fastvideo.train.callbacks.ema.EMACallback
|
||||
decay: 0.9999
|
||||
start_iter: 0
|
||||
|
||||
validation:
|
||||
_target_: fastvideo.train.callbacks.validation.ValidationCallback
|
||||
pipeline_target: fastvideo.pipelines.basic.wan.wan_pipeline.WanPipeline
|
||||
dataset_file: path/to/validation.json
|
||||
every_steps: 100
|
||||
sampling_steps: [4]
|
||||
guidance_scale: 5.0
|
||||
```
|
||||
|
||||
See [Callbacks](#callbacks) for details on each callback.
|
||||
|
||||
### `pipeline` — Inference pipeline overrides
|
||||
|
||||
Optional overrides for the inference pipeline used during validation:
|
||||
|
||||
```yaml
|
||||
pipeline:
|
||||
flow_shift: 8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Training Methods
|
||||
|
||||
### Supervised Fine-Tuning (SFT)
|
||||
|
||||
Standard flow-matching loss. The simplest method — train the student to predict
|
||||
noise (or clean x0) from noised data samples.
|
||||
|
||||
```yaml
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
|
||||
attn_kind: dense # "dense" or "vsa"
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `attn_kind` | `"dense"` | Attention mode: `"dense"` (standard) or `"vsa"` (sparse) |
|
||||
|
||||
### Diffusion-Forcing SFT (DFSFT)
|
||||
|
||||
SFT with **per-chunk inhomogeneous timesteps** — each temporal chunk of the
|
||||
video gets a different noise level. This is a prerequisite for training causal /
|
||||
streaming models that must handle mixed-noise inputs.
|
||||
|
||||
```yaml
|
||||
method:
|
||||
_target_: fastvideo.train.methods.fine_tuning.dfsft.DiffusionForcingSFTMethod
|
||||
chunk_size: 3
|
||||
min_timestep_ratio: 0.0
|
||||
max_timestep_ratio: 1.0
|
||||
attn_kind: dense
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `chunk_size` | `3` | Latent frames per temporal chunk |
|
||||
| `min_timestep_ratio` | `0.0` | Lower bound of timestep sampling range |
|
||||
| `max_timestep_ratio` | `1.0` | Upper bound of timestep sampling range |
|
||||
| `attn_kind` | `"dense"` | `"dense"` or `"vsa"` |
|
||||
|
||||
### DMD2 (Distribution Matching Distillation)
|
||||
|
||||
Distill a many-step teacher into a few-step student. The student learns to match
|
||||
the teacher's score function, guided by a trainable critic network.
|
||||
|
||||
```yaml
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
teacher:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: false
|
||||
disable_custom_init_weights: true
|
||||
critic:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
disable_custom_init_weights: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.distribution_matching.dmd2.DMD2Method
|
||||
rollout_mode: simulate
|
||||
dmd_denoising_steps: [1000, 750, 500, 250]
|
||||
generator_update_interval: 5
|
||||
real_score_guidance_scale: 4.5
|
||||
|
||||
fake_score_learning_rate: 8.0e-6
|
||||
fake_score_betas: [0.0, 0.999]
|
||||
fake_score_lr_scheduler: constant
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `rollout_mode` | *(required)* | `"simulate"` (pure noise) or `"data_latent"` (from data) |
|
||||
| `dmd_denoising_steps` | *(required)* | Timestep schedule for student rollout |
|
||||
| `generator_update_interval` | `1` | Update student every N critic steps |
|
||||
| `real_score_guidance_scale` | `1.0` | CFG scale for teacher predictions |
|
||||
| `fake_score_learning_rate` | *(required)* | Critic optimizer learning rate |
|
||||
| `fake_score_betas` | *(required)* | Critic optimizer Adam betas |
|
||||
| `fake_score_lr_scheduler` | *(required)* | Critic LR scheduler type |
|
||||
|
||||
### Self-Forcing (Causal DMD)
|
||||
|
||||
Extends DMD2 for **streaming / causal video generation**. The student processes
|
||||
video in temporal chunks, feeding its own denoised outputs as context for future
|
||||
chunks — simulating autoregressive rollout during training.
|
||||
|
||||
Requires a causal model class (e.g., `WanCausalModel`) for the student:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.wan_causal.WanCausalModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.distribution_matching.self_forcing.SelfForcingMethod
|
||||
rollout_mode: simulate
|
||||
dmd_denoising_steps: [1000, 750, 500, 250]
|
||||
student_sample_type: sde
|
||||
context_noise: 0.0
|
||||
enable_gradient_in_rollout: true
|
||||
start_gradient_frame: 0
|
||||
```
|
||||
|
||||
Self-Forcing inherits all DMD2 parameters, plus:
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `student_sample_type` | `"sde"` | `"sde"` or `"ode"` for intermediate steps |
|
||||
| `same_step_across_blocks` | `false` | Use same exit timestep for all blocks |
|
||||
| `last_step_only` | `false` | Always exit at the final denoising step |
|
||||
| `context_noise` | `0.0` | Noise added to context frames (0 = clean) |
|
||||
| `enable_gradient_in_rollout` | `true` | Enable backprop through rollout |
|
||||
| `start_gradient_frame` | `0` | Frame index where gradients begin |
|
||||
|
||||
---
|
||||
|
||||
## Callbacks
|
||||
|
||||
Callbacks are pluggable hooks that run at specific points in the training loop.
|
||||
Configure them under the `callbacks` section.
|
||||
|
||||
### GradNormClipCallback
|
||||
|
||||
Clips gradient norms before the optimizer step. Optionally logs per-module
|
||||
gradient norms to the tracker.
|
||||
|
||||
```yaml
|
||||
callbacks:
|
||||
grad_clip:
|
||||
max_grad_norm: 1.0 # 0.0 = disabled
|
||||
log_grad_norms: false
|
||||
```
|
||||
|
||||
### EMACallback
|
||||
|
||||
Maintains an exponential moving average of the student's weights. The EMA
|
||||
weights are automatically swapped in during validation.
|
||||
|
||||
```yaml
|
||||
callbacks:
|
||||
ema:
|
||||
_target_: fastvideo.train.callbacks.ema.EMACallback
|
||||
decay: 0.9999
|
||||
start_iter: 0 # delay EMA updates until this iteration
|
||||
```
|
||||
|
||||
The EMA callback owns its own state and checkpoints independently — EMA weights
|
||||
are saved and restored automatically on resume.
|
||||
|
||||
### ValidationCallback
|
||||
|
||||
Runs inference with the trained model at regular intervals, saving generated
|
||||
videos and logging them to the tracker (W&B).
|
||||
|
||||
```yaml
|
||||
callbacks:
|
||||
validation:
|
||||
_target_: fastvideo.train.callbacks.validation.ValidationCallback
|
||||
pipeline_target: fastvideo.pipelines.basic.wan.wan_pipeline.WanPipeline
|
||||
dataset_file: path/to/validation.json
|
||||
every_steps: 100
|
||||
sampling_steps: [4]
|
||||
sampling_timesteps: [1000, 750, 500, 250] # explicit timestep list
|
||||
guidance_scale: 5.0
|
||||
rollout_mode: parallel # "parallel" or "streaming"
|
||||
```
|
||||
|
||||
The validation dataset is a JSON file containing a list of prompt strings.
|
||||
If EMA is enabled, validation automatically uses the EMA weights.
|
||||
|
||||
---
|
||||
|
||||
## Checkpointing and Resume
|
||||
|
||||
### Checkpoint format
|
||||
|
||||
Checkpoints use PyTorch Distributed Checkpoint (DCP) format, compatible with
|
||||
FSDP/HSDP sharding. Each checkpoint saves:
|
||||
|
||||
- Model weights (all roles)
|
||||
- Optimizer states (all roles)
|
||||
- LR scheduler states
|
||||
- RNG states (for exact reproducibility)
|
||||
- EMA shadow weights (if enabled)
|
||||
- Training step counter
|
||||
|
||||
Checkpoints are saved to `<output_dir>/checkpoint-<step>/`.
|
||||
|
||||
### Saving checkpoints
|
||||
|
||||
```yaml
|
||||
training:
|
||||
checkpoint:
|
||||
output_dir: outputs/my_run
|
||||
training_state_checkpointing_steps: 1000 # save every N steps (0 = off)
|
||||
checkpoints_total_limit: 3 # rolling window (0 = keep all)
|
||||
```
|
||||
|
||||
### Resuming training
|
||||
|
||||
Use `--resume-from-checkpoint` to resume from a specific checkpoint:
|
||||
|
||||
```bash
|
||||
# Via the helper script
|
||||
bash examples/train/run.sh my_config.yaml --resume outputs/my_run/checkpoint-2000
|
||||
|
||||
# Via torchrun directly
|
||||
torchrun --nproc_per_node=8 \
|
||||
fastvideo/train/entrypoint/train.py \
|
||||
--config my_config.yaml \
|
||||
--resume-from-checkpoint outputs/my_run/checkpoint-2000
|
||||
```
|
||||
|
||||
Or set it in the YAML:
|
||||
|
||||
```yaml
|
||||
training:
|
||||
checkpoint:
|
||||
resume_from_checkpoint: outputs/my_run/checkpoint-2000
|
||||
```
|
||||
|
||||
### Reproducibility
|
||||
|
||||
The training entrypoint enables deterministic mode automatically:
|
||||
|
||||
- `torch.backends.cudnn.benchmark = False`
|
||||
- `torch.backends.cudnn.deterministic = True`
|
||||
- `torch.use_deterministic_algorithms(True)`
|
||||
|
||||
A shared CUDA RNG generator is seeded from `training.data.seed` and threaded
|
||||
through all random operations (noise sampling, timestep sampling, etc.).
|
||||
Ranks within the same sequence-parallel group share a seed, ensuring identical
|
||||
noise across SP shards.
|
||||
|
||||
---
|
||||
|
||||
## Distributed Training
|
||||
|
||||
The framework supports HSDP (Hybrid Sharded Data Parallel), Tensor Parallelism
|
||||
(TP), and Sequence Parallelism (SP):
|
||||
|
||||
```yaml
|
||||
training:
|
||||
distributed:
|
||||
num_gpus: 8
|
||||
sp_size: 1 # sequence parallelism group size
|
||||
tp_size: 1 # tensor parallelism group size
|
||||
hsdp_replicate_dim: 1 # number of HSDP replicas
|
||||
hsdp_shard_dim: 8 # number of HSDP shards
|
||||
```
|
||||
|
||||
**HSDP** shards model parameters across `hsdp_shard_dim` GPUs and replicates
|
||||
across `hsdp_replicate_dim` groups. The product
|
||||
`hsdp_replicate_dim * hsdp_shard_dim` should equal `num_gpus`.
|
||||
|
||||
**Sequence parallelism** splits the sequence (video frames) across `sp_size`
|
||||
GPUs within each data-parallel group. Useful for long videos that don't fit on a
|
||||
single GPU.
|
||||
|
||||
---
|
||||
|
||||
## VSA (Variable Sparse Attention)
|
||||
|
||||
VSA progressively increases attention sparsity during training, reducing compute
|
||||
while maintaining quality:
|
||||
|
||||
```yaml
|
||||
training:
|
||||
vsa:
|
||||
sparsity: 0.9 # target sparsity level
|
||||
decay_rate: 0.03 # sparsity increment per decay interval
|
||||
decay_interval_steps: 1 # steps between sparsity increases
|
||||
```
|
||||
|
||||
The effective sparsity at step `t` is
|
||||
`min(sparsity, decay_rate * (t // decay_interval_steps))`.
|
||||
|
||||
---
|
||||
|
||||
## Extending the Framework
|
||||
|
||||
### Adding a new model
|
||||
|
||||
1. Create a new module under `fastvideo/train/models/` (e.g.,
|
||||
`fastvideo/train/models/mymodel/mymodel.py`).
|
||||
2. Subclass `ModelBase` (or `CausalModelBase` for streaming models).
|
||||
3. Implement the required methods:
|
||||
- `prepare_batch()` — convert raw dataloader output to `TrainingBatch`
|
||||
- `add_noise()` — forward-process noise addition
|
||||
- `predict_noise()` — run the transformer forward pass
|
||||
- `backward()` — backward pass with forward context restoration
|
||||
4. Reference it in your YAML config:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.mymodel.mymodel.MyModel
|
||||
init_from: my-org/my-model
|
||||
trainable: true
|
||||
```
|
||||
|
||||
### Adding a new training method
|
||||
|
||||
1. Create a new module under `fastvideo/train/methods/`.
|
||||
2. Subclass `TrainingMethod`.
|
||||
3. Implement the required methods:
|
||||
- `single_train_step()` — one forward pass returning losses, outputs, metrics
|
||||
- `get_optimizers()` — return optimizer list
|
||||
- `get_lr_schedulers()` — return scheduler list
|
||||
4. Reference it in your config:
|
||||
|
||||
```yaml
|
||||
method:
|
||||
_target_: fastvideo.train.methods.my_method.MyMethod
|
||||
my_param: 42
|
||||
```
|
||||
|
||||
Method-specific parameters are accessible via `self.method_config` (a plain
|
||||
dict).
|
||||
|
||||
### Adding a new callback
|
||||
|
||||
1. Create a new module under `fastvideo/train/callbacks/`.
|
||||
2. Subclass `Callback`.
|
||||
3. Override the hooks you need: `on_train_start`, `on_training_step_end`,
|
||||
`on_before_optimizer_step`, etc.
|
||||
4. Optionally implement `state_dict()` / `load_state_dict()` for checkpoint
|
||||
persistence.
|
||||
5. Add it to your config:
|
||||
|
||||
```yaml
|
||||
callbacks:
|
||||
my_callback:
|
||||
_target_: fastvideo.train.callbacks.my_callback.MyCallback
|
||||
my_param: 42
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
fastvideo/train/
|
||||
entrypoint/
|
||||
train.py # CLI entrypoint (torchrun)
|
||||
trainer.py # Training loop orchestrator
|
||||
models/
|
||||
base.py # ModelBase, CausalModelBase ABCs
|
||||
wan/
|
||||
wan.py # Wan 2.1 T2V model
|
||||
wan_causal.py # Wan causal (streaming) model
|
||||
methods/
|
||||
base.py # TrainingMethod ABC
|
||||
distribution_matching/
|
||||
dmd2.py # DMD2 distillation
|
||||
self_forcing.py # Self-Forcing (causal DMD)
|
||||
fine_tuning/
|
||||
finetune.py # Supervised fine-tuning
|
||||
dfsft.py # Diffusion-forcing SFT
|
||||
callbacks/
|
||||
callback.py # Callback ABC and CallbackDict
|
||||
grad_clip.py # Gradient clipping + norm logging
|
||||
ema.py # EMA weight averaging
|
||||
validation.py # Periodic inference validation
|
||||
utils/
|
||||
config.py # YAML parser -> RunConfig
|
||||
training_config.py # Typed config dataclasses
|
||||
builder.py # Model/method instantiation
|
||||
optimizer.py # Optimizer/scheduler construction
|
||||
checkpoint.py # DCP save/resume
|
||||
dataloader.py # Dataset/dataloader construction
|
||||
tracking.py # W&B tracker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Training Architecture](../design/training_architecture.md) — design
|
||||
rationale, model/method abstractions, and open questions.
|
||||
- [Training Overview](overview.md) — data requirements and preprocessing.
|
||||
- [Data Preprocessing](data_preprocess.md) — how to prepare datasets.
|
||||
- [Config Reference](../../examples/train/configs/example.yaml) — fully-commented
|
||||
YAML config with all fields and defaults.
|
||||
@@ -0,0 +1,70 @@
|
||||
# DFSFT (Diffusion-Forcing SFT): Wan 2.1 T2V 1.3B Causal
|
||||
#
|
||||
# - Student: trainable causal Wan model
|
||||
# - Training: inhomogeneous timesteps per chunk (diffusion forcing)
|
||||
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanCausalModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.fine_tuning.dfsft.DiffusionForcingSFTMethod
|
||||
chunk_size: 3
|
||||
|
||||
training:
|
||||
distributed:
|
||||
num_gpus: 8
|
||||
sp_size: 1
|
||||
tp_size: 1
|
||||
hsdp_replicate_dim: 1
|
||||
hsdp_shard_dim: 8
|
||||
|
||||
data:
|
||||
data_path: data/Wan-Syn_77x448x832_600k
|
||||
dataloader_num_workers: 4
|
||||
train_batch_size: 1
|
||||
training_cfg_rate: 0.0
|
||||
seed: 1000
|
||||
num_latent_t: 20
|
||||
num_height: 448
|
||||
num_width: 832
|
||||
num_frames: 77
|
||||
|
||||
optimizer:
|
||||
learning_rate: 2.0e-6
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
lr_scheduler: constant
|
||||
lr_warmup_steps: 0
|
||||
|
||||
loop:
|
||||
max_train_steps: 4000
|
||||
gradient_accumulation_steps: 1
|
||||
|
||||
checkpoint:
|
||||
output_dir: outputs/wan2.1_causal_dfsft
|
||||
training_state_checkpointing_steps: 1000
|
||||
checkpoints_total_limit: 3
|
||||
|
||||
tracker:
|
||||
project_name: distillation_wan_r
|
||||
run_name: wan2.1_causal_dfsft
|
||||
|
||||
model:
|
||||
enable_gradient_checkpointing_type: full
|
||||
|
||||
callbacks:
|
||||
grad_clip:
|
||||
max_grad_norm: 1.0
|
||||
validation:
|
||||
pipeline_target: fastvideo.pipelines.basic.wan.wan_causal_pipeline.WanCausalPipeline
|
||||
dataset_file: examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/validation_64.json
|
||||
every_steps: 50
|
||||
sampling_steps: [40]
|
||||
guidance_scale: 6.0
|
||||
num_frames: 69
|
||||
|
||||
pipeline:
|
||||
flow_shift: 8
|
||||
@@ -0,0 +1,94 @@
|
||||
# DMD2 distillation: Wan 2.1 T2V 1.3B (teacher 50-step -> student 4-step).
|
||||
#
|
||||
# - Teacher: frozen pretrained Wan 2.1 T2V 1.3B
|
||||
# - Student: trainable, initialized from the same pretrained weights
|
||||
# - Critic: trainable, initialized from the same pretrained weights
|
||||
# - Validation: 4-step SDE sampling
|
||||
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
teacher:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: false
|
||||
disable_custom_init_weights: true
|
||||
critic:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
disable_custom_init_weights: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.distribution_matching.dmd2.DMD2Method
|
||||
rollout_mode: simulate
|
||||
generator_update_interval: 5
|
||||
real_score_guidance_scale: 4.5
|
||||
dmd_denoising_steps: [1000, 750, 500, 250]
|
||||
|
||||
# Critic optimizer (required — no fallback to training.optimizer)
|
||||
fake_score_learning_rate: 8.0e-6
|
||||
fake_score_betas: [0.0, 0.999]
|
||||
fake_score_lr_scheduler: constant
|
||||
|
||||
training:
|
||||
distributed:
|
||||
num_gpus: 4
|
||||
sp_size: 1
|
||||
tp_size: 1
|
||||
hsdp_replicate_dim: 1
|
||||
hsdp_shard_dim: 4
|
||||
|
||||
data:
|
||||
data_path: data/Wan-Syn_77x448x832_600k
|
||||
dataloader_num_workers: 4
|
||||
train_batch_size: 1
|
||||
training_cfg_rate: 0.0
|
||||
seed: 1000
|
||||
num_latent_t: 20
|
||||
num_height: 448
|
||||
num_width: 832
|
||||
num_frames: 77
|
||||
|
||||
optimizer:
|
||||
learning_rate: 2.0e-6
|
||||
betas: [0.0, 0.999]
|
||||
weight_decay: 0.01
|
||||
lr_scheduler: constant
|
||||
lr_warmup_steps: 0
|
||||
|
||||
loop:
|
||||
max_train_steps: 4000
|
||||
gradient_accumulation_steps: 1
|
||||
|
||||
checkpoint:
|
||||
output_dir: outputs/wan2.1_dmd2_4steps
|
||||
training_state_checkpointing_steps: 20
|
||||
checkpoints_total_limit: 3
|
||||
|
||||
tracker:
|
||||
project_name: distillation_wan
|
||||
run_name: wan2.1_dmd2_4steps
|
||||
|
||||
model:
|
||||
enable_gradient_checkpointing_type: full
|
||||
|
||||
callbacks:
|
||||
grad_clip:
|
||||
max_grad_norm: 1.0
|
||||
validation:
|
||||
pipeline_target: fastvideo.pipelines.basic.wan.wan_dmd_pipeline.WanDMDPipeline
|
||||
dataset_file: examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/validation_4.json
|
||||
every_steps: 50
|
||||
sampling_steps: [4]
|
||||
sampling_timesteps: [1000, 750, 500, 250]
|
||||
guidance_scale: 6.0
|
||||
ema:
|
||||
_target_: fastvideo.train.callbacks.ema.EMACallback
|
||||
decay: 0.98
|
||||
start_iter: 0 # delay EMA updates until this iteration
|
||||
|
||||
pipeline:
|
||||
flow_shift: 8
|
||||
@@ -0,0 +1,194 @@
|
||||
# ==============================================================================
|
||||
# Full configuration reference for fastvideo.train
|
||||
#
|
||||
# Legend:
|
||||
# [TYPED] — parsed into a typed dataclass; fields are validated with
|
||||
# defaults. Unknown keys are silently ignored.
|
||||
# [FREE] — free-form dict passed as-is to the target class / method.
|
||||
# Keys depend on the _target_ class constructor / method_config.
|
||||
# [RESOLVED] — parsed by PipelineConfig.from_kwargs(); auto-populated from
|
||||
# the model's config files. Only scalar overrides are useful.
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# models: [FREE]
|
||||
#
|
||||
# Each role is instantiated via _target_(*, training_config=..., **kwargs).
|
||||
# Keys here are constructor kwargs of the _target_ class (e.g. WanModel).
|
||||
# You can define any role name (student, teacher, critic, etc.).
|
||||
# ------------------------------------------------------------------------------
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanModel # required
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers # required: HF repo or local path
|
||||
trainable: true # default: true
|
||||
disable_custom_init_weights: false # default: false
|
||||
flow_shift: 3.0 # default: 3.0
|
||||
enable_gradient_checkpointing_type: null # default: null (falls back to training.model)
|
||||
|
||||
teacher:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: false
|
||||
disable_custom_init_weights: true
|
||||
|
||||
critic:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
disable_custom_init_weights: true
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# method: [FREE]
|
||||
#
|
||||
# Instantiated via _target_(*, cfg=RunConfig, role_models=...).
|
||||
# All keys besides _target_ are available in self.method_config (a plain dict).
|
||||
# Keys depend entirely on the method class.
|
||||
# ------------------------------------------------------------------------------
|
||||
method:
|
||||
_target_: fastvideo.train.methods.distribution_matching.dmd2.DMD2Method # required
|
||||
|
||||
# --- DMD2-specific keys (read from self.method_config) ---
|
||||
rollout_mode: simulate # required: "simulate" or "data_latent"
|
||||
generator_update_interval: 5 # default: 1
|
||||
dmd_denoising_steps: [1000, 750, 500, 250] # SDE timestep schedule
|
||||
|
||||
# Critic optimizer (all required — no fallback)
|
||||
fake_score_learning_rate: 8.0e-6
|
||||
fake_score_betas: [0.0, 0.999]
|
||||
fake_score_lr_scheduler: constant
|
||||
|
||||
# CFG conditioning policy (optional)
|
||||
# cfg_uncond:
|
||||
# on_missing: error # "error" or "ignore"
|
||||
# text: keep # "keep", "zero", "drop", "negative_prompt"
|
||||
# image: keep # "keep", "zero", "drop"
|
||||
# action: keep # "keep", "zero", "drop"
|
||||
|
||||
# --- FineTuneMethod keys (if using finetune instead) ---
|
||||
# _target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# training: [TYPED] -> TrainingConfig
|
||||
#
|
||||
# Every field below has a typed default. Unknown keys are ignored.
|
||||
# ------------------------------------------------------------------------------
|
||||
training:
|
||||
|
||||
# --- training.distributed [TYPED] -> DistributedConfig ---
|
||||
distributed:
|
||||
num_gpus: 8 # default: 1
|
||||
tp_size: 1 # default: 1
|
||||
sp_size: 1 # default: 1 (defaults to num_gpus in loader)
|
||||
hsdp_replicate_dim: 1 # default: 1
|
||||
hsdp_shard_dim: 8 # default: -1 (defaults to num_gpus in loader)
|
||||
pin_cpu_memory: false # default: false
|
||||
|
||||
# --- training.data [TYPED] -> DataConfig ---
|
||||
data:
|
||||
data_path: data/my_dataset # default: ""
|
||||
train_batch_size: 1 # default: 1
|
||||
dataloader_num_workers: 4 # default: 0
|
||||
training_cfg_rate: 0.1 # default: 0.0
|
||||
seed: 1000 # default: 0
|
||||
num_height: 448 # default: 0
|
||||
num_width: 832 # default: 0
|
||||
num_latent_t: 20 # default: 0
|
||||
num_frames: 77 # default: 0
|
||||
|
||||
# --- training.optimizer [TYPED] -> OptimizerConfig ---
|
||||
# Note: only for the student optimizer. Critic optimizer is in method config.
|
||||
optimizer:
|
||||
learning_rate: 2.0e-6 # default: 0.0
|
||||
betas: [0.9, 0.999] # default: [0.9, 0.999]
|
||||
weight_decay: 0.01 # default: 0.0
|
||||
lr_scheduler: constant # default: "constant"
|
||||
lr_warmup_steps: 0 # default: 0
|
||||
lr_num_cycles: 0 # default: 0
|
||||
lr_power: 0.0 # default: 0.0
|
||||
min_lr_ratio: 0.5 # default: 0.5
|
||||
|
||||
# --- training.loop [TYPED] -> TrainingLoopConfig ---
|
||||
loop:
|
||||
max_train_steps: 10000 # default: 0
|
||||
gradient_accumulation_steps: 1 # default: 1
|
||||
|
||||
# --- training.checkpoint [TYPED] -> CheckpointConfig ---
|
||||
checkpoint:
|
||||
output_dir: outputs/my_run # default: ""
|
||||
resume_from_checkpoint: "" # default: "" (or use --resume-from-checkpoint CLI)
|
||||
training_state_checkpointing_steps: 1000 # default: 0 (disabled)
|
||||
checkpoints_total_limit: 3 # default: 0 (keep all)
|
||||
|
||||
# --- training.tracker [TYPED] -> TrackerConfig ---
|
||||
tracker:
|
||||
trackers: [] # default: [] (auto-adds "wandb" if project_name is set)
|
||||
project_name: my_project # default: "fastvideo"
|
||||
run_name: my_run # default: ""
|
||||
|
||||
# --- training.vsa [TYPED] -> VSAConfig ---
|
||||
vsa:
|
||||
sparsity: 0.0 # default: 0.0 (0.0 = disabled)
|
||||
|
||||
# --- training.model [TYPED] -> ModelTrainingConfig ---
|
||||
model:
|
||||
weighting_scheme: uniform # default: "uniform"
|
||||
logit_mean: 0.0 # default: 0.0
|
||||
logit_std: 1.0 # default: 1.0
|
||||
mode_scale: 1.0 # default: 1.0
|
||||
precondition_outputs: false # default: false
|
||||
moba_config: {} # default: {}
|
||||
enable_gradient_checkpointing_type: full # default: null ("full" or null)
|
||||
|
||||
# --- training top-level [TYPED] ---
|
||||
dit_precision: fp32 # default: "fp32" (master weight precision)
|
||||
# model_path: ... # default: "" (auto-derived from models.student.init_from)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# callbacks: [FREE]
|
||||
#
|
||||
# Each callback is instantiated via _target_(*, **kwargs).
|
||||
# The callback name (e.g. "grad_clip") is arbitrary — only _target_ matters.
|
||||
# training_config is injected automatically (not from YAML).
|
||||
# ------------------------------------------------------------------------------
|
||||
callbacks:
|
||||
|
||||
# --- GradNormClipCallback ---
|
||||
grad_clip:
|
||||
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback # optional if using default registry
|
||||
max_grad_norm: 1.0 # default: 0.0 (0.0 = disabled)
|
||||
log_grad_norms: false # default: false
|
||||
|
||||
# --- EMACallback ---
|
||||
# ema:
|
||||
# _target_: fastvideo.train.callbacks.ema.EMACallback
|
||||
# decay: 0.9999 # default: 0.9999
|
||||
# start_iter: 0 # default: 0
|
||||
|
||||
# --- ValidationCallback ---
|
||||
validation:
|
||||
_target_: fastvideo.train.callbacks.validation.ValidationCallback # optional if using default registry
|
||||
pipeline_target: fastvideo.pipelines.basic.wan.wan_pipeline.WanPipeline # required
|
||||
dataset_file: path/to/validation.json # required
|
||||
every_steps: 100 # default: 100
|
||||
sampling_steps: [4] # default: [40]
|
||||
guidance_scale: 5.0 # default: null (uses model default)
|
||||
num_frames: null # default: null (derived from training.data)
|
||||
output_dir: null # default: null (falls back to training.checkpoint.output_dir)
|
||||
sampling_timesteps: null # default: null (explicit timestep list for streaming)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# pipeline: [RESOLVED] -> PipelineConfig
|
||||
#
|
||||
# Parsed by PipelineConfig.from_kwargs(). Most fields are auto-populated from
|
||||
# the model's config files (vae_config, dit_config, text_encoder_configs, etc.).
|
||||
# Only scalar overrides are typically needed here.
|
||||
# ------------------------------------------------------------------------------
|
||||
pipeline:
|
||||
flow_shift: 3 # default: null (model-specific)
|
||||
# flow_shift_sr: null # default: null (super-resolution shift)
|
||||
# embedded_cfg_scale: 6.0 # default: 6.0
|
||||
# is_causal: false # default: false
|
||||
# vae_tiling: true # default: true
|
||||
# vae_sp: true # default: true
|
||||
# disable_autocast: false # default: false
|
||||
@@ -0,0 +1,74 @@
|
||||
# V3 config: Wan 2.1 T2V 1.3B finetune with VSA (phase 3.4, 0.9 sparsity).
|
||||
#
|
||||
# Uses _target_-based instantiation — each model role is an independent
|
||||
# class instance; the method class is resolved directly from the YAML.
|
||||
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
|
||||
|
||||
training:
|
||||
distributed:
|
||||
num_gpus: 8
|
||||
sp_size: 1
|
||||
tp_size: 1
|
||||
hsdp_replicate_dim: 8
|
||||
hsdp_shard_dim: 1
|
||||
|
||||
data:
|
||||
data_path: data/Wan-Syn_77x448x832_600k
|
||||
dataloader_num_workers: 4
|
||||
train_batch_size: 1
|
||||
training_cfg_rate: 0.0
|
||||
seed: 1000
|
||||
num_latent_t: 20
|
||||
num_height: 448
|
||||
num_width: 832
|
||||
num_frames: 77
|
||||
|
||||
optimizer:
|
||||
learning_rate: 1.0e-6
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
lr_scheduler: constant
|
||||
lr_warmup_steps: 0
|
||||
|
||||
loop:
|
||||
max_train_steps: 4000
|
||||
gradient_accumulation_steps: 1
|
||||
|
||||
checkpoint:
|
||||
output_dir: outputs/phase3.4_wan2.1_finetune_vsa_0.9_v3
|
||||
training_state_checkpointing_steps: 100
|
||||
checkpoints_total_limit: 3
|
||||
resume_from_checkpoint: latest
|
||||
|
||||
tracker:
|
||||
project_name: distillation_wan_r
|
||||
run_name: phase3.4_wan_finetune_vsa_0.9_v3
|
||||
|
||||
model:
|
||||
enable_gradient_checkpointing_type: full
|
||||
|
||||
vsa:
|
||||
sparsity: 0.9
|
||||
|
||||
callbacks:
|
||||
grad_clip:
|
||||
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
|
||||
max_grad_norm: 1.0
|
||||
validation:
|
||||
_target_: fastvideo.train.callbacks.validation.ValidationCallback
|
||||
pipeline_target: fastvideo.pipelines.basic.wan.wan_pipeline.WanPipeline
|
||||
dataset_file: examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/validation_4.json
|
||||
every_steps: 50
|
||||
sampling_steps: [50]
|
||||
guidance_scale: 5.0
|
||||
|
||||
pipeline:
|
||||
flow_shift: 3
|
||||
@@ -0,0 +1,98 @@
|
||||
# Self-Forcing distillation: Wan 2.1 T2V 1.3B Causal
|
||||
#
|
||||
# - Teacher: frozen pretrained Wan 2.1 T2V 1.3B
|
||||
# - Student: trainable causal Wan model
|
||||
# - Critic: trainable, initialized from pretrained weights
|
||||
# - Training: streaming rollout with SDE sampling
|
||||
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanCausalModel
|
||||
init_from: wlsaidhi/SFWan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
transformer_override_safetensor: /mnt/weka/home/hao.zhang/wl/Self-Forcing/diffusers_ode_init/model.safetensors
|
||||
teacher:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-14B-Diffusers
|
||||
trainable: false
|
||||
disable_custom_init_weights: true
|
||||
critic:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
disable_custom_init_weights: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.distribution_matching.self_forcing.SelfForcingMethod
|
||||
rollout_mode: simulate
|
||||
generator_update_interval: 5
|
||||
real_score_guidance_scale: 4.5
|
||||
dmd_denoising_steps: [1000, 750, 500, 250]
|
||||
|
||||
chunk_size: 3
|
||||
student_sample_type: sde
|
||||
context_noise: 0.0
|
||||
enable_gradient_in_rollout: true
|
||||
start_gradient_frame: 0
|
||||
|
||||
# Critic optimizer
|
||||
fake_score_learning_rate: 8.0e-6
|
||||
fake_score_betas: [0.0, 0.999]
|
||||
fake_score_lr_scheduler: constant
|
||||
|
||||
training:
|
||||
distributed:
|
||||
num_gpus: 8
|
||||
sp_size: 1
|
||||
tp_size: 1
|
||||
hsdp_replicate_dim: 1
|
||||
hsdp_shard_dim: 8
|
||||
|
||||
data:
|
||||
data_path: data/Wan-Syn_77x448x832_600k
|
||||
dataloader_num_workers: 4
|
||||
train_batch_size: 1
|
||||
training_cfg_rate: 0.0
|
||||
seed: 1000
|
||||
num_latent_t: 20
|
||||
num_height: 448
|
||||
num_width: 832
|
||||
num_frames: 77
|
||||
|
||||
optimizer:
|
||||
learning_rate: 1e-5
|
||||
betas: [0.0, 0.999]
|
||||
weight_decay: 0.01
|
||||
lr_scheduler: constant
|
||||
lr_warmup_steps: 0
|
||||
|
||||
loop:
|
||||
max_train_steps: 4000
|
||||
gradient_accumulation_steps: 1
|
||||
|
||||
checkpoint:
|
||||
output_dir: outputs/wan2.1_causal_self_forcing
|
||||
training_state_checkpointing_steps: 1000
|
||||
checkpoints_total_limit: 3
|
||||
|
||||
tracker:
|
||||
project_name: self-forcing-wan
|
||||
run_name: wan2.1_causal_self_forcing
|
||||
|
||||
model:
|
||||
enable_gradient_checkpointing_type: full
|
||||
|
||||
callbacks:
|
||||
grad_clip:
|
||||
max_grad_norm: 1.0
|
||||
validation:
|
||||
pipeline_target: fastvideo.pipelines.basic.wan.wan_causal_dmd_pipeline.WanCausalDMDPipeline
|
||||
dataset_file: examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/validation_4.json
|
||||
every_steps: 50
|
||||
sampling_steps: [4]
|
||||
sampling_timesteps: [1000, 750, 500, 250]
|
||||
num_frames: 69
|
||||
guidance_scale: 6.0
|
||||
|
||||
pipeline:
|
||||
flow_shift: 5
|
||||
@@ -0,0 +1,105 @@
|
||||
# Self-Forcing distillation: Wan 2.1 T2V 1.3B Causal with VSA (0.9 sparsity)
|
||||
#
|
||||
# - Teacher: frozen pretrained Wan 2.1 T2V 14B
|
||||
# - Student: trainable causal Wan model with VSA sparse attention
|
||||
# - Critic: trainable, initialized from pretrained weights
|
||||
# - Training: streaming rollout with SDE sampling + VSA 0.9 sparsity
|
||||
|
||||
models:
|
||||
student:
|
||||
_target_: fastvideo.train.models.wan.WanCausalModel
|
||||
init_from: wlsaidhi/SFWan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
transformer_override_safetensor: /mnt/weka/home/hao.zhang/wl/Self-Forcing/diffusers_ode_init/model.safetensors
|
||||
teacher:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-14B-Diffusers
|
||||
trainable: false
|
||||
disable_custom_init_weights: true
|
||||
critic:
|
||||
_target_: fastvideo.train.models.wan.WanModel
|
||||
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
trainable: true
|
||||
disable_custom_init_weights: true
|
||||
|
||||
method:
|
||||
_target_: fastvideo.train.methods.distribution_matching.self_forcing.SelfForcingMethod
|
||||
rollout_mode: simulate
|
||||
generator_update_interval: 5
|
||||
real_score_guidance_scale: 4.5
|
||||
dmd_denoising_steps: [1000, 750, 500, 250]
|
||||
|
||||
chunk_size: 3
|
||||
student_sample_type: sde
|
||||
context_noise: 0.0
|
||||
enable_gradient_in_rollout: true
|
||||
start_gradient_frame: 0
|
||||
|
||||
# Critic optimizer
|
||||
fake_score_learning_rate: 8.0e-6
|
||||
fake_score_betas: [0.0, 0.999]
|
||||
fake_score_lr_scheduler: constant
|
||||
|
||||
attn_kind: vsa
|
||||
|
||||
training:
|
||||
distributed:
|
||||
num_gpus: 8
|
||||
sp_size: 1
|
||||
tp_size: 1
|
||||
hsdp_replicate_dim: 1
|
||||
hsdp_shard_dim: 8
|
||||
|
||||
data:
|
||||
data_path: data/Wan-Syn_77x448x832_600k
|
||||
dataloader_num_workers: 4
|
||||
train_batch_size: 1
|
||||
training_cfg_rate: 0.0
|
||||
seed: 1000
|
||||
num_latent_t: 20
|
||||
num_height: 448
|
||||
num_width: 832
|
||||
num_frames: 77
|
||||
|
||||
optimizer:
|
||||
learning_rate: 1e-5
|
||||
betas: [0.0, 0.999]
|
||||
weight_decay: 0.01
|
||||
lr_scheduler: constant
|
||||
lr_warmup_steps: 0
|
||||
|
||||
loop:
|
||||
max_train_steps: 4000
|
||||
gradient_accumulation_steps: 1
|
||||
|
||||
checkpoint:
|
||||
output_dir: outputs/wan2.1_causal_self_forcing_vsa_0.9
|
||||
training_state_checkpointing_steps: 1000
|
||||
checkpoints_total_limit: 3
|
||||
|
||||
tracker:
|
||||
project_name: self-forcing-wan
|
||||
run_name: wan2.1_causal_self_forcing_vsa_0.9
|
||||
|
||||
model:
|
||||
enable_gradient_checkpointing_type: full
|
||||
|
||||
vsa:
|
||||
sparsity: 0.9
|
||||
decay_rate: 0.03
|
||||
decay_interval_steps: 1
|
||||
|
||||
callbacks:
|
||||
grad_clip:
|
||||
max_grad_norm: 1.0
|
||||
validation:
|
||||
pipeline_target: fastvideo.pipelines.basic.wan.wan_causal_dmd_pipeline.WanCausalDMDPipeline
|
||||
dataset_file: examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/validation_4.json
|
||||
every_steps: 50
|
||||
sampling_steps: [4]
|
||||
sampling_timesteps: [1000, 750, 500, 250]
|
||||
num_frames: 69
|
||||
guidance_scale: 6.0
|
||||
|
||||
pipeline:
|
||||
flow_shift: 5
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch training from a YAML config.
|
||||
#
|
||||
# Usage:
|
||||
# bash examples/train/run.sh <config.yaml> [--dotted.key value ...]
|
||||
#
|
||||
# Examples:
|
||||
# bash examples/train/run.sh examples/train/finetune_wan2.1_t2v_1.3B_vsa_phase3.4_0.9sparsity.yaml
|
||||
# bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml --dry-run
|
||||
# bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml \
|
||||
# --training.distributed.num_gpus 4 \
|
||||
# --training.optimizer.learning_rate 1e-5
|
||||
# bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml \
|
||||
# --training.checkpoint.resume_from_checkpoint outputs/my_run/checkpoint-1000
|
||||
#
|
||||
# Logs are written to logs/<config_name>_<timestamp>.log (and also printed to stdout).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG="${1:?Usage: $0 <config.yaml> [extra flags...]}"
|
||||
shift
|
||||
|
||||
# ── GPU / node settings ──────────────────────────────────────────
|
||||
NUM_GPUS="${NUM_GPUS:-$(nvidia-smi -L 2>/dev/null | wc -l)}"
|
||||
NUM_GPUS="${NUM_GPUS:-1}"
|
||||
NNODES="${NNODES:-1}"
|
||||
NODE_RANK="${NODE_RANK:-0}"
|
||||
MASTER_ADDR="${MASTER_ADDR:-127.0.0.1}"
|
||||
MASTER_PORT="${MASTER_PORT:-29501}"
|
||||
export TOKENIZERS_PARALLELISM=false
|
||||
# ── W&B ──────────────────────────────────────────────────────────
|
||||
export WANDB_API_KEY="${WANDB_API_KEY:-}"
|
||||
export WANDB_MODE="${WANDB_MODE:-online}"
|
||||
|
||||
# ── Log file ─────────────────────────────────────────────────────
|
||||
CONFIG_NAME="$(basename "${CONFIG}" .yaml)"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
LOG_DIR="${LOG_DIR:-examples/train}"
|
||||
mkdir -p "${LOG_DIR}"
|
||||
LOG_FILE="${LOG_DIR}/${CONFIG_NAME}_${TIMESTAMP}.log"
|
||||
|
||||
echo "=== Train Training ==="
|
||||
echo "Config: ${CONFIG}"
|
||||
echo "Num GPUs: ${NUM_GPUS}"
|
||||
echo "Num Nodes: ${NNODES}"
|
||||
echo "Node Rank: ${NODE_RANK}"
|
||||
echo "Master: ${MASTER_ADDR}:${MASTER_PORT}"
|
||||
echo "Extra args: $*"
|
||||
echo "Log file: ${LOG_FILE}"
|
||||
echo "=============================="
|
||||
|
||||
python -m torch.distributed.run \
|
||||
--nnodes "${NNODES}" \
|
||||
--node_rank "${NODE_RANK}" \
|
||||
--nproc_per_node "${NUM_GPUS}" \
|
||||
--master_addr "${MASTER_ADDR}" \
|
||||
--master_port "${MASTER_PORT}" \
|
||||
fastvideo/train/entrypoint/train.py \
|
||||
--config "${CONFIG}" \
|
||||
"$@" \
|
||||
2>&1 | tee "${LOG_FILE}"
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Submit a multi-node Slurm training job.
|
||||
#
|
||||
# Usage:
|
||||
# bash examples/train/run_slurm.sh <config.yaml> <num_nodes> [--dotted.key value ...]
|
||||
#
|
||||
# Examples:
|
||||
# bash examples/train/run_slurm.sh examples/train/configs/example.yaml 2
|
||||
# bash examples/train/run_slurm.sh examples/train/configs/distill_wan2.1_t2v_1.3B_dmd2.yaml 4 \
|
||||
# --training.optimizer.learning_rate 1e-5
|
||||
# bash examples/train/run_slurm.sh examples/train/configs/example.yaml 8 \
|
||||
# --training.checkpoint.resume_from_checkpoint outputs/my_run/checkpoint-1000
|
||||
#
|
||||
# Environment variables (override defaults):
|
||||
# PARTITION Slurm partition (default: main)
|
||||
# NUM_GPUS GPUs per node (default: 8)
|
||||
# CPUS_PER_TASK CPUs per task (default: 128)
|
||||
# MEM Memory per node (default: 1440G)
|
||||
# JOB_NAME Slurm job name (default: derived from config)
|
||||
# OUTPUT_DIR Directory for slurm logs (default: slurm_logs)
|
||||
# MASTER_PORT Rendezvous port (default: 29500)
|
||||
# EXCLUDE Nodes to exclude (default: "")
|
||||
# WANDB_API_KEY W&B API key (default: "")
|
||||
# WANDB_MODE W&B mode (default: online)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG="${1:?Usage: $0 <config.yaml> <num_nodes> [extra flags...]}"
|
||||
NUM_NODES="${2:?Usage: $0 <config.yaml> <num_nodes> [extra flags...]}"
|
||||
shift 2
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────
|
||||
PARTITION="${PARTITION:-main}"
|
||||
NUM_GPUS="${NUM_GPUS:-8}"
|
||||
CPUS_PER_TASK="${CPUS_PER_TASK:-128}"
|
||||
MEM="${MEM:-1440G}"
|
||||
MASTER_PORT="${MASTER_PORT:-29500}"
|
||||
EXCLUDE="${EXCLUDE:-}"
|
||||
WANDB_API_KEY="${WANDB_API_KEY:-}"
|
||||
WANDB_MODE="${WANDB_MODE:-online}"
|
||||
|
||||
TOTAL_GPUS=$(( NUM_NODES * NUM_GPUS ))
|
||||
|
||||
CONFIG_NAME="$(basename "${CONFIG}" .yaml)"
|
||||
JOB_NAME="${JOB_NAME:-${CONFIG_NAME}}"
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-logs/slurm}"
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
|
||||
# ── Build sbatch args ─────────────────────────────────────────────
|
||||
SBATCH_ARGS=(
|
||||
--job-name="${JOB_NAME}"
|
||||
--partition="${PARTITION}"
|
||||
--nodes="${NUM_NODES}"
|
||||
--ntasks="${NUM_NODES}"
|
||||
--ntasks-per-node=1
|
||||
--gres="gpu:${NUM_GPUS}"
|
||||
--cpus-per-task="${CPUS_PER_TASK}"
|
||||
--mem="${MEM}"
|
||||
--output="${OUTPUT_DIR}/${JOB_NAME}_%j.out"
|
||||
--error="${OUTPUT_DIR}/${JOB_NAME}_%j.err"
|
||||
--exclusive
|
||||
)
|
||||
|
||||
if [[ -n "${EXCLUDE}" ]]; then
|
||||
SBATCH_ARGS+=(--exclude="${EXCLUDE}")
|
||||
fi
|
||||
|
||||
# ── Collect extra overrides for the training script ───────────────
|
||||
EXTRA_ARGS=("$@")
|
||||
|
||||
echo "=== Slurm Training Submission ==="
|
||||
echo "Config: ${CONFIG}"
|
||||
echo "Nodes: ${NUM_NODES}"
|
||||
echo "GPUs/node: ${NUM_GPUS}"
|
||||
echo "Total GPUs: ${TOTAL_GPUS}"
|
||||
echo "Partition: ${PARTITION}"
|
||||
echo "Job name: ${JOB_NAME}"
|
||||
echo "Extra args: ${EXTRA_ARGS[*]:-}"
|
||||
echo "================================="
|
||||
|
||||
# ── Submit ────────────────────────────────────────────────────────
|
||||
sbatch "${SBATCH_ARGS[@]}" <<EOF
|
||||
#!/bin/bash
|
||||
set -e -x
|
||||
|
||||
# ── Environment ───────────────────────────────────────────────────
|
||||
export NCCL_P2P_DISABLE=1
|
||||
export TORCH_NCCL_ENABLE_MONITORING=0
|
||||
export TRITON_CACHE_DIR=/tmp/triton_cache_\${SLURM_PROCID}
|
||||
export TOKENIZERS_PARALLELISM=false
|
||||
export WANDB_API_KEY="${WANDB_API_KEY}"
|
||||
export WANDB_MODE="${WANDB_MODE}"
|
||||
|
||||
# ── Rendezvous ────────────────────────────────────────────────────
|
||||
export MASTER_PORT=${MASTER_PORT}
|
||||
nodes=( \$(scontrol show hostnames \$SLURM_JOB_NODELIST) )
|
||||
export MASTER_ADDR=\${nodes[0]}
|
||||
export NODE_RANK=\$SLURM_PROCID
|
||||
|
||||
echo "MASTER_ADDR: \$MASTER_ADDR"
|
||||
echo "NODE_RANK: \$NODE_RANK"
|
||||
|
||||
# ── Launch ────────────────────────────────────────────────────────
|
||||
srun torchrun \\
|
||||
--nnodes \$SLURM_JOB_NUM_NODES \\
|
||||
--nproc_per_node ${NUM_GPUS} \\
|
||||
--node_rank \$SLURM_PROCID \\
|
||||
--rdzv_backend=c10d \\
|
||||
--rdzv_endpoint="\$MASTER_ADDR:\$MASTER_PORT" \\
|
||||
fastvideo/train/entrypoint/train.py \\
|
||||
--config ${CONFIG} \\
|
||||
--training.distributed.num_gpus ${TOTAL_GPUS} \\
|
||||
${EXTRA_ARGS[*]:-}
|
||||
EOF
|
||||
@@ -2,35 +2,15 @@
|
||||
"data": [
|
||||
{
|
||||
"caption": "In the video, a woman is elegantly showcasing her earrings, bringing attention to their intricate design with a gentle touch of her fingers. She is bathed in ambient purple and pink lighting, which casts a soft glow on her delicate features and enhances the vivid tones of her lipstick and eye makeup. Her hair is styled to frame her face smoothly, emphasizing the contours of her jawline and cheekbones. The background features a blurred neon light, adding an artistic and modern touch to the overall aesthetic.",
|
||||
"video_path": "Fashion/mixkit-face-of-an-elegant-and-captivating-woman-41914_clip_1.mp4",
|
||||
"num_inference_steps": 3,
|
||||
"height": 448,
|
||||
"width": 832,
|
||||
"num_frames": 61
|
||||
},
|
||||
{
|
||||
"caption": "In the video, a lone rider guides a majestic horse across an expansive, open field as the sun sets in the background. The rider, dressed in a classic blue shirt and wide-brimmed hat, sits confidently in the saddle, silhouetted against the warm glow of the evening sky. The horse moves gracefully, its mane and tail flowing with each step, creating a sense of harmony between horse and rider. Surrounding the pair, towering trees form a natural border, their leaves gently rustling in the breeze. The shadows lengthen on the ground, accentuating the serene and timeless feel of the scene. The distant hills and wooden fences frame the horizon, adding depth to the tranquil landscape. A few horses graze peacefully in the background, blending into the pastoral setting. The overall ambiance evokes a sense of calmness and quietude, capturing a perfect moment in the golden light of dusk.",
|
||||
"video_path": "Man/mixkit-a-rancher-riding-a-horse-at-sunset-1143_clip_1.mp4",
|
||||
"num_inference_steps": 3,
|
||||
"height": 448,
|
||||
"width": 832,
|
||||
"num_frames": 61
|
||||
},
|
||||
{
|
||||
"caption": "In a dimly lit, eerie setting, a mysterious pink bottle labeled \"Authentic 100% organic POISON\" sits prominently in the foreground, casting a menacing aura. The bottle is accentuated by green fog, which swirls lightly around it, enhancing its sinister allure. Behind it, a shadowy golden bottle adorned with a spider emblem subtly emerges, adding an extra layer of mystery to the scene. Dim candles provide faint, flickering light, which complements the dark atmosphere, making the setting ideal for an illusion of hidden dangers.",
|
||||
"video_path": "smoke/mixkit-poison-in-halloween-ritual-33879_clip_1.mp4",
|
||||
"num_inference_steps": 3,
|
||||
"height": 448,
|
||||
"width": 832,
|
||||
"num_frames": 61
|
||||
},
|
||||
{
|
||||
"caption": "The video opens with a tranquil scene in the heart of a dense forest, emphasizing two large, textured tree trunks in the foreground framing the view. Sunlight filters through the canopy above, casting intricate patterns of light and shadow on the trees and the ground. Between the tree trunks, a clear view of a calm, muddy river unfolds, its surface shimmering under the gentle sunlight. The riverbank is decorated with a variety of small bushes and vibrant foliage, subtly transitioning into the deep greens of tall, leafy plants. In the background, the dense forest looms, filled with dark, towering trees, their branches intertwining to form an intricate canopy. The scene is bathed in the soft glow of the sun, creating a serene and picturesque setting. Occasional sunbeams pierce through the foliage, adding a magical aura to the landscape. The vibrant reds and oranges of the smaller plants add contrast, bringing warmth to the earthy tones of the scenery. Overall, this harmonious blend of natural elements creates a peaceful and idyllic forest setting.",
|
||||
"video_path": "forest/mixkit-view-of-a-river-between-two-old-trees-560_clip_1.mp4",
|
||||
"num_inference_steps": 3,
|
||||
"height": 448,
|
||||
"width": 832,
|
||||
"num_frames": 61
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import math
|
||||
import torch
|
||||
from .block_sparse_attn import block_sparse_attn
|
||||
from .triton_kernels.block_sparse_attn_triton import triton_block_sparse_attn_forward
|
||||
from .triton_kernels.st_attn_triton import sliding_tile_attention_triton
|
||||
from .triton_kernels.index import map_to_index
|
||||
|
||||
# Try to load the C++ extension
|
||||
try:
|
||||
from fastvideo_kernel._C import fastvideo_kernel_ops
|
||||
sta_fwd = getattr(fastvideo_kernel_ops, "sta_fwd", None)
|
||||
block_sparse_fwd = getattr(fastvideo_kernel_ops, "block_sparse_fwd", None)
|
||||
block_sparse_bwd = getattr(fastvideo_kernel_ops, "block_sparse_bwd", None)
|
||||
except ImportError:
|
||||
sta_fwd = None
|
||||
block_sparse_fwd = None
|
||||
block_sparse_bwd = None
|
||||
|
||||
|
||||
def sliding_tile_attention(
|
||||
q: torch.Tensor,
|
||||
@@ -135,14 +130,8 @@ def video_sparse_attn(
|
||||
mask = torch.zeros_like(scores,
|
||||
dtype=torch.bool).scatter_(-1, topk_idx, True)
|
||||
|
||||
idx, num = map_to_index(mask)
|
||||
|
||||
if block_sparse_fwd is not None:
|
||||
# Use autograd-enabled wrapper so backward works (and still uses SM90 kernel when available)
|
||||
out_s = block_sparse_attn(q, k, v, mask, variable_block_sizes)[0]
|
||||
else:
|
||||
# Triton-only forward (kept for environments without the wrapper deps)
|
||||
out_s, _ = triton_block_sparse_attn_forward(q, k, v, idx, num, variable_block_sizes)
|
||||
# out_s = block_sparse_attn(q, k, v, mask, variable_block_sizes)[0]
|
||||
out_s = block_sparse_attn(q, k, v, mask, variable_block_sizes)[0]
|
||||
|
||||
if compress_attn_weight is not None:
|
||||
return out_c * compress_attn_weight + out_s
|
||||
|
||||
@@ -4,26 +4,36 @@ import torch
|
||||
import torch.nn.functional as F
|
||||
from flash_attn import flash_attn_func as flash_attn_2_func
|
||||
from dataclasses import dataclass
|
||||
|
||||
try:
|
||||
from flash_attn_interface import flash_attn_func as flash_attn_3_func
|
||||
from fastvideo.attention.utils.flash_attn_cute import flash_attn_func
|
||||
|
||||
# flash_attn 3 no longer have a different API, see following commit:
|
||||
# https://github.com/Dao-AILab/flash-attention/commit/ed209409acedbb2379f870bbd03abce31a7a51b7
|
||||
flash_attn_func = flash_attn_3_func
|
||||
fa_version = "4"
|
||||
except ImportError:
|
||||
flash_attn_func = flash_attn_2_func
|
||||
try:
|
||||
from flash_attn_interface import flash_attn_func as flash_attn_3_func
|
||||
|
||||
from fastvideo.attention.backends.abstract import (AttentionBackend,
|
||||
AttentionImpl,
|
||||
AttentionMetadata,
|
||||
AttentionMetadataBuilder)
|
||||
# flash_attn 3 no longer have a different API, see following commit:
|
||||
# https://github.com/Dao-AILab/flash-attention/commit/ed209409acedbb2379f870bbd03abce31a7a51b7
|
||||
flash_attn_func = flash_attn_3_func
|
||||
fa_version = "3"
|
||||
except ImportError:
|
||||
flash_attn_func = flash_attn_2_func
|
||||
fa_version = "2"
|
||||
|
||||
from fastvideo.attention.backends.abstract import (
|
||||
AttentionBackend,
|
||||
AttentionImpl,
|
||||
AttentionMetadata,
|
||||
AttentionMetadataBuilder,
|
||||
)
|
||||
from fastvideo.logger import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
logger.info("Using FlashAttention-%s backend", fa_version)
|
||||
|
||||
|
||||
class FlashAttentionBackend(AttentionBackend):
|
||||
|
||||
accept_output_buffer: bool = True
|
||||
|
||||
@staticmethod
|
||||
@@ -117,11 +127,13 @@ class FlashAttentionImpl(AttentionImpl):
|
||||
f"expected {key_len}, got {key_padding_mask.shape[-1]}")
|
||||
return key_padding_mask
|
||||
|
||||
if attn_metadata is not None and hasattr(
|
||||
attn_metadata,
|
||||
"attn_mask") and attn_metadata.attn_mask is not None:
|
||||
if (attn_metadata is not None and hasattr(attn_metadata, "attn_mask")
|
||||
and attn_metadata.attn_mask is not None):
|
||||
from fastvideo.attention.utils.flash_attn_no_pad import (
|
||||
flash_attn_no_pad, flash_attn_varlen_qk_no_pad)
|
||||
flash_attn_no_pad,
|
||||
flash_attn_varlen_qk_no_pad,
|
||||
)
|
||||
|
||||
attn_mask = attn_metadata.attn_mask
|
||||
|
||||
# flash_attn_no_pad packs q/k/v as one tensor and assumes equal q/k
|
||||
@@ -160,5 +172,6 @@ class FlashAttentionImpl(AttentionImpl):
|
||||
key,
|
||||
value,
|
||||
softmax_scale=self.softmax_scale,
|
||||
causal=self.causal)
|
||||
causal=self.causal,
|
||||
)
|
||||
return output
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
from flash_attn.cute.interface import _flash_attn_bwd, _flash_attn_fwd
|
||||
else:
|
||||
# This error will be caught in flash_attn.py or flash_attn_no_pad.py
|
||||
raise ImportError(
|
||||
"flash_attn.cute is only available on CUDA devices; this error must be handled internally"
|
||||
)
|
||||
|
||||
|
||||
def _check_dropout(dropout_p: float) -> None:
|
||||
if dropout_p != 0.0:
|
||||
raise NotImplementedError(
|
||||
f"flash_attn.cute does not support dropout (got dropout_p={dropout_p})"
|
||||
)
|
||||
|
||||
|
||||
@torch.library.custom_op(
|
||||
"fastvideo::_flash_attn_cute_forward",
|
||||
mutates_args=(),
|
||||
device_types="cuda",
|
||||
)
|
||||
def _flash_attn_cute_forward(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
softmax_scale: float | None,
|
||||
causal: bool,
|
||||
deterministic: bool,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
out, lse = _flash_attn_fwd(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
softmax_scale=softmax_scale,
|
||||
causal=causal,
|
||||
window_size_left=None,
|
||||
window_size_right=None,
|
||||
softcap=0.0,
|
||||
num_splits=1,
|
||||
pack_gqa=None,
|
||||
)
|
||||
return out, lse
|
||||
|
||||
|
||||
@torch.library.register_fake("fastvideo::_flash_attn_cute_forward")
|
||||
def _flash_attn_cute_forward_fake(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
softmax_scale: float | None,
|
||||
causal: bool,
|
||||
deterministic: bool,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
del k, softmax_scale, causal, deterministic
|
||||
batch, seqlen_q, nheads = q.shape[:3]
|
||||
out = q.new_empty(batch, seqlen_q, nheads, v.shape[-1])
|
||||
lse = q.new_empty(batch, nheads, seqlen_q, dtype=torch.float32)
|
||||
return out, lse
|
||||
|
||||
|
||||
def _flash_attn_cute_setup_context(ctx: torch.autograd.function.FunctionCtx,
|
||||
inputs, output) -> None:
|
||||
q, k, v, softmax_scale, causal, deterministic = inputs
|
||||
out, lse = output
|
||||
ctx.save_for_backward(q, k, v, out, lse)
|
||||
ctx.softmax_scale = softmax_scale
|
||||
ctx.causal = causal
|
||||
ctx.deterministic = deterministic
|
||||
|
||||
|
||||
def _flash_attn_cute_backward(
|
||||
ctx: torch.autograd.function.FunctionCtx,
|
||||
grad_out: torch.Tensor,
|
||||
grad_lse: torch.Tensor | None,
|
||||
):
|
||||
del grad_lse
|
||||
q, k, v, out, lse = ctx.saved_tensors
|
||||
dq, dk, dv = _flash_attn_bwd(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
out,
|
||||
grad_out,
|
||||
lse,
|
||||
softmax_scale=ctx.softmax_scale,
|
||||
causal=ctx.causal,
|
||||
softcap=0.0,
|
||||
window_size_left=None,
|
||||
window_size_right=None,
|
||||
deterministic=ctx.deterministic,
|
||||
)
|
||||
return dq, dk, dv, None, None, None
|
||||
|
||||
|
||||
torch.library.register_autograd(
|
||||
"fastvideo::_flash_attn_cute_forward",
|
||||
_flash_attn_cute_backward,
|
||||
setup_context=_flash_attn_cute_setup_context,
|
||||
)
|
||||
|
||||
|
||||
@torch.library.custom_op(
|
||||
"fastvideo::_flash_attn_cute_varlen_forward",
|
||||
mutates_args=(),
|
||||
device_types="cuda",
|
||||
)
|
||||
def _flash_attn_cute_varlen_forward(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
cu_seqlens_q: torch.Tensor,
|
||||
cu_seqlens_k: torch.Tensor,
|
||||
max_seqlen_q: int,
|
||||
max_seqlen_k: int,
|
||||
softmax_scale: float | None,
|
||||
causal: bool,
|
||||
deterministic: bool,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
out, lse = _flash_attn_fwd(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
cu_seqlens_q=cu_seqlens_q,
|
||||
cu_seqlens_k=cu_seqlens_k,
|
||||
max_seqlen_q=max_seqlen_q,
|
||||
max_seqlen_k=max_seqlen_k,
|
||||
softmax_scale=softmax_scale,
|
||||
causal=causal,
|
||||
window_size_left=None,
|
||||
window_size_right=None,
|
||||
softcap=0.0,
|
||||
num_splits=1,
|
||||
pack_gqa=None,
|
||||
)
|
||||
return out, lse
|
||||
|
||||
|
||||
@torch.library.register_fake("fastvideo::_flash_attn_cute_varlen_forward")
|
||||
def _flash_attn_cute_varlen_forward_fake(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
cu_seqlens_q: torch.Tensor,
|
||||
cu_seqlens_k: torch.Tensor,
|
||||
max_seqlen_q: int,
|
||||
max_seqlen_k: int,
|
||||
softmax_scale: float | None,
|
||||
causal: bool,
|
||||
deterministic: bool,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
del k, cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k, softmax_scale
|
||||
del causal
|
||||
del deterministic
|
||||
total_q, nheads = q.shape[:2]
|
||||
out = q.new_empty(total_q, nheads, v.shape[-1])
|
||||
lse = q.new_empty(nheads, total_q, dtype=torch.float32)
|
||||
return out, lse
|
||||
|
||||
|
||||
def _flash_attn_cute_varlen_setup_context(
|
||||
ctx: torch.autograd.function.FunctionCtx, inputs, output) -> None:
|
||||
(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
cu_seqlens_q,
|
||||
cu_seqlens_k,
|
||||
max_seqlen_q,
|
||||
max_seqlen_k,
|
||||
softmax_scale,
|
||||
causal,
|
||||
deterministic,
|
||||
) = inputs
|
||||
out, lse = output
|
||||
ctx.save_for_backward(q, k, v, out, lse, cu_seqlens_q, cu_seqlens_k)
|
||||
ctx.max_seqlen_q = max_seqlen_q
|
||||
ctx.max_seqlen_k = max_seqlen_k
|
||||
ctx.softmax_scale = softmax_scale
|
||||
ctx.causal = causal
|
||||
ctx.deterministic = deterministic
|
||||
|
||||
|
||||
def _flash_attn_cute_varlen_backward(
|
||||
ctx: torch.autograd.function.FunctionCtx,
|
||||
grad_out: torch.Tensor,
|
||||
grad_lse: torch.Tensor | None,
|
||||
):
|
||||
del grad_lse
|
||||
q, k, v, out, lse, cu_seqlens_q, cu_seqlens_k = ctx.saved_tensors
|
||||
dq, dk, dv = _flash_attn_bwd(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
out,
|
||||
grad_out,
|
||||
lse,
|
||||
softmax_scale=ctx.softmax_scale,
|
||||
causal=ctx.causal,
|
||||
softcap=0.0,
|
||||
window_size_left=None,
|
||||
window_size_right=None,
|
||||
cu_seqlens_q=cu_seqlens_q,
|
||||
cu_seqlens_k=cu_seqlens_k,
|
||||
max_seqlen_q=ctx.max_seqlen_q,
|
||||
max_seqlen_k=ctx.max_seqlen_k,
|
||||
deterministic=ctx.deterministic,
|
||||
)
|
||||
return dq, dk, dv, None, None, None, None, None, None, None
|
||||
|
||||
|
||||
torch.library.register_autograd(
|
||||
"fastvideo::_flash_attn_cute_varlen_forward",
|
||||
_flash_attn_cute_varlen_backward,
|
||||
setup_context=_flash_attn_cute_varlen_setup_context,
|
||||
)
|
||||
|
||||
|
||||
def flash_attn_func(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
dropout_p: float = 0.0,
|
||||
softmax_scale: float | None = None,
|
||||
causal: bool = False,
|
||||
deterministic: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Only returns the output, not the lse."""
|
||||
_check_dropout(dropout_p)
|
||||
out, _ = torch.ops.fastvideo._flash_attn_cute_forward(
|
||||
q, k, v, softmax_scale, causal, deterministic)
|
||||
return out
|
||||
|
||||
|
||||
def flash_attn_varlen_func(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
cu_seqlens_q: torch.Tensor,
|
||||
cu_seqlens_k: torch.Tensor,
|
||||
max_seqlen_q: int,
|
||||
max_seqlen_k: int,
|
||||
dropout_p: float = 0.0,
|
||||
softmax_scale: float | None = None,
|
||||
causal: bool = False,
|
||||
deterministic: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Only returns the output, not the lse."""
|
||||
_check_dropout(dropout_p)
|
||||
out, _ = torch.ops.fastvideo._flash_attn_cute_varlen_forward(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
cu_seqlens_q,
|
||||
cu_seqlens_k,
|
||||
max_seqlen_q,
|
||||
max_seqlen_k,
|
||||
softmax_scale,
|
||||
causal,
|
||||
deterministic,
|
||||
)
|
||||
return out
|
||||
@@ -15,16 +15,29 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from einops import rearrange
|
||||
from flash_attn.bert_padding import pad_input, unpad_input
|
||||
from flash_attn import flash_attn_varlen_qkvpacked_func
|
||||
|
||||
try:
|
||||
from fastvideo.attention.utils.flash_attn_cute import (
|
||||
flash_attn_varlen_func as flash_attn_varlen_func_impl, )
|
||||
except ImportError:
|
||||
try:
|
||||
from flash_attn_interface import (
|
||||
flash_attn_varlen_func as flash_attn_varlen_func_impl, )
|
||||
except ImportError:
|
||||
from flash_attn import (
|
||||
flash_attn_varlen_func as flash_attn_varlen_func_impl, )
|
||||
|
||||
|
||||
def flash_attn_no_pad(qkv,
|
||||
key_padding_mask,
|
||||
causal=False,
|
||||
dropout_p=0.0,
|
||||
softmax_scale=None,
|
||||
deterministic=False):
|
||||
from flash_attn import flash_attn_varlen_qkvpacked_func
|
||||
from flash_attn.bert_padding import pad_input, unpad_input
|
||||
def flash_attn_no_pad(
|
||||
qkv,
|
||||
key_padding_mask,
|
||||
causal=False,
|
||||
dropout_p=0.0,
|
||||
softmax_scale=None,
|
||||
deterministic=False,
|
||||
):
|
||||
batch_size = qkv.shape[0]
|
||||
seqlen = qkv.shape[1]
|
||||
nheads = qkv.shape[-2]
|
||||
@@ -46,22 +59,28 @@ def flash_attn_no_pad(qkv,
|
||||
deterministic=deterministic,
|
||||
)
|
||||
output = rearrange(
|
||||
pad_input(rearrange(output_unpad, "nnz h d -> nnz (h d)"), indices,
|
||||
batch_size, seqlen),
|
||||
pad_input(
|
||||
rearrange(output_unpad, "nnz h d -> nnz (h d)"),
|
||||
indices,
|
||||
batch_size,
|
||||
seqlen,
|
||||
),
|
||||
"b s (h d) -> b s h d",
|
||||
h=nheads,
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def flash_attn_no_pad_v3(qkv,
|
||||
key_padding_mask,
|
||||
causal=False,
|
||||
dropout_p=0.0,
|
||||
softmax_scale=None,
|
||||
deterministic=False):
|
||||
from flash_attn.bert_padding import pad_input, unpad_input
|
||||
from flash_attn_interface import flash_attn_varlen_func as flash_attn_varlen_func_v3
|
||||
def flash_attn_no_pad_v3(
|
||||
qkv,
|
||||
key_padding_mask,
|
||||
causal=False,
|
||||
dropout_p=0.0,
|
||||
softmax_scale=None,
|
||||
deterministic=False,
|
||||
):
|
||||
from flash_attn_interface import (
|
||||
flash_attn_varlen_func as flash_attn_varlen_func_v3, )
|
||||
|
||||
if flash_attn_varlen_func_v3 is None:
|
||||
raise ImportError("FlashAttention V3 backend not available")
|
||||
@@ -80,22 +99,29 @@ def flash_attn_no_pad_v3(qkv,
|
||||
key_unpad = rearrange(key_unpad, "nnz (h d) -> nnz h d", h=nheads)
|
||||
value_unpad = rearrange(value_unpad, "nnz (h d) -> nnz h d", h=nheads)
|
||||
|
||||
output_unpad = flash_attn_varlen_func_v3(query_unpad,
|
||||
key_unpad,
|
||||
value_unpad,
|
||||
cu_seqlens_q,
|
||||
cu_seqlens_k,
|
||||
max_seqlen_q,
|
||||
max_seqlen_q,
|
||||
softmax_scale=softmax_scale,
|
||||
causal=causal,
|
||||
deterministic=deterministic)
|
||||
output_unpad = flash_attn_varlen_func_v3(
|
||||
query_unpad,
|
||||
key_unpad,
|
||||
value_unpad,
|
||||
cu_seqlens_q,
|
||||
cu_seqlens_k,
|
||||
max_seqlen_q,
|
||||
max_seqlen_q,
|
||||
softmax_scale=softmax_scale,
|
||||
causal=causal,
|
||||
deterministic=deterministic,
|
||||
)
|
||||
|
||||
output = rearrange(pad_input(
|
||||
rearrange(output_unpad, "nnz h d -> nnz (h d)"), indices, batch_size,
|
||||
seqlen),
|
||||
"b s (h d) -> b s h d",
|
||||
h=nheads)
|
||||
output = rearrange(
|
||||
pad_input(
|
||||
rearrange(output_unpad, "nnz h d -> nnz (h d)"),
|
||||
indices,
|
||||
batch_size,
|
||||
seqlen,
|
||||
),
|
||||
"b s (h d) -> b s h d",
|
||||
h=nheads,
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
@@ -110,16 +136,6 @@ def flash_attn_varlen_qk_no_pad(
|
||||
softmax_scale=None,
|
||||
deterministic=False,
|
||||
):
|
||||
from flash_attn.bert_padding import pad_input, unpad_input
|
||||
|
||||
try:
|
||||
from flash_attn_interface import flash_attn_varlen_func as flash_attn_varlen_func_impl
|
||||
except ImportError:
|
||||
from flash_attn import flash_attn_varlen_func as flash_attn_varlen_func_impl
|
||||
|
||||
if flash_attn_varlen_func_impl is None:
|
||||
raise ImportError("FlashAttention varlen backend not available")
|
||||
|
||||
batch_size, q_seqlen, nheads, _ = query.shape
|
||||
|
||||
query_unpad, q_indices, cu_seqlens_q, max_seqlen_q, _ = unpad_input(
|
||||
@@ -148,8 +164,12 @@ def flash_attn_varlen_qk_no_pad(
|
||||
)
|
||||
|
||||
output = rearrange(
|
||||
pad_input(rearrange(output_unpad, "nnz h d -> nnz (h d)"), q_indices,
|
||||
batch_size, q_seqlen),
|
||||
pad_input(
|
||||
rearrange(output_unpad, "nnz h d -> nnz (h d)"),
|
||||
q_indices,
|
||||
batch_size,
|
||||
q_seqlen,
|
||||
),
|
||||
"b s (h d) -> b s h d",
|
||||
h=nheads,
|
||||
)
|
||||
|
||||
@@ -343,11 +343,16 @@ class LatentsParquetMapStyleDataset(Dataset):
|
||||
for idx in indices
|
||||
]
|
||||
|
||||
# Inject sample indices for deterministic CFG dropout
|
||||
# that is reproducible across checkpoint resume.
|
||||
for row, idx in zip(rows, indices):
|
||||
row["_sample_index"] = idx
|
||||
|
||||
batch = collate_rows_from_parquet_schema(rows,
|
||||
self.parquet_schema,
|
||||
self.text_padding_length,
|
||||
cfg_rate=self.cfg_rate,
|
||||
rng=self.rng)
|
||||
seed=self.seed)
|
||||
return batch
|
||||
|
||||
def __len__(self):
|
||||
@@ -387,7 +392,6 @@ def build_parquet_map_style_dataloader(
|
||||
collate_fn=passthrough,
|
||||
num_workers=num_data_workers,
|
||||
pin_memory=True,
|
||||
pin_memory_device=current_platform.device_name,
|
||||
persistent_workers=num_data_workers > 0,
|
||||
)
|
||||
return dataset, loader
|
||||
|
||||
@@ -100,7 +100,8 @@ def collate_rows_from_parquet_schema(rows,
|
||||
parquet_schema,
|
||||
text_padding_length,
|
||||
cfg_rate=0.0,
|
||||
rng=None) -> dict[str, Any]:
|
||||
rng=None,
|
||||
seed=0) -> dict[str, Any]:
|
||||
"""
|
||||
Collate rows from parquet files based on the provided schema.
|
||||
Dynamically processes tensor fields based on schema and returns batched data.
|
||||
@@ -149,13 +150,30 @@ def collate_rows_from_parquet_schema(rows,
|
||||
if len(bytes_data) == 0:
|
||||
tensor = torch.zeros(0, dtype=torch.bfloat16)
|
||||
else:
|
||||
# Convert bytes to tensor using float32 as default
|
||||
if tensor_name == 'text_embedding' and (rng.random(
|
||||
) if rng else random.random()) < cfg_rate:
|
||||
data = np.zeros((512, 4096), dtype=np.float32)
|
||||
# Deterministic per-sample CFG dropout
|
||||
# using sample index (resume-safe).
|
||||
drop = False
|
||||
if (tensor_name == 'text_embedding'
|
||||
and cfg_rate > 0):
|
||||
sample_idx = row.get(
|
||||
"_sample_index")
|
||||
if sample_idx is not None:
|
||||
drop = (random.Random(
|
||||
seed ^ sample_idx
|
||||
).random() < cfg_rate)
|
||||
else:
|
||||
drop = ((rng.random()
|
||||
if rng else
|
||||
random.random())
|
||||
< cfg_rate)
|
||||
if drop:
|
||||
data = np.zeros(
|
||||
(512, 4096), dtype=np.float32)
|
||||
else:
|
||||
data = np.frombuffer(
|
||||
bytes_data, dtype=np.float32).reshape(shape).copy()
|
||||
bytes_data,
|
||||
dtype=np.float32,
|
||||
).reshape(shape).copy()
|
||||
tensor = torch.from_numpy(data)
|
||||
# if len(data.shape) == 3:
|
||||
# B, L, D = tensor.shape
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Wan causal pipeline with standard multi-step denoising.
|
||||
|
||||
Block-by-block causal inference with KV caching, using the full
|
||||
scheduler timestep schedule (40-50 steps) rather than DMD few-step.
|
||||
"""
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.schedulers.scheduling_flow_unipc_multistep import (
|
||||
FlowUniPCMultistepScheduler)
|
||||
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
|
||||
# isort: off
|
||||
from fastvideo.pipelines.stages import (
|
||||
CausalDenoisingStage,
|
||||
ConditioningStage,
|
||||
DecodingStage,
|
||||
InputValidationStage,
|
||||
LatentPreparationStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage,
|
||||
)
|
||||
# isort: on
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class WanCausalPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
"""Wan causal pipeline with standard multi-step denoising."""
|
||||
|
||||
_required_config_modules = [
|
||||
"text_encoder",
|
||||
"tokenizer",
|
||||
"vae",
|
||||
"transformer",
|
||||
"scheduler",
|
||||
]
|
||||
|
||||
def initialize_pipeline(
|
||||
self,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
):
|
||||
self.modules["scheduler"] = (FlowUniPCMultistepScheduler(
|
||||
shift=fastvideo_args.pipeline_config.flow_shift, ))
|
||||
|
||||
def create_pipeline_stages(
|
||||
self,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
) -> None:
|
||||
self.add_stage(
|
||||
stage_name="input_validation_stage",
|
||||
stage=InputValidationStage(),
|
||||
)
|
||||
|
||||
self.add_stage(
|
||||
stage_name="prompt_encoding_stage",
|
||||
stage=TextEncodingStage(
|
||||
text_encoders=[self.get_module("text_encoder")],
|
||||
tokenizers=[self.get_module("tokenizer")],
|
||||
),
|
||||
)
|
||||
|
||||
self.add_stage(
|
||||
stage_name="conditioning_stage",
|
||||
stage=ConditioningStage(),
|
||||
)
|
||||
|
||||
self.add_stage(
|
||||
stage_name="timestep_preparation_stage",
|
||||
stage=TimestepPreparationStage(
|
||||
scheduler=self.get_module("scheduler"), ),
|
||||
)
|
||||
|
||||
self.add_stage(
|
||||
stage_name="latent_preparation_stage",
|
||||
stage=LatentPreparationStage(
|
||||
scheduler=self.get_module("scheduler"),
|
||||
transformer=self.get_module("transformer", None),
|
||||
),
|
||||
)
|
||||
|
||||
self.add_stage(
|
||||
stage_name="denoising_stage",
|
||||
stage=CausalDenoisingStage(
|
||||
transformer=self.get_module("transformer"),
|
||||
scheduler=self.get_module("scheduler"),
|
||||
),
|
||||
)
|
||||
|
||||
self.add_stage(
|
||||
stage_name="decoding_stage",
|
||||
stage=DecodingStage(vae=self.get_module("vae")),
|
||||
)
|
||||
|
||||
|
||||
EntryClass = WanCausalPipeline
|
||||
@@ -7,7 +7,8 @@ complete diffusion pipelines.
|
||||
"""
|
||||
|
||||
from fastvideo.pipelines.stages.base import PipelineStage
|
||||
from fastvideo.pipelines.stages.causal_denoising import CausalDMDDenosingStage
|
||||
from fastvideo.pipelines.stages.causal_denoising import (CausalDMDDenosingStage,
|
||||
CausalDenoisingStage)
|
||||
from fastvideo.pipelines.stages.conditioning import ConditioningStage
|
||||
from fastvideo.pipelines.stages.decoding import DecodingStage
|
||||
from fastvideo.pipelines.stages.denoising import (
|
||||
@@ -63,6 +64,7 @@ __all__ = [
|
||||
"DenoisingStage",
|
||||
"DmdDenoisingStage",
|
||||
"CausalDMDDenosingStage",
|
||||
"CausalDenoisingStage",
|
||||
"MatrixGameCausalDenoisingStage",
|
||||
"HYWorldDenoisingStage",
|
||||
"GameCraftDenoisingStage",
|
||||
|
||||
@@ -482,3 +482,190 @@ class CausalDMDDenosingStage(DenoisingStage):
|
||||
"negative_prompt_embeds", batch.negative_prompt_embeds, lambda x:
|
||||
not batch.do_classifier_free_guidance or V.list_not_empty(x))
|
||||
return result
|
||||
|
||||
|
||||
class CausalDenoisingStage(CausalDMDDenosingStage):
|
||||
"""Causal block-by-block denoising with standard multi-step
|
||||
flow matching (scheduler.step), not DMD few-step.
|
||||
|
||||
Each block is fully denoised through all scheduler timesteps
|
||||
before moving to the next block. After each block is denoised,
|
||||
the KV cache is updated with clean context so subsequent blocks
|
||||
can attend to prior clean frames.
|
||||
"""
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
) -> ForwardBatch:
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
and not fastvideo_args.disable_autocast)
|
||||
|
||||
latent_seq_length = (batch.latents.shape[-1] * batch.latents.shape[-2])
|
||||
patch_ratio = (self.transformer.config.arch_config.patch_size[-1] *
|
||||
self.transformer.config.arch_config.patch_size[-2])
|
||||
self.frame_seq_length = latent_seq_length // patch_ratio
|
||||
|
||||
pos_cond_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.transformer.forward,
|
||||
{
|
||||
"encoder_attention_mask": batch.prompt_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
assert batch.latents is not None, ("latents must be provided")
|
||||
latents = batch.latents # [B, C, T, H, W]
|
||||
b, c, t, h, w = latents.shape
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
assert torch.isnan(prompt_embeds[0]).sum() == 0
|
||||
|
||||
num_inference_steps = batch.num_inference_steps
|
||||
|
||||
# Initialize caches
|
||||
kv_cache = self._initialize_kv_cache(
|
||||
batch_size=b,
|
||||
dtype=target_dtype,
|
||||
device=latents.device,
|
||||
)
|
||||
crossattn_cache = self._initialize_crossattn_cache(
|
||||
batch_size=b,
|
||||
max_text_len=(fastvideo_args.pipeline_config.
|
||||
text_encoder_configs[0].arch_config.text_len),
|
||||
dtype=target_dtype,
|
||||
device=latents.device,
|
||||
)
|
||||
|
||||
# Determine block sizes
|
||||
if t % self.num_frames_per_block != 0:
|
||||
raise ValueError("num_frames must be divisible by "
|
||||
"num_frames_per_block")
|
||||
num_blocks = t // self.num_frames_per_block
|
||||
block_sizes = [self.num_frames_per_block] * num_blocks
|
||||
start_index = 0
|
||||
pos_start_base = 0
|
||||
|
||||
context_noise = getattr(
|
||||
fastvideo_args.pipeline_config,
|
||||
"context_noise",
|
||||
0,
|
||||
)
|
||||
|
||||
with self.progress_bar(total=len(block_sizes) *
|
||||
num_inference_steps) as progress_bar:
|
||||
for current_num_frames in block_sizes:
|
||||
current_latents = latents[
|
||||
:,
|
||||
:,
|
||||
start_index:start_index + current_num_frames,
|
||||
:,
|
||||
:,
|
||||
]
|
||||
|
||||
# Reset scheduler per block so its multi-step
|
||||
# history starts fresh.
|
||||
self.scheduler.set_timesteps(
|
||||
num_inference_steps,
|
||||
device=latents.device,
|
||||
)
|
||||
timesteps = self.scheduler.timesteps
|
||||
|
||||
for i, t_cur in enumerate(timesteps):
|
||||
latent_model_input = current_latents.to(target_dtype)
|
||||
t_expanded = t_cur * torch.ones(
|
||||
(b, 1),
|
||||
device=latents.device,
|
||||
dtype=torch.long,
|
||||
)
|
||||
|
||||
with (
|
||||
torch.autocast(
|
||||
device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled,
|
||||
),
|
||||
set_forward_context(
|
||||
current_timestep=i,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
),
|
||||
):
|
||||
# Transformer returns [B, C, T, H, W],
|
||||
# permute to [B, T, C, H, W] then flatten
|
||||
# B*T for the scheduler.
|
||||
noise_pred = self.transformer(
|
||||
latent_model_input,
|
||||
prompt_embeds,
|
||||
t_expanded,
|
||||
kv_cache=kv_cache,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=((pos_start_base + start_index) *
|
||||
self.frame_seq_length),
|
||||
start_frame=start_index,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
|
||||
# Flatten [B,C,T,H,W] -> [B*T,C,H,W] for
|
||||
# scheduler, then unflatten back.
|
||||
nf = current_num_frames
|
||||
noise_pred_flat = (noise_pred.permute(0, 2, 1, 3,
|
||||
4).flatten(0, 1))
|
||||
latents_flat = (current_latents.permute(0, 2, 1, 3,
|
||||
4).flatten(0, 1))
|
||||
updated_flat = self.scheduler.step(
|
||||
noise_pred_flat,
|
||||
t_cur,
|
||||
latents_flat,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
current_latents = (updated_flat.unflatten(
|
||||
0, (b, nf)).permute(0, 2, 1, 3, 4))
|
||||
|
||||
if progress_bar is not None:
|
||||
progress_bar.update()
|
||||
|
||||
# Write denoised block back
|
||||
latents[
|
||||
:,
|
||||
:,
|
||||
start_index:start_index + current_num_frames,
|
||||
:,
|
||||
:,
|
||||
] = current_latents
|
||||
|
||||
# Update KV cache with clean context
|
||||
t_context = torch.ones(
|
||||
[b],
|
||||
device=latents.device,
|
||||
dtype=torch.long,
|
||||
) * int(context_noise)
|
||||
context_bcthw = current_latents.to(target_dtype)
|
||||
with (
|
||||
torch.autocast(
|
||||
device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled,
|
||||
),
|
||||
set_forward_context(
|
||||
current_timestep=0,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
),
|
||||
):
|
||||
self.transformer(
|
||||
context_bcthw,
|
||||
prompt_embeds,
|
||||
t_context.unsqueeze(1),
|
||||
kv_cache=kv_cache,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=((pos_start_base + start_index) *
|
||||
self.frame_seq_length),
|
||||
start_frame=start_index,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
|
||||
start_index += current_num_frames
|
||||
|
||||
batch.latents = latents
|
||||
return batch
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
|
||||
_MODULE_NAME = "_fastvideo_flash_attn_cute_for_tests"
|
||||
|
||||
|
||||
def _load_local_flash_attn_cute_module():
|
||||
if _MODULE_NAME in sys.modules:
|
||||
return sys.modules[_MODULE_NAME]
|
||||
|
||||
module_path = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "attention"
|
||||
/ "utils"
|
||||
/ "flash_attn_cute.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location(_MODULE_NAME, module_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Failed to load module spec from {module_path}")
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[_MODULE_NAME] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _assert_close(
|
||||
actual: torch.Tensor,
|
||||
expected: torch.Tensor,
|
||||
*,
|
||||
dtype: torch.dtype,
|
||||
is_grad: bool = False,
|
||||
) -> None:
|
||||
if dtype == torch.float16:
|
||||
atol = 6e-3 if is_grad else 4e-3
|
||||
rtol = 6e-3 if is_grad else 4e-3
|
||||
else:
|
||||
atol = 2e-2 if is_grad else 1e-2
|
||||
rtol = 2e-2 if is_grad else 1e-2
|
||||
torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
def _extract_out(x):
|
||||
if isinstance(x, tuple):
|
||||
return x[0]
|
||||
return x
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def flash_attn_impls():
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA is required for flash_attn.cute parity tests")
|
||||
|
||||
major, _ = torch.cuda.get_device_capability()
|
||||
if major not in (9, 10, 11):
|
||||
pytest.skip(
|
||||
f"flash_attn.cute supports SM 9/10/11, got compute capability {major}"
|
||||
)
|
||||
|
||||
try:
|
||||
from flash_attn.cute import (
|
||||
flash_attn_func as ref_flash_attn_func,
|
||||
flash_attn_varlen_func as ref_flash_attn_varlen_func,
|
||||
)
|
||||
except ImportError as exc:
|
||||
pytest.skip(f"flash_attn.cute is not available: {exc}")
|
||||
|
||||
mod = _load_local_flash_attn_cute_module()
|
||||
return (
|
||||
mod.flash_attn_func,
|
||||
mod.flash_attn_varlen_func,
|
||||
ref_flash_attn_func,
|
||||
ref_flash_attn_varlen_func,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("causal", [False, True])
|
||||
def test_flash_attn_func_parity_forward_backward(
|
||||
flash_attn_impls, dtype: torch.dtype, causal: bool
|
||||
):
|
||||
if dtype == torch.bfloat16 and not torch.cuda.is_bf16_supported():
|
||||
pytest.skip("bfloat16 is not supported on this GPU")
|
||||
|
||||
custom_flash_attn_func, _, ref_flash_attn_func, _ = flash_attn_impls
|
||||
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda")
|
||||
batch, seqlen, nheads, headdim = 2, 64, 4, 64
|
||||
|
||||
q_ref = torch.randn(
|
||||
batch,
|
||||
seqlen,
|
||||
nheads,
|
||||
headdim,
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
requires_grad=True,
|
||||
)
|
||||
k_ref = torch.randn_like(q_ref, requires_grad=True)
|
||||
v_ref = torch.randn_like(q_ref, requires_grad=True)
|
||||
|
||||
q_test = q_ref.detach().clone().requires_grad_(True)
|
||||
k_test = k_ref.detach().clone().requires_grad_(True)
|
||||
v_test = v_ref.detach().clone().requires_grad_(True)
|
||||
|
||||
out_ref = _extract_out(ref_flash_attn_func(
|
||||
q_ref,
|
||||
k_ref,
|
||||
v_ref,
|
||||
softmax_scale=None,
|
||||
causal=causal,
|
||||
deterministic=False,
|
||||
))
|
||||
out_test = _extract_out(custom_flash_attn_func(
|
||||
q_test,
|
||||
k_test,
|
||||
v_test,
|
||||
dropout_p=0.0,
|
||||
softmax_scale=None,
|
||||
causal=causal,
|
||||
deterministic=False,
|
||||
))
|
||||
|
||||
_assert_close(out_test, out_ref, dtype=dtype)
|
||||
|
||||
dout = torch.randn_like(out_ref)
|
||||
dq_ref, dk_ref, dv_ref = torch.autograd.grad(
|
||||
(out_ref * dout).sum(), (q_ref, k_ref, v_ref)
|
||||
)
|
||||
dq_test, dk_test, dv_test = torch.autograd.grad(
|
||||
(out_test * dout).sum(), (q_test, k_test, v_test)
|
||||
)
|
||||
|
||||
_assert_close(dq_test, dq_ref, dtype=dtype, is_grad=True)
|
||||
_assert_close(dk_test, dk_ref, dtype=dtype, is_grad=True)
|
||||
_assert_close(dv_test, dv_ref, dtype=dtype, is_grad=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("causal", [False, True])
|
||||
def test_flash_attn_varlen_func_parity_forward_backward(
|
||||
flash_attn_impls, dtype: torch.dtype, causal: bool
|
||||
):
|
||||
if dtype == torch.bfloat16 and not torch.cuda.is_bf16_supported():
|
||||
pytest.skip("bfloat16 is not supported on this GPU")
|
||||
|
||||
(
|
||||
_,
|
||||
custom_flash_attn_varlen_func,
|
||||
_,
|
||||
ref_flash_attn_varlen_func,
|
||||
) = flash_attn_impls
|
||||
|
||||
torch.manual_seed(1)
|
||||
device = torch.device("cuda")
|
||||
nheads, headdim = 4, 64
|
||||
seqlens = [40, 24, 16]
|
||||
cu_vals = [0]
|
||||
for seqlen in seqlens:
|
||||
cu_vals.append(cu_vals[-1] + seqlen)
|
||||
cu = torch.tensor(cu_vals, device=device, dtype=torch.int32)
|
||||
total = int(cu[-1].item())
|
||||
max_seqlen = max(seqlens)
|
||||
|
||||
q_ref = torch.randn(
|
||||
total, nheads, headdim, device=device, dtype=dtype, requires_grad=True
|
||||
)
|
||||
k_ref = torch.randn_like(q_ref, requires_grad=True)
|
||||
v_ref = torch.randn_like(q_ref, requires_grad=True)
|
||||
|
||||
q_test = q_ref.detach().clone().requires_grad_(True)
|
||||
k_test = k_ref.detach().clone().requires_grad_(True)
|
||||
v_test = v_ref.detach().clone().requires_grad_(True)
|
||||
|
||||
out_ref = _extract_out(ref_flash_attn_varlen_func(
|
||||
q_ref,
|
||||
k_ref,
|
||||
v_ref,
|
||||
cu_seqlens_q=cu,
|
||||
cu_seqlens_k=cu,
|
||||
max_seqlen_q=max_seqlen,
|
||||
max_seqlen_k=max_seqlen,
|
||||
softmax_scale=None,
|
||||
causal=causal,
|
||||
deterministic=False,
|
||||
))
|
||||
out_test = _extract_out(custom_flash_attn_varlen_func(
|
||||
q_test,
|
||||
k_test,
|
||||
v_test,
|
||||
cu,
|
||||
cu,
|
||||
max_seqlen,
|
||||
max_seqlen,
|
||||
dropout_p=0.0,
|
||||
softmax_scale=None,
|
||||
causal=causal,
|
||||
deterministic=False,
|
||||
))
|
||||
|
||||
_assert_close(out_test, out_ref, dtype=dtype)
|
||||
|
||||
dout = torch.randn_like(out_ref)
|
||||
dq_ref, dk_ref, dv_ref = torch.autograd.grad(
|
||||
(out_ref * dout).sum(), (q_ref, k_ref, v_ref)
|
||||
)
|
||||
dq_test, dk_test, dv_test = torch.autograd.grad(
|
||||
(out_test * dout).sum(), (q_test, k_test, v_test)
|
||||
)
|
||||
|
||||
_assert_close(dq_test, dq_ref, dtype=dtype, is_grad=True)
|
||||
_assert_close(dk_test, dk_ref, dtype=dtype, is_grad=True)
|
||||
_assert_close(dv_test, dv_ref, dtype=dtype, is_grad=True)
|
||||
@@ -0,0 +1,3 @@
|
||||
[style]
|
||||
based_on_style = pep8
|
||||
column_limit = 120
|
||||
@@ -0,0 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from fastvideo.train.trainer import Trainer
|
||||
|
||||
__all__ = [
|
||||
"Trainer",
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from fastvideo.train.callbacks.callback import (
|
||||
Callback,
|
||||
CallbackDict,
|
||||
)
|
||||
from fastvideo.train.callbacks.ema import (
|
||||
EMACallback, )
|
||||
from fastvideo.train.callbacks.grad_clip import (
|
||||
GradNormClipCallback, )
|
||||
from fastvideo.train.callbacks.validation import (
|
||||
ValidationCallback, )
|
||||
|
||||
__all__ = [
|
||||
"Callback",
|
||||
"CallbackDict",
|
||||
"EMACallback",
|
||||
"GradNormClipCallback",
|
||||
"ValidationCallback",
|
||||
]
|
||||
@@ -0,0 +1,175 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Callback base class and CallbackDict manager.
|
||||
|
||||
Adapted from FastGen's callback pattern to FastVideo's types.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.train.utils.instantiate import instantiate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.methods.base import TrainingMethod
|
||||
from fastvideo.train.utils.training_config import (
|
||||
TrainingConfig, )
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# Well-known callback names that don't need ``_target_`` in YAML.
|
||||
_BUILTIN_CALLBACKS: dict[str, str] = {
|
||||
"grad_clip": "fastvideo.train.callbacks.grad_clip.GradNormClipCallback",
|
||||
"validation": "fastvideo.train.callbacks.validation.ValidationCallback",
|
||||
"ema": "fastvideo.train.callbacks.ema.EMACallback",
|
||||
}
|
||||
|
||||
|
||||
class Callback:
|
||||
"""Base callback with no-op hooks.
|
||||
|
||||
Subclasses override whichever hooks they need. The
|
||||
``training_config`` and ``method`` attributes are set by
|
||||
``CallbackDict`` after instantiation.
|
||||
"""
|
||||
|
||||
training_config: TrainingConfig
|
||||
method: TrainingMethod
|
||||
_callback_dict: CallbackDict | None
|
||||
|
||||
def on_train_start(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def on_training_step_end(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
loss_dict: dict[str, Any],
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def on_before_optimizer_step(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def on_validation_begin(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def on_validation_end(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def on_train_end(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def state_dict(self) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
def load_state_dict(
|
||||
self,
|
||||
state_dict: dict[str, Any],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class CallbackDict:
|
||||
"""Manages a collection of named callbacks.
|
||||
|
||||
Instantiates each callback from its ``_target_`` config and
|
||||
dispatches hook calls to all registered callbacks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
callback_configs: dict[str, dict[str, Any]],
|
||||
training_config: TrainingConfig,
|
||||
) -> None:
|
||||
self._callbacks: dict[str, Callback] = {}
|
||||
if not callback_configs:
|
||||
return
|
||||
for name, cb_cfg in callback_configs.items():
|
||||
cb_cfg = dict(cb_cfg)
|
||||
if "_target_" not in cb_cfg:
|
||||
if name in _BUILTIN_CALLBACKS:
|
||||
cb_cfg["_target_"] = (_BUILTIN_CALLBACKS[name])
|
||||
else:
|
||||
logger.warning(
|
||||
"Callback %r is missing "
|
||||
"'_target_', skipping: %s",
|
||||
name,
|
||||
cb_cfg,
|
||||
)
|
||||
continue
|
||||
logger.info(
|
||||
"Instantiating callback %r: %s",
|
||||
name,
|
||||
cb_cfg,
|
||||
)
|
||||
cb = instantiate(cb_cfg)
|
||||
if not isinstance(cb, Callback):
|
||||
raise TypeError(f"Callback {name!r} resolved to "
|
||||
f"{type(cb).__name__}, expected a "
|
||||
f"Callback subclass.")
|
||||
cb.training_config = training_config
|
||||
cb._callback_dict = self
|
||||
self._callbacks[name] = cb
|
||||
|
||||
def __getattr__(
|
||||
self,
|
||||
method_name: str,
|
||||
) -> Callable[..., Any]:
|
||||
if method_name.startswith("_"):
|
||||
raise AttributeError(method_name)
|
||||
|
||||
if method_name == "state_dict":
|
||||
|
||||
def _state_dict() -> dict[str, Any]:
|
||||
return {n: cb.state_dict() for n, cb in self._callbacks.items()}
|
||||
|
||||
return _state_dict
|
||||
|
||||
if method_name == "load_state_dict":
|
||||
|
||||
def _load_state_dict(state_dict: dict[str, Any], ) -> None:
|
||||
for n, cb in self._callbacks.items():
|
||||
if n in state_dict:
|
||||
cb.load_state_dict(state_dict[n])
|
||||
else:
|
||||
logger.warning(
|
||||
"Callback %r not found in "
|
||||
"checkpoint.",
|
||||
n,
|
||||
)
|
||||
|
||||
return _load_state_dict
|
||||
|
||||
def _dispatch(*args: Any, **kwargs: Any) -> None:
|
||||
for cb in self._callbacks.values():
|
||||
fn = getattr(cb, method_name, None)
|
||||
if fn is None:
|
||||
continue
|
||||
if not callable(fn):
|
||||
continue
|
||||
fn(*args, **kwargs)
|
||||
|
||||
return _dispatch
|
||||
@@ -0,0 +1,154 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""EMA (Exponential Moving Average) callback.
|
||||
|
||||
Owns the full EMA lifecycle: creation, per-step updates, weight
|
||||
swapping for validation, and checkpoint state. All EMA config
|
||||
lives under ``callbacks.ema`` in the YAML file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from collections.abc import Generator
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.train.callbacks.callback import Callback
|
||||
from fastvideo.training.training_utils import EMA_FSDP
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.methods.base import TrainingMethod
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class EMACallback(Callback):
|
||||
"""Manage EMA shadow weights for the student transformer.
|
||||
|
||||
All configuration lives in the YAML ``callbacks.ema`` section:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
callbacks:
|
||||
ema:
|
||||
decay: 0.9999
|
||||
start_iter: 0
|
||||
|
||||
The callback creates an ``EMA_FSDP`` instance at train start,
|
||||
updates it after each optimizer step, and exposes an
|
||||
``ema_context()`` context manager for temporarily swapping
|
||||
EMA weights into the live model (used by validation).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
decay: float = 0.9999,
|
||||
start_iter: int = 0,
|
||||
) -> None:
|
||||
self._decay = float(decay)
|
||||
self._start_iter = int(start_iter)
|
||||
self._ema_started = False
|
||||
self.student_ema: EMA_FSDP | None = None
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Hooks
|
||||
# ----------------------------------------------------------
|
||||
|
||||
def on_train_start(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
student = getattr(method, "student", None)
|
||||
if student is None or student.transformer is None:
|
||||
raise ValueError("No student transformer found on method, cannot initialize EMA")
|
||||
|
||||
logger.info(
|
||||
"Initializing EMA (local_shard) with "
|
||||
"decay=%s from student transformer",
|
||||
self._decay,
|
||||
)
|
||||
self.student_ema = EMA_FSDP(
|
||||
student.transformer,
|
||||
decay=self._decay,
|
||||
mode="local_shard",
|
||||
)
|
||||
logger.info(
|
||||
"EMA callback enabled (decay=%s, "
|
||||
"start_iter=%d).",
|
||||
self._decay,
|
||||
self._start_iter,
|
||||
)
|
||||
|
||||
def on_training_step_end(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
loss_dict: dict[str, Any],
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
if self.student_ema is None:
|
||||
return
|
||||
|
||||
if iteration < self._start_iter:
|
||||
return
|
||||
if not self._ema_started:
|
||||
logger.info(
|
||||
"Starting EMA updates at iteration %d "
|
||||
"(re-initializing shadow from current "
|
||||
"model).",
|
||||
iteration,
|
||||
)
|
||||
self.student_ema._init_shadow(method.student.transformer, )
|
||||
self._ema_started = True
|
||||
|
||||
self.student_ema.update(method.student.transformer, )
|
||||
|
||||
tracker = getattr(method, "tracker", None)
|
||||
if tracker is not None:
|
||||
tracker.log(
|
||||
{"ema/decay": self.student_ema.decay},
|
||||
iteration,
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# EMA context manager
|
||||
# ----------------------------------------------------------
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ema_context(
|
||||
self,
|
||||
transformer: torch.nn.Module,
|
||||
) -> Generator[torch.nn.Module, None, None]:
|
||||
"""Temporarily swap EMA weights into *transformer*.
|
||||
|
||||
If EMA is not active, yields the transformer unchanged.
|
||||
"""
|
||||
if (self.student_ema is not None and self._ema_started):
|
||||
with self.student_ema.apply_to_model(transformer, ):
|
||||
yield transformer
|
||||
else:
|
||||
yield transformer
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Checkpoint state
|
||||
# ----------------------------------------------------------
|
||||
|
||||
def state_dict(self) -> dict[str, Any]:
|
||||
if self.student_ema is None:
|
||||
return {}
|
||||
return {
|
||||
"student_ema": self.student_ema.state_dict(),
|
||||
"ema_started": self._ema_started,
|
||||
}
|
||||
|
||||
def load_state_dict(
|
||||
self,
|
||||
state_dict: dict[str, Any],
|
||||
) -> None:
|
||||
ema_state = state_dict.get("student_ema")
|
||||
if (ema_state is not None and self.student_ema is not None):
|
||||
self.student_ema.load_state_dict(ema_state)
|
||||
self._ema_started = bool(state_dict.get("ema_started", False), )
|
||||
@@ -0,0 +1,61 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Gradient norm clipping callback.
|
||||
|
||||
Clips gradients on modules returned by
|
||||
``method.get_grad_clip_targets()`` before the optimizer step.
|
||||
Optionally logs per-module grad norms to the tracker.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.train.callbacks.callback import Callback
|
||||
from fastvideo.train.utils.optimizer import (
|
||||
clip_grad_norm_if_needed, )
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.methods.base import TrainingMethod
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class GradNormClipCallback(Callback):
|
||||
"""Clip gradient norms before the optimizer step.
|
||||
|
||||
``max_grad_norm`` must be set explicitly in the callback
|
||||
config (``callbacks.grad_clip.max_grad_norm``).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
max_grad_norm: float = 1.0,
|
||||
log_grad_norms: bool = True,
|
||||
) -> None:
|
||||
self._max_grad_norm = float(max_grad_norm)
|
||||
self._log_grad_norms = bool(log_grad_norms)
|
||||
|
||||
def on_before_optimizer_step(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
max_norm = self._max_grad_norm
|
||||
if max_norm <= 0.0:
|
||||
return
|
||||
|
||||
targets = method.get_grad_clip_targets(iteration)
|
||||
tracker = getattr(method, "tracker", None)
|
||||
|
||||
for name, module in targets.items():
|
||||
grad_norm = clip_grad_norm_if_needed(
|
||||
module,
|
||||
max_norm,
|
||||
)
|
||||
if (self._log_grad_norms and tracker is not None and grad_norm > 0.0):
|
||||
tracker.log(
|
||||
{f"grad_norm/{name}": grad_norm},
|
||||
iteration,
|
||||
)
|
||||
@@ -0,0 +1,464 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Validation callback.
|
||||
|
||||
All configuration is read from the YAML ``callbacks.validation``
|
||||
section. The pipeline class is resolved from
|
||||
``pipeline_target``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import imageio
|
||||
import numpy as np
|
||||
import torch
|
||||
import torchvision
|
||||
from einops import rearrange
|
||||
from torch.utils.data import DataLoader
|
||||
|
||||
from fastvideo.configs.sample import SamplingParam
|
||||
from fastvideo.dataset.validation_dataset import (
|
||||
ValidationDataset, )
|
||||
from fastvideo.distributed import (
|
||||
get_sp_group,
|
||||
get_world_group,
|
||||
)
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines import ForwardBatch
|
||||
from fastvideo.train.callbacks.callback import Callback
|
||||
from fastvideo.train.utils.instantiate import resolve_target
|
||||
from fastvideo.train.utils.moduleloader import (
|
||||
make_inference_args, )
|
||||
from fastvideo.training.trackers import DummyTracker
|
||||
from fastvideo.utils import shallow_asdict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.methods.base import TrainingMethod
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _ValidationStepResult:
|
||||
videos: list[list[np.ndarray]]
|
||||
captions: list[str]
|
||||
|
||||
|
||||
class ValidationCallback(Callback):
|
||||
"""Generic validation callback driven entirely by YAML
|
||||
config.
|
||||
|
||||
Works with any pipeline that follows the
|
||||
``PipelineCls.from_pretrained(...)`` + ``pipeline.forward()``
|
||||
contract.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
pipeline_target: str,
|
||||
dataset_file: str,
|
||||
every_steps: int = 100,
|
||||
sampling_steps: list[int] | None = None,
|
||||
guidance_scale: float | None = None,
|
||||
num_frames: int | None = None,
|
||||
output_dir: str | None = None,
|
||||
sampling_timesteps: list[int] | None = None,
|
||||
**pipeline_kwargs: Any,
|
||||
) -> None:
|
||||
self.pipeline_target = str(pipeline_target)
|
||||
self.dataset_file = str(dataset_file)
|
||||
self.every_steps = int(every_steps)
|
||||
self.sampling_steps = ([int(s) for s in sampling_steps] if sampling_steps else [40])
|
||||
self.guidance_scale = (float(guidance_scale) if guidance_scale is not None else None)
|
||||
self.num_frames = (int(num_frames) if num_frames is not None else None)
|
||||
self.output_dir = (str(output_dir) if output_dir is not None else None)
|
||||
self.sampling_timesteps = ([int(s) for s in sampling_timesteps] if sampling_timesteps is not None else None)
|
||||
self.pipeline_kwargs = dict(pipeline_kwargs)
|
||||
|
||||
# Set after on_train_start.
|
||||
self._pipeline: Any | None = None
|
||||
self._pipeline_key: tuple[Any, ...] | None = None
|
||||
self._sampling_param: SamplingParam | None = None
|
||||
self.tracker: Any = DummyTracker()
|
||||
self.validation_random_generator: (torch.Generator | None) = None
|
||||
self.seed: int = 0
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Callback hooks
|
||||
# ----------------------------------------------------------
|
||||
|
||||
def on_train_start(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
self.method = method
|
||||
tc = self.training_config
|
||||
|
||||
self.world_group = get_world_group()
|
||||
self.sp_group = get_sp_group()
|
||||
self.global_rank = self.world_group.rank
|
||||
self.rank_in_sp_group = (self.sp_group.rank_in_group)
|
||||
self.sp_world_size = self.sp_group.world_size
|
||||
|
||||
seed = tc.data.seed
|
||||
if seed is None:
|
||||
raise ValueError("training.data.seed must be set "
|
||||
"for validation")
|
||||
self.seed = int(seed)
|
||||
self.validation_random_generator = (torch.Generator(device="cpu").manual_seed(self.seed))
|
||||
|
||||
tracker = getattr(method, "tracker", None)
|
||||
if tracker is not None:
|
||||
self.tracker = tracker
|
||||
|
||||
def on_validation_begin(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
iteration: int = 0,
|
||||
) -> None:
|
||||
if self.every_steps <= 0:
|
||||
return
|
||||
if iteration % self.every_steps != 0:
|
||||
return
|
||||
|
||||
self._run_validation(method, iteration)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Core validation logic
|
||||
# ----------------------------------------------------------
|
||||
|
||||
def _run_validation(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
step: int,
|
||||
) -> None:
|
||||
|
||||
transformer = method.student.transformer
|
||||
# Look for an EMA callback to temporarily swap
|
||||
# EMA weights during validation.
|
||||
ema_cb = self._find_ema_callback()
|
||||
if ema_cb is not None:
|
||||
ctx = ema_cb.ema_context(transformer)
|
||||
else:
|
||||
ctx = contextlib.nullcontext(transformer)
|
||||
with ctx as t:
|
||||
self._run_validation_inner(
|
||||
method,
|
||||
step,
|
||||
t,
|
||||
)
|
||||
|
||||
def _find_ema_callback(self) -> Any | None:
|
||||
"""Find the EMA callback in the callback dict."""
|
||||
from fastvideo.train.callbacks.ema import (
|
||||
EMACallback, )
|
||||
|
||||
cb_dict = getattr(self, "_callback_dict", None)
|
||||
if cb_dict is not None:
|
||||
for cb in cb_dict._callbacks.values():
|
||||
if isinstance(cb, EMACallback):
|
||||
return cb
|
||||
return None
|
||||
|
||||
def _run_validation_inner(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
step: int,
|
||||
transformer: torch.nn.Module,
|
||||
) -> None:
|
||||
tc = self.training_config
|
||||
was_training = bool(getattr(transformer, "training", False))
|
||||
|
||||
output_dir = (self.output_dir or tc.checkpoint.output_dir)
|
||||
|
||||
try:
|
||||
transformer.eval()
|
||||
num_sp_groups = (self.world_group.world_size // self.sp_group.world_size)
|
||||
|
||||
for num_inference_steps in self.sampling_steps:
|
||||
result = self._run_validation_for_steps(
|
||||
num_inference_steps,
|
||||
transformer=transformer,
|
||||
)
|
||||
|
||||
if self.rank_in_sp_group != 0:
|
||||
continue
|
||||
|
||||
if self.global_rank == 0:
|
||||
all_videos = list(result.videos)
|
||||
all_captions = list(result.captions)
|
||||
for sp_idx in range(1, num_sp_groups):
|
||||
src = (sp_idx * self.sp_world_size)
|
||||
recv_v = (self.world_group.recv_object(src=src))
|
||||
recv_c = (self.world_group.recv_object(src=src))
|
||||
all_videos.extend(recv_v)
|
||||
all_captions.extend(recv_c)
|
||||
|
||||
os.makedirs(
|
||||
output_dir,
|
||||
exist_ok=True,
|
||||
)
|
||||
video_filenames: list[str] = []
|
||||
sp = self._get_sampling_param()
|
||||
for i, video in enumerate(all_videos):
|
||||
fname = os.path.join(
|
||||
output_dir,
|
||||
f"validation_step_{step}"
|
||||
f"_inference_steps_"
|
||||
f"{num_inference_steps}"
|
||||
f"_video_{i}.mp4",
|
||||
)
|
||||
imageio.mimsave(
|
||||
fname,
|
||||
video,
|
||||
fps=sp.fps,
|
||||
)
|
||||
video_filenames.append(fname)
|
||||
|
||||
video_logs = []
|
||||
for fname, cap in zip(
|
||||
video_filenames,
|
||||
all_captions,
|
||||
strict=True,
|
||||
):
|
||||
art = self.tracker.video(
|
||||
fname,
|
||||
caption=cap,
|
||||
)
|
||||
if art is not None:
|
||||
video_logs.append(art)
|
||||
if video_logs:
|
||||
logs = {
|
||||
f"validation_videos_"
|
||||
f"{num_inference_steps}"
|
||||
f"_steps": video_logs
|
||||
}
|
||||
self.tracker.log_artifacts(
|
||||
logs,
|
||||
step,
|
||||
)
|
||||
else:
|
||||
self.world_group.send_object(
|
||||
result.videos,
|
||||
dst=0,
|
||||
)
|
||||
self.world_group.send_object(
|
||||
result.captions,
|
||||
dst=0,
|
||||
)
|
||||
finally:
|
||||
if was_training:
|
||||
transformer.train()
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Pipeline management
|
||||
# ----------------------------------------------------------
|
||||
|
||||
def _get_sampling_param(self) -> SamplingParam:
|
||||
if self._sampling_param is None:
|
||||
self._sampling_param = (SamplingParam.from_pretrained(self.training_config.model_path))
|
||||
return self._sampling_param
|
||||
|
||||
def _get_pipeline(
|
||||
self,
|
||||
*,
|
||||
transformer: torch.nn.Module,
|
||||
) -> Any:
|
||||
key = (id(transformer), )
|
||||
if (self._pipeline is not None and self._pipeline_key == key):
|
||||
return self._pipeline
|
||||
|
||||
tc = self.training_config
|
||||
PipelineCls = resolve_target(self.pipeline_target)
|
||||
flow_shift = getattr(
|
||||
tc.pipeline_config,
|
||||
"flow_shift",
|
||||
None,
|
||||
)
|
||||
|
||||
kwargs: dict[str, Any] = {
|
||||
"inference_mode": True,
|
||||
"loaded_modules": {
|
||||
"transformer": transformer,
|
||||
},
|
||||
"tp_size": tc.distributed.tp_size,
|
||||
"sp_size": tc.distributed.sp_size,
|
||||
"num_gpus": tc.distributed.num_gpus,
|
||||
"pin_cpu_memory": (tc.distributed.pin_cpu_memory),
|
||||
"dit_cpu_offload": True,
|
||||
}
|
||||
if flow_shift is not None:
|
||||
kwargs["flow_shift"] = float(flow_shift)
|
||||
|
||||
self._pipeline = PipelineCls.from_pretrained(
|
||||
tc.model_path,
|
||||
**kwargs,
|
||||
)
|
||||
self._pipeline_key = key
|
||||
return self._pipeline
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Batch preparation
|
||||
# ----------------------------------------------------------
|
||||
|
||||
def _prepare_validation_batch(
|
||||
self,
|
||||
sampling_param: SamplingParam,
|
||||
validation_batch: dict[str, Any],
|
||||
num_inference_steps: int,
|
||||
) -> ForwardBatch:
|
||||
tc = self.training_config
|
||||
|
||||
sampling_param.prompt = validation_batch["prompt"]
|
||||
sampling_param.height = tc.data.num_height
|
||||
sampling_param.width = tc.data.num_width
|
||||
sampling_param.num_inference_steps = int(num_inference_steps)
|
||||
sampling_param.data_type = "video"
|
||||
if self.guidance_scale is not None:
|
||||
sampling_param.guidance_scale = float(self.guidance_scale)
|
||||
sampling_param.seed = self.seed
|
||||
|
||||
# image_path for I2V pipelines.
|
||||
img_path = (validation_batch.get("image_path") or validation_batch.get("video_path"))
|
||||
if img_path is not None and (img_path.startswith("http") or os.path.isfile(img_path)):
|
||||
sampling_param.image_path = img_path
|
||||
|
||||
temporal_compression_factor = int(
|
||||
tc.pipeline_config.vae_config.arch_config.temporal_compression_ratio # type: ignore[union-attr]
|
||||
)
|
||||
default_num_frames = ((tc.data.num_latent_t - 1) * temporal_compression_factor + 1)
|
||||
if self.num_frames is not None:
|
||||
sampling_param.num_frames = int(self.num_frames)
|
||||
else:
|
||||
sampling_param.num_frames = int(default_num_frames)
|
||||
|
||||
latents_size = [
|
||||
(sampling_param.num_frames - 1) // 4 + 1,
|
||||
sampling_param.height // 8,
|
||||
sampling_param.width // 8,
|
||||
]
|
||||
n_tokens = (latents_size[0] * latents_size[1] * latents_size[2])
|
||||
|
||||
sampling_timesteps_tensor = (torch.tensor(
|
||||
[int(s) for s in self.sampling_timesteps],
|
||||
dtype=torch.long,
|
||||
) if self.sampling_timesteps is not None else None)
|
||||
|
||||
inference_args = make_inference_args(
|
||||
tc,
|
||||
model_path=tc.model_path,
|
||||
)
|
||||
|
||||
batch = ForwardBatch(
|
||||
**shallow_asdict(sampling_param),
|
||||
latents=None,
|
||||
generator=self.validation_random_generator,
|
||||
n_tokens=n_tokens,
|
||||
eta=0.0,
|
||||
VSA_sparsity=tc.vsa_sparsity,
|
||||
timesteps=sampling_timesteps_tensor,
|
||||
)
|
||||
batch._inference_args = inference_args # type: ignore[attr-defined]
|
||||
|
||||
# Conditionally set I2V fields.
|
||||
if ("image" in validation_batch and validation_batch["image"] is not None):
|
||||
batch.pil_image = validation_batch["image"]
|
||||
|
||||
return batch
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Validation loop
|
||||
# ----------------------------------------------------------
|
||||
|
||||
def _run_validation_for_steps(
|
||||
self,
|
||||
num_inference_steps: int,
|
||||
*,
|
||||
transformer: torch.nn.Module,
|
||||
) -> _ValidationStepResult:
|
||||
tc = self.training_config
|
||||
pipeline = self._get_pipeline(transformer=transformer, )
|
||||
sampling_param = self._get_sampling_param()
|
||||
|
||||
dataset = ValidationDataset(self.dataset_file)
|
||||
dataloader = DataLoader(
|
||||
dataset,
|
||||
batch_size=None,
|
||||
num_workers=0,
|
||||
)
|
||||
|
||||
inference_args = make_inference_args(
|
||||
tc,
|
||||
model_path=tc.model_path,
|
||||
)
|
||||
|
||||
# Propagate sampling_timesteps to pipeline_config so
|
||||
# causal/DMD denoising stages can read them.
|
||||
if (self.sampling_timesteps is not None and inference_args.pipeline_config.dmd_denoising_steps is None):
|
||||
inference_args.pipeline_config.dmd_denoising_steps = ([int(s) for s in self.sampling_timesteps])
|
||||
|
||||
videos: list[list[np.ndarray]] = []
|
||||
captions: list[str] = []
|
||||
|
||||
for validation_batch in dataloader:
|
||||
batch = self._prepare_validation_batch(
|
||||
sampling_param,
|
||||
validation_batch,
|
||||
num_inference_steps,
|
||||
)
|
||||
|
||||
assert (batch.prompt is not None and isinstance(batch.prompt, str))
|
||||
captions.append(batch.prompt)
|
||||
|
||||
with torch.no_grad():
|
||||
output_batch = pipeline.forward(
|
||||
batch,
|
||||
inference_args,
|
||||
)
|
||||
|
||||
samples = output_batch.output.cpu()
|
||||
if self.rank_in_sp_group != 0:
|
||||
continue
|
||||
|
||||
video = rearrange(
|
||||
samples,
|
||||
"b c t h w -> t b c h w",
|
||||
)
|
||||
frames: list[np.ndarray] = []
|
||||
for x in video:
|
||||
x = torchvision.utils.make_grid(
|
||||
x,
|
||||
nrow=6,
|
||||
)
|
||||
x = (x.transpose(0, 1).transpose(1, 2).squeeze(-1))
|
||||
frames.append((x * 255).numpy().astype(np.uint8))
|
||||
videos.append(frames)
|
||||
|
||||
return _ValidationStepResult(
|
||||
videos=videos,
|
||||
captions=captions,
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# State management
|
||||
# ----------------------------------------------------------
|
||||
|
||||
def state_dict(self) -> dict[str, Any]:
|
||||
state: dict[str, Any] = {}
|
||||
if self.validation_random_generator is not None:
|
||||
state["validation_rng"] = (self.validation_random_generator.get_state())
|
||||
return state
|
||||
|
||||
def load_state_dict(
|
||||
self,
|
||||
state_dict: dict[str, Any],
|
||||
) -> None:
|
||||
rng_state = state_dict.get("validation_rng")
|
||||
if (rng_state is not None and self.validation_random_generator is not None):
|
||||
self.validation_random_generator.set_state(rng_state)
|
||||
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
@@ -0,0 +1,397 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Convert a DCP training checkpoint to a diffusers-style model directory.
|
||||
|
||||
Works on a single GPU regardless of how many GPUs were used for training
|
||||
(DCP handles resharding automatically).
|
||||
|
||||
Usage (no torchrun needed)::
|
||||
|
||||
python -m fastvideo.train.entrypoint.dcp_to_diffusers \
|
||||
--checkpoint /path/to/checkpoint-1000 \
|
||||
--output-dir /path/to/diffusers_output
|
||||
|
||||
Or with torchrun (also fine)::
|
||||
|
||||
torchrun --nproc_per_node=1 \
|
||||
-m fastvideo.train.entrypoint.dcp_to_diffusers \
|
||||
--checkpoint ... --output-dir ...
|
||||
|
||||
The checkpoint must contain ``metadata.json`` (written by
|
||||
``CheckpointManager``). If the checkpoint predates metadata
|
||||
support, pass ``--config`` explicitly to provide the training
|
||||
YAML.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _ensure_distributed() -> None:
|
||||
"""Set up a single-process distributed env if needed.
|
||||
|
||||
When running under ``torchrun`` the env vars are already set.
|
||||
For plain ``python`` we fill in the minimum required vars so
|
||||
that ``init_process_group`` succeeds with world_size=1.
|
||||
"""
|
||||
for key, default in [
|
||||
("RANK", "0"),
|
||||
("LOCAL_RANK", "0"),
|
||||
("WORLD_SIZE", "1"),
|
||||
("MASTER_ADDR", "127.0.0.1"),
|
||||
("MASTER_PORT", "29500"),
|
||||
]:
|
||||
os.environ.setdefault(key, default)
|
||||
|
||||
|
||||
def _save_role_pretrained(
|
||||
*,
|
||||
role: str,
|
||||
base_model_path: str,
|
||||
output_dir: str,
|
||||
module_names: list[str] | None = None,
|
||||
overwrite: bool = False,
|
||||
model: Any,
|
||||
) -> str:
|
||||
"""Export a role's modules into a diffusers-style model dir.
|
||||
|
||||
Produces a ``model_path`` loadable by
|
||||
``PipelineComponentLoader`` (``model_index.json``,
|
||||
``transformer/``, ``vae/``, etc. copied from
|
||||
``base_model_path``).
|
||||
"""
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from torch.distributed.checkpoint.state_dict import (
|
||||
StateDictOptions,
|
||||
get_model_state_dict,
|
||||
)
|
||||
|
||||
from fastvideo.utils import maybe_download_model
|
||||
|
||||
def _rank() -> int:
|
||||
if dist.is_available() and dist.is_initialized():
|
||||
return int(dist.get_rank())
|
||||
return 0
|
||||
|
||||
def _barrier() -> None:
|
||||
if dist.is_available() and dist.is_initialized():
|
||||
dist.barrier()
|
||||
|
||||
local_base = Path(maybe_download_model(str(base_model_path))).resolve()
|
||||
dst = Path(os.path.expanduser(str(output_dir))).resolve()
|
||||
|
||||
if _rank() == 0:
|
||||
if dst.exists():
|
||||
if overwrite:
|
||||
shutil.rmtree(dst, ignore_errors=True)
|
||||
else:
|
||||
raise FileExistsError(f"Refusing to overwrite existing "
|
||||
f"directory: {dst}. "
|
||||
"Pass --overwrite to replace it.")
|
||||
|
||||
def _copy_or_link(src: str, dest: str) -> None:
|
||||
try:
|
||||
os.link(src, dest)
|
||||
except OSError:
|
||||
shutil.copy2(src, dest)
|
||||
|
||||
logger.info(
|
||||
"Creating pretrained export dir at %s "
|
||||
"(base=%s)",
|
||||
dst,
|
||||
local_base,
|
||||
)
|
||||
shutil.copytree(
|
||||
local_base,
|
||||
dst,
|
||||
symlinks=True,
|
||||
copy_function=_copy_or_link,
|
||||
)
|
||||
|
||||
_barrier()
|
||||
|
||||
modules: dict[str, torch.nn.Module] = {}
|
||||
if model.transformer is not None:
|
||||
modules["transformer"] = model.transformer
|
||||
|
||||
if module_names is None:
|
||||
module_names = sorted(modules.keys())
|
||||
|
||||
for module_name in module_names:
|
||||
if module_name not in modules:
|
||||
raise KeyError(f"Role {role!r} does not have module "
|
||||
f"{module_name!r}. "
|
||||
f"Available: {sorted(modules.keys())}")
|
||||
|
||||
module_dir = dst / module_name
|
||||
if not module_dir.is_dir():
|
||||
raise FileNotFoundError(f"Export directory missing component "
|
||||
f"dir {module_name!r}: {module_dir}")
|
||||
|
||||
options = StateDictOptions(
|
||||
full_state_dict=True,
|
||||
cpu_offload=True,
|
||||
)
|
||||
state_dict = get_model_state_dict(
|
||||
modules[module_name],
|
||||
options=options,
|
||||
)
|
||||
|
||||
if _rank() == 0:
|
||||
for path in module_dir.glob("*.safetensors"):
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
tensor_state: dict[str, torch.Tensor] = {}
|
||||
for key, value in state_dict.items():
|
||||
if not isinstance(value, torch.Tensor):
|
||||
raise TypeError(f"Expected tensor in state_dict "
|
||||
f"for {module_name}.{key}, "
|
||||
f"got {type(value).__name__}")
|
||||
tensor_state[key] = value.detach().cpu()
|
||||
|
||||
from safetensors.torch import save_file
|
||||
|
||||
out_path = module_dir / "model.safetensors"
|
||||
logger.info(
|
||||
"Saving %s weights to %s (%s tensors)",
|
||||
module_name,
|
||||
out_path,
|
||||
len(tensor_state),
|
||||
)
|
||||
save_file(tensor_state, str(out_path))
|
||||
|
||||
_barrier()
|
||||
|
||||
return str(dst)
|
||||
|
||||
|
||||
def convert(
|
||||
*,
|
||||
checkpoint_dir: str,
|
||||
output_dir: str,
|
||||
config_path: str | None = None,
|
||||
role: str = "student",
|
||||
overwrite: bool = False,
|
||||
) -> str:
|
||||
"""Load a DCP checkpoint and export as a diffusers model.
|
||||
|
||||
Returns the path to the exported model directory.
|
||||
"""
|
||||
_ensure_distributed()
|
||||
|
||||
from fastvideo.distributed import (
|
||||
maybe_init_distributed_environment_and_model_parallel, )
|
||||
from fastvideo.train.utils.builder import build_from_config
|
||||
from fastvideo.train.utils.checkpoint import (
|
||||
CheckpointManager,
|
||||
_resolve_resume_checkpoint,
|
||||
)
|
||||
from fastvideo.train.utils.config import (
|
||||
RunConfig,
|
||||
load_run_config,
|
||||
)
|
||||
|
||||
import torch.distributed.checkpoint as dcp
|
||||
|
||||
# -- Resolve checkpoint directory --
|
||||
resolved = _resolve_resume_checkpoint(
|
||||
checkpoint_dir,
|
||||
output_dir=checkpoint_dir,
|
||||
)
|
||||
dcp_dir = resolved / "dcp"
|
||||
if not dcp_dir.is_dir():
|
||||
raise FileNotFoundError(f"Missing dcp/ under {resolved}")
|
||||
|
||||
# -- Obtain config --
|
||||
cfg: RunConfig
|
||||
if config_path is not None:
|
||||
cfg = load_run_config(config_path)
|
||||
else:
|
||||
metadata = CheckpointManager.load_metadata(resolved)
|
||||
raw_config = metadata.get("config")
|
||||
if raw_config is None:
|
||||
raise ValueError("Checkpoint metadata.json does not "
|
||||
"contain 'config'. Pass --config "
|
||||
"explicitly.")
|
||||
cfg = _run_config_from_raw(raw_config)
|
||||
|
||||
tc = cfg.training
|
||||
|
||||
# -- Init distributed (1 GPU is enough; DCP reshards) --
|
||||
maybe_init_distributed_environment_and_model_parallel(
|
||||
tp_size=1,
|
||||
sp_size=1,
|
||||
)
|
||||
|
||||
# Override distributed config so model loading uses 1 GPU.
|
||||
tc.distributed.tp_size = 1
|
||||
tc.distributed.sp_size = 1
|
||||
tc.distributed.num_gpus = 1
|
||||
tc.distributed.hsdp_replicate_dim = 1
|
||||
tc.distributed.hsdp_shard_dim = 1
|
||||
|
||||
# -- Build model (loads pretrained weights + FSDP) --
|
||||
_, method, _, _ = build_from_config(cfg)
|
||||
|
||||
# -- Load DCP weights into the model --
|
||||
states = method.checkpoint_state()
|
||||
logger.info(
|
||||
"Loading DCP checkpoint from %s",
|
||||
resolved,
|
||||
)
|
||||
dcp.load(states, checkpoint_id=str(dcp_dir))
|
||||
|
||||
# -- Export to diffusers format --
|
||||
model = method._role_models[role]
|
||||
base_model_path = str(tc.model_path)
|
||||
if not base_model_path:
|
||||
raise ValueError("Cannot determine base_model_path from "
|
||||
"config. Ensure models.student.init_from "
|
||||
"is set.")
|
||||
|
||||
logger.info(
|
||||
"Exporting role=%s to %s (base=%s)",
|
||||
role,
|
||||
output_dir,
|
||||
base_model_path,
|
||||
)
|
||||
result = _save_role_pretrained(
|
||||
role=role,
|
||||
base_model_path=base_model_path,
|
||||
output_dir=output_dir,
|
||||
overwrite=overwrite,
|
||||
model=model,
|
||||
)
|
||||
logger.info("Export complete: %s", result)
|
||||
return result
|
||||
|
||||
|
||||
def _run_config_from_raw(raw: dict[str, Any], ) -> Any:
|
||||
"""Reconstruct a RunConfig from a raw config dict.
|
||||
|
||||
This mirrors ``load_run_config`` but operates on an
|
||||
already-parsed dict (from metadata.json) instead of
|
||||
reading from a YAML file.
|
||||
"""
|
||||
from fastvideo.train.utils.config import (
|
||||
RunConfig,
|
||||
_build_training_config,
|
||||
_parse_pipeline_config,
|
||||
_require_mapping,
|
||||
_require_str,
|
||||
)
|
||||
|
||||
models_raw = _require_mapping(
|
||||
raw.get("models"),
|
||||
where="models",
|
||||
)
|
||||
models: dict[str, dict[str, Any]] = {}
|
||||
for role_key, model_cfg_raw in models_raw.items():
|
||||
role_str = _require_str(
|
||||
role_key,
|
||||
where="models.<role>",
|
||||
)
|
||||
model_cfg = _require_mapping(
|
||||
model_cfg_raw,
|
||||
where=f"models.{role_str}",
|
||||
)
|
||||
models[role_str] = dict(model_cfg)
|
||||
|
||||
method_raw = _require_mapping(
|
||||
raw.get("method"),
|
||||
where="method",
|
||||
)
|
||||
method = dict(method_raw)
|
||||
|
||||
callbacks_raw = raw.get("callbacks")
|
||||
callbacks: dict[str, dict[str, Any]] = (_require_mapping(
|
||||
callbacks_raw,
|
||||
where="callbacks",
|
||||
) if callbacks_raw is not None else {})
|
||||
|
||||
pipeline_config = _parse_pipeline_config(
|
||||
raw,
|
||||
models=models,
|
||||
)
|
||||
|
||||
training_raw = _require_mapping(
|
||||
raw.get("training"),
|
||||
where="training",
|
||||
)
|
||||
t = dict(training_raw)
|
||||
training = _build_training_config(
|
||||
t,
|
||||
models=models,
|
||||
pipeline_config=pipeline_config,
|
||||
)
|
||||
|
||||
return RunConfig(
|
||||
models=models,
|
||||
method=method,
|
||||
training=training,
|
||||
callbacks=callbacks,
|
||||
raw=raw,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=("Convert a DCP training checkpoint to a "
|
||||
"diffusers-style model directory. "
|
||||
"Only 1 GPU needed (DCP reshards "
|
||||
"automatically)."), )
|
||||
parser.add_argument(
|
||||
"--checkpoint",
|
||||
type=str,
|
||||
required=True,
|
||||
help=("Path to checkpoint-<step> dir, its dcp/ "
|
||||
"subdir, or an output_dir (auto-picks "
|
||||
"latest)."),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Destination for the diffusers model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default=None,
|
||||
help=("Training YAML config. If omitted, read "
|
||||
"from checkpoint metadata.json."),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--role",
|
||||
type=str,
|
||||
default="student",
|
||||
help="Role to export (default: student).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite output-dir if it exists.",
|
||||
)
|
||||
args = parser.parse_args(sys.argv[1:])
|
||||
|
||||
convert(
|
||||
checkpoint_dir=args.checkpoint,
|
||||
output_dir=args.output_dir,
|
||||
config_path=args.config,
|
||||
role=args.role,
|
||||
overwrite=args.overwrite,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,221 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Convert Self-Forcing ode_init.pt to HuggingFace diffusers format.
|
||||
|
||||
The official ode_init.pt from
|
||||
https://huggingface.co/gdhe17/Self-Forcing/resolve/main/checkpoints/ode_init.pt
|
||||
stores weights under ``{"generator": {<original_wan_keys>}}``.
|
||||
|
||||
This script converts those keys to diffusers
|
||||
``WanTransformer3DModel`` format, verifies them against a reference
|
||||
model, and saves a complete diffusers-compatible model directory
|
||||
(transformer + scheduler + vae + text_encoder + tokenizer).
|
||||
|
||||
Usage:
|
||||
python -m fastvideo.train.entrypoint.misc.wan_ode_init_conversion \
|
||||
--input /path/to/ode_init.pt \
|
||||
--output /path/to/WanOdeInit \
|
||||
--base-model Wan-AI/Wan2.1-T2V-1.3B-Diffusers
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import torch
|
||||
|
||||
# -- Key conversion -----------------------------------------------
|
||||
|
||||
|
||||
def _convert_key(key: str) -> str:
|
||||
"""Map a single original-Wan key to diffusers key."""
|
||||
k = key
|
||||
if k.startswith("model."):
|
||||
k = k[len("model."):]
|
||||
|
||||
# Top-level modules
|
||||
k = k.replace("head.modulation", "scale_shift_table")
|
||||
k = k.replace("head.head.", "proj_out.")
|
||||
k = k.replace(
|
||||
"time_embedding.0.",
|
||||
"condition_embedder.time_embedder.linear_1.",
|
||||
)
|
||||
k = k.replace(
|
||||
"time_embedding.2.",
|
||||
"condition_embedder.time_embedder.linear_2.",
|
||||
)
|
||||
k = k.replace(
|
||||
"time_projection.1.",
|
||||
"condition_embedder.time_proj.",
|
||||
)
|
||||
k = k.replace(
|
||||
"text_embedding.0.",
|
||||
"condition_embedder.text_embedder.linear_1.",
|
||||
)
|
||||
k = k.replace(
|
||||
"text_embedding.2.",
|
||||
"condition_embedder.text_embedder.linear_2.",
|
||||
)
|
||||
|
||||
# Block-level: order matters — rename attn sub-modules
|
||||
# before renaming q/k/v/o so prefixes don't collide.
|
||||
k = k.replace(".modulation", ".scale_shift_table")
|
||||
k = k.replace(".self_attn.", ".attn1.")
|
||||
k = k.replace(".cross_attn.", ".attn2.")
|
||||
k = k.replace(".norm3.", ".norm2.")
|
||||
|
||||
# Attention projections
|
||||
k = k.replace(".attn1.q.", ".attn1.to_q.")
|
||||
k = k.replace(".attn1.k.", ".attn1.to_k.")
|
||||
k = k.replace(".attn1.v.", ".attn1.to_v.")
|
||||
k = k.replace(".attn1.o.", ".attn1.to_out.0.")
|
||||
k = k.replace(".attn2.q.", ".attn2.to_q.")
|
||||
k = k.replace(".attn2.k.", ".attn2.to_k.")
|
||||
k = k.replace(".attn2.v.", ".attn2.to_v.")
|
||||
k = k.replace(".attn2.o.", ".attn2.to_out.0.")
|
||||
|
||||
# FFN
|
||||
k = k.replace(".ffn.0.", ".ffn.net.0.proj.")
|
||||
k = k.replace(".ffn.2.", ".ffn.net.2.")
|
||||
|
||||
return k
|
||||
|
||||
|
||||
def convert_state_dict(orig_sd: dict[str, torch.Tensor], ) -> dict[str, torch.Tensor]:
|
||||
"""Convert an entire original-Wan state dict."""
|
||||
return {_convert_key(k): v for k, v in orig_sd.items()}
|
||||
|
||||
|
||||
# -- Main ---------------------------------------------------------
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Convert ode_init.pt to diffusers format", )
|
||||
parser.add_argument(
|
||||
"--input",
|
||||
required=True,
|
||||
help="Path to ode_init.pt",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
required=True,
|
||||
help="Output directory for diffusers model",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-model",
|
||||
default="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
help="HF repo or local path for base model "
|
||||
"(provides config, VAE, tokenizer, etc.)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-verify",
|
||||
action="store_true",
|
||||
help="Skip strict load verification",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# 1. Load checkpoint
|
||||
print(f"Loading {args.input} ...")
|
||||
ckpt = torch.load(args.input, map_location="cpu", weights_only=False)
|
||||
if "generator" in ckpt:
|
||||
orig_sd = ckpt["generator"]
|
||||
elif isinstance(ckpt, dict) and any(k.startswith("model.") for k in ckpt):
|
||||
orig_sd = ckpt
|
||||
else:
|
||||
raise ValueError("Cannot find weights in checkpoint. "
|
||||
"Expected key 'generator' or keys starting "
|
||||
"with 'model.'.")
|
||||
print(f" Found {len(orig_sd)} weight tensors")
|
||||
|
||||
# 2. Convert keys
|
||||
print("Converting keys ...")
|
||||
new_sd = convert_state_dict(orig_sd)
|
||||
|
||||
# 3. Verify against reference model
|
||||
if not args.skip_verify:
|
||||
from diffusers import WanTransformer3DModel
|
||||
|
||||
print(f"Loading reference model from "
|
||||
f"{args.base_model} for verification ...")
|
||||
ref_model = WanTransformer3DModel.from_pretrained(
|
||||
args.base_model,
|
||||
subfolder="transformer",
|
||||
torch_dtype=torch.bfloat16,
|
||||
)
|
||||
ref_keys = set(ref_model.state_dict().keys())
|
||||
new_keys = set(new_sd.keys())
|
||||
missing = sorted(ref_keys - new_keys)
|
||||
extra = sorted(new_keys - ref_keys)
|
||||
if missing:
|
||||
print(f" WARNING: {len(missing)} missing keys:")
|
||||
for k in missing[:10]:
|
||||
print(f" {k}")
|
||||
if len(missing) > 10:
|
||||
print(f" ... and {len(missing) - 10} more")
|
||||
if extra:
|
||||
print(f" WARNING: {len(extra)} extra keys:")
|
||||
for k in extra[:10]:
|
||||
print(f" {k}")
|
||||
if len(extra) > 10:
|
||||
print(f" ... and {len(extra) - 10} more")
|
||||
if missing or extra:
|
||||
raise RuntimeError("Key mismatch — conversion mapping needs "
|
||||
"updating. Use --skip-verify to bypass.")
|
||||
|
||||
ref_model.load_state_dict(new_sd, strict=True)
|
||||
print(" Strict load OK — all keys match!")
|
||||
else:
|
||||
ref_model = None
|
||||
print(" Skipping verification")
|
||||
|
||||
# 4. Save transformer
|
||||
output_dir = os.path.abspath(args.output)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
transformer_dir = os.path.join(output_dir, "transformer")
|
||||
|
||||
if ref_model is not None:
|
||||
print(f"Saving transformer to {transformer_dir} ...")
|
||||
ref_model.save_pretrained(transformer_dir)
|
||||
else:
|
||||
from safetensors.torch import save_file
|
||||
|
||||
os.makedirs(transformer_dir, exist_ok=True)
|
||||
print(f"Saving transformer weights to "
|
||||
f"{transformer_dir} ...")
|
||||
save_file(new_sd, os.path.join(transformer_dir, "model.safetensors"))
|
||||
|
||||
# 5. Copy non-transformer files from base model
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
print(f"Downloading base model files from "
|
||||
f"{args.base_model} ...")
|
||||
base_path = snapshot_download(
|
||||
args.base_model,
|
||||
allow_patterns=[
|
||||
"model_index.json",
|
||||
"scheduler/*",
|
||||
"tokenizer/*",
|
||||
"vae/*",
|
||||
"text_encoder/*",
|
||||
],
|
||||
)
|
||||
for item in sorted(os.listdir(base_path)):
|
||||
if item == "transformer":
|
||||
continue
|
||||
src = os.path.join(base_path, item)
|
||||
dst = os.path.join(output_dir, item)
|
||||
if os.path.isfile(src):
|
||||
shutil.copy2(src, dst)
|
||||
print(f" Copied {item}")
|
||||
elif os.path.isdir(src):
|
||||
if os.path.exists(dst):
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
print(f" Copied {item}/")
|
||||
|
||||
print(f"\nDone! Diffusers model saved to {output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,146 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""YAML-only training entrypoint.
|
||||
|
||||
Usage::
|
||||
|
||||
torchrun --nproc_per_node=<N> -m fastvideo.train.entrypoint.train \
|
||||
--config path/to/run.yaml
|
||||
|
||||
Any unknown ``--dotted.key value`` arguments are applied as
|
||||
overrides to the YAML config before parsing. For example::
|
||||
|
||||
torchrun --nproc_per_node=8 -m fastvideo.train.entrypoint.train \
|
||||
--config path/to/run.yaml \
|
||||
--training.distributed.num_gpus 8 \
|
||||
--training.optimizer.learning_rate 1e-5
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def run_training_from_config(
|
||||
config_path: str,
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
overrides: list[str] | None = None,
|
||||
) -> None:
|
||||
"""YAML-only training entrypoint (schema v2)."""
|
||||
|
||||
from fastvideo.distributed import (
|
||||
maybe_init_distributed_environment_and_model_parallel, )
|
||||
from fastvideo.train import Trainer
|
||||
from fastvideo.train.utils.checkpoint import (
|
||||
CheckpointConfig,
|
||||
CheckpointManager,
|
||||
)
|
||||
from fastvideo.train.utils.builder import build_from_config
|
||||
from fastvideo.train.utils.config import load_run_config
|
||||
|
||||
# Enable deterministic mode for reproducibility.
|
||||
torch.backends.cudnn.benchmark = False
|
||||
torch.backends.cudnn.deterministic = True
|
||||
|
||||
cfg = load_run_config(config_path, overrides=overrides)
|
||||
tc = cfg.training
|
||||
|
||||
# Auto-set attention backend for VSA when sparsity is configured.
|
||||
if tc.vsa_sparsity > 0.0:
|
||||
os.environ.setdefault(
|
||||
"FASTVIDEO_ATTENTION_BACKEND",
|
||||
"VIDEO_SPARSE_ATTN",
|
||||
)
|
||||
|
||||
maybe_init_distributed_environment_and_model_parallel(
|
||||
tc.distributed.tp_size,
|
||||
tc.distributed.sp_size,
|
||||
)
|
||||
|
||||
_, method, dataloader, start_step = build_from_config(cfg)
|
||||
|
||||
if dry_run:
|
||||
logger.info("Dry-run: config parsed and "
|
||||
"build_from_config succeeded.")
|
||||
return
|
||||
|
||||
trainer = Trainer(
|
||||
tc,
|
||||
config=cfg.resolved_config(),
|
||||
callback_configs=cfg.callbacks,
|
||||
)
|
||||
|
||||
# Attach the exact YAML used for this run to the
|
||||
# tracker (e.g., W&B Files).
|
||||
trainer.tracker.log_file(
|
||||
os.path.abspath(os.path.expanduser(config_path)),
|
||||
name="run.yaml",
|
||||
)
|
||||
|
||||
ckpt_config = CheckpointConfig(
|
||||
save_steps=int(tc.checkpoint.training_state_checkpointing_steps or 0),
|
||||
keep_last=int(tc.checkpoint.checkpoints_total_limit or 0),
|
||||
)
|
||||
|
||||
checkpoint_manager = CheckpointManager(
|
||||
method=method,
|
||||
dataloader=dataloader,
|
||||
output_dir=tc.checkpoint.output_dir,
|
||||
config=ckpt_config,
|
||||
callbacks=trainer.callbacks,
|
||||
raw_config=cfg.raw,
|
||||
)
|
||||
|
||||
trainer.run(
|
||||
method,
|
||||
dataloader=dataloader,
|
||||
max_steps=tc.loop.max_train_steps,
|
||||
start_step=start_step,
|
||||
checkpoint_manager=checkpoint_manager,
|
||||
)
|
||||
|
||||
|
||||
def main(
|
||||
args: Any,
|
||||
overrides: list[str] | None = None,
|
||||
) -> None:
|
||||
config_path = str(args.config)
|
||||
dry_run = bool(args.dry_run)
|
||||
logger.info(
|
||||
"Starting training from config=%s",
|
||||
config_path,
|
||||
)
|
||||
run_training_from_config(
|
||||
config_path,
|
||||
dry_run=dry_run,
|
||||
overrides=overrides,
|
||||
)
|
||||
logger.info("Training completed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
parser = argparse.ArgumentParser(description="YAML-only training entrypoint.", )
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
required=True,
|
||||
help=("Path to training YAML config (schema v2)."),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help=("Parse config and build runtime, "
|
||||
"but do not start training."),
|
||||
)
|
||||
args, unknown = parser.parse_known_args(argv[1:])
|
||||
main(args, overrides=unknown if unknown else None)
|
||||
@@ -0,0 +1,27 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from fastvideo.train.methods.base import TrainingMethod
|
||||
|
||||
__all__ = [
|
||||
"TrainingMethod",
|
||||
"DMD2Method",
|
||||
"FineTuneMethod",
|
||||
"SelfForcingMethod",
|
||||
"DiffusionForcingSFTMethod",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "DMD2Method":
|
||||
from fastvideo.train.methods.distribution_matching.dmd2 import DMD2Method
|
||||
return DMD2Method
|
||||
if name == "FineTuneMethod":
|
||||
from fastvideo.train.methods.fine_tuning.finetune import FineTuneMethod
|
||||
return FineTuneMethod
|
||||
if name == "SelfForcingMethod":
|
||||
from fastvideo.train.methods.distribution_matching.self_forcing import SelfForcingMethod
|
||||
return SelfForcingMethod
|
||||
if name == "DiffusionForcingSFTMethod":
|
||||
from fastvideo.train.methods.fine_tuning.dfsft import DiffusionForcingSFTMethod
|
||||
return DiffusionForcingSFTMethod
|
||||
raise AttributeError(name)
|
||||
@@ -0,0 +1,244 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Literal, TypeAlias
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo import envs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.train.models.base import ModelBase
|
||||
from fastvideo.train.utils.checkpoint import _RoleModuleContainer
|
||||
from fastvideo.training.checkpointing_utils import (
|
||||
ModelWrapper,
|
||||
OptimizerWrapper,
|
||||
SchedulerWrapper,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
LogScalar: TypeAlias = float | int | torch.Tensor
|
||||
|
||||
|
||||
class TrainingMethod(torch.nn.Module, ABC):
|
||||
"""Base training method (algorithm layer).
|
||||
|
||||
Subclasses own their role models (student, teacher, critic, …) as
|
||||
plain attributes and manage optimizers directly — no ``RoleManager``
|
||||
or ``RoleHandle``.
|
||||
|
||||
The constructor receives *role_models* (a ``dict[str, ModelBase]``)
|
||||
and a *cfg* object. It calls ``init_preprocessors`` on the student
|
||||
and builds ``self.role_modules`` for FSDP wrapping.
|
||||
|
||||
A single shared CUDA RNG generator (``cuda_generator``) is
|
||||
created in :meth:`on_train_start`. All ``torch.randn`` /
|
||||
``torch.randint`` calls in methods **and** models must use this
|
||||
generator instead of relying on global RNG state.
|
||||
"""
|
||||
|
||||
# Shared CUDA RNG generator (initialized in on_train_start).
|
||||
cuda_generator: torch.Generator | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
cfg: Any,
|
||||
role_models: dict[str, ModelBase],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.tracker: Any | None = None
|
||||
self._role_models: dict[str, ModelBase] = dict(role_models)
|
||||
|
||||
self.student = role_models["student"]
|
||||
self.training_config = cfg.training
|
||||
self.method_config: dict[str, Any] = dict(cfg.method)
|
||||
self.validation_config: dict[str, Any] = dict(getattr(cfg, "validation", {}) or {})
|
||||
|
||||
# Build nn.ModuleDict for FSDP / checkpoint visibility.
|
||||
self.role_modules = torch.nn.ModuleDict()
|
||||
for role, model in role_models.items():
|
||||
mods: dict[str, torch.nn.Module] = {}
|
||||
transformer = getattr(model, "transformer", None)
|
||||
if isinstance(transformer, torch.nn.Module):
|
||||
mods["transformer"] = transformer
|
||||
if mods:
|
||||
self.role_modules[role] = torch.nn.ModuleDict(mods)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def set_tracker(self, tracker: Any) -> None:
|
||||
self.tracker = tracker
|
||||
|
||||
@abstractmethod
|
||||
def single_train_step(
|
||||
self,
|
||||
batch: dict[str, Any],
|
||||
iteration: int,
|
||||
) -> tuple[
|
||||
dict[str, torch.Tensor],
|
||||
dict[str, Any],
|
||||
dict[str, LogScalar],
|
||||
]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_optimizers(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> Sequence[torch.optim.Optimizer]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_lr_schedulers(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> Sequence[Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _optimizer_dict(self) -> dict[str, Any]:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _lr_scheduler_dict(self) -> dict[str, Any]:
|
||||
...
|
||||
|
||||
def checkpoint_state(self) -> dict[str, Any]:
|
||||
"""Return DCP-ready checkpoint state for all trainable roles.
|
||||
|
||||
Keys follow the convention:
|
||||
``roles.<role>.<module>``, ``optimizers.<role>``,
|
||||
``schedulers.<role>``, ``random_state.*``.
|
||||
|
||||
EMA state is managed by the ``EMACallback`` and is
|
||||
checkpointed through the callback state mechanism.
|
||||
"""
|
||||
states: dict[str, Any] = {}
|
||||
|
||||
for role, model in self._role_models.items():
|
||||
if not getattr(model, "_trainable", False):
|
||||
continue
|
||||
|
||||
modules: dict[str, torch.nn.Module] = {}
|
||||
if model.transformer is not None:
|
||||
modules["transformer"] = model.transformer
|
||||
|
||||
container = _RoleModuleContainer(modules)
|
||||
|
||||
for module_name, module in modules.items():
|
||||
states[f"roles.{role}.{module_name}"] = ModelWrapper(module)
|
||||
|
||||
opt = self._optimizer_dict.get(role)
|
||||
if opt is not None:
|
||||
states[f"optimizers.{role}"] = OptimizerWrapper(container, opt)
|
||||
|
||||
sched = self._lr_scheduler_dict.get(role)
|
||||
if sched is not None:
|
||||
states[f"schedulers.{role}"] = SchedulerWrapper(sched)
|
||||
|
||||
return states
|
||||
|
||||
def backward(
|
||||
self,
|
||||
loss_map: dict[str, torch.Tensor],
|
||||
outputs: dict[str, Any],
|
||||
*,
|
||||
grad_accum_rounds: int = 1,
|
||||
) -> None:
|
||||
del outputs
|
||||
grad_accum_rounds = max(1, int(grad_accum_rounds))
|
||||
(loss_map["total_loss"] / grad_accum_rounds).backward()
|
||||
|
||||
def optimizers_schedulers_step(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> None:
|
||||
for optimizer in self.get_optimizers(iteration):
|
||||
optimizer.step()
|
||||
for scheduler in self.get_lr_schedulers(iteration):
|
||||
scheduler.step()
|
||||
|
||||
def optimizers_zero_grad(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> None:
|
||||
for optimizer in self.get_optimizers(iteration):
|
||||
try:
|
||||
optimizer.zero_grad(set_to_none=True)
|
||||
except TypeError:
|
||||
optimizer.zero_grad()
|
||||
|
||||
def seed_optimizer_state_for_resume(self) -> None:
|
||||
"""Seed optimizer state so DCP can load saved state.
|
||||
|
||||
A fresh optimizer has empty state (exp_avg, exp_avg_sq,
|
||||
step are only created on the first optimizer.step()).
|
||||
DCP needs matching entries to load into; without them
|
||||
the saved optimizer state is silently dropped.
|
||||
"""
|
||||
for opt in self.get_optimizers(0):
|
||||
for group in opt.param_groups:
|
||||
for p in group["params"]:
|
||||
if not p.requires_grad:
|
||||
continue
|
||||
if len(opt.state.get(p, {})) > 0:
|
||||
continue
|
||||
opt.state[p] = {
|
||||
"step": torch.tensor(0.0),
|
||||
"exp_avg": torch.zeros_like(p),
|
||||
"exp_avg_sq": torch.zeros_like(p),
|
||||
}
|
||||
|
||||
# -- Shared hooks (override in subclasses as needed) --
|
||||
|
||||
def get_grad_clip_targets(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> dict[str, torch.nn.Module]:
|
||||
"""Return modules whose gradients should be clipped.
|
||||
|
||||
Override in subclasses to add/conditionally include
|
||||
modules (e.g. critic, conditionally student).
|
||||
Default: student transformer.
|
||||
"""
|
||||
return {"student": self.student.transformer}
|
||||
|
||||
def on_train_start(self) -> None:
|
||||
from fastvideo.distributed import (
|
||||
get_world_group, )
|
||||
from fastvideo.utils import set_random_seed
|
||||
|
||||
seed = self.training_config.data.seed
|
||||
if seed is None:
|
||||
raise ValueError("training.data.seed must be set")
|
||||
seed = int(seed)
|
||||
|
||||
world_group = get_world_group()
|
||||
global_rank = int(world_group.rank)
|
||||
sp_size = int(self.training_config.distributed.sp_size or 1)
|
||||
|
||||
# Ranks within the same SP group share a seed.
|
||||
if sp_size > 1:
|
||||
sp_group_seed = seed + (global_rank // sp_size)
|
||||
else:
|
||||
sp_group_seed = seed + global_rank
|
||||
|
||||
set_random_seed(seed)
|
||||
|
||||
self.cuda_generator = torch.Generator(device=self.student.device).manual_seed(sp_group_seed)
|
||||
|
||||
self.student.on_train_start()
|
||||
|
||||
@staticmethod
|
||||
def _infer_attn_kind() -> Literal["dense", "vsa"]:
|
||||
"""Derive attn_kind from ``FASTVIDEO_ATTENTION_BACKEND``."""
|
||||
backend = envs.FASTVIDEO_ATTENTION_BACKEND
|
||||
if backend == "VIDEO_SPARSE_ATTN":
|
||||
return "vsa"
|
||||
return "dense"
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
__all__: list[str] = []
|
||||
@@ -0,0 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from fastvideo.train.methods.distribution_matching.dmd2 import DMD2Method
|
||||
from fastvideo.train.methods.distribution_matching.self_forcing import (
|
||||
SelfForcingMethod, )
|
||||
|
||||
__all__ = [
|
||||
"DMD2Method",
|
||||
"SelfForcingMethod",
|
||||
]
|
||||
@@ -0,0 +1,630 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""DMD2 distillation method (algorithm layer)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from fastvideo.train.methods.base import TrainingMethod, LogScalar
|
||||
from fastvideo.train.models.base import ModelBase
|
||||
from fastvideo.train.utils.optimizer import (
|
||||
build_optimizer_and_scheduler, )
|
||||
from fastvideo.train.utils.config import (
|
||||
get_optional_float,
|
||||
get_optional_int,
|
||||
parse_betas,
|
||||
)
|
||||
|
||||
|
||||
class DMD2Method(TrainingMethod):
|
||||
"""DMD2 distillation algorithm (method layer).
|
||||
|
||||
Owns role model instances directly:
|
||||
- ``self.student`` — trainable student :class:`ModelBase`
|
||||
- ``self.teacher`` — frozen teacher :class:`ModelBase`
|
||||
- ``self.critic`` — trainable critic :class:`ModelBase`
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
cfg: Any,
|
||||
role_models: dict[str, ModelBase],
|
||||
) -> None:
|
||||
super().__init__(cfg=cfg, role_models=role_models)
|
||||
|
||||
if "student" not in role_models:
|
||||
raise ValueError("DMD2Method requires role 'student'")
|
||||
if "teacher" not in role_models:
|
||||
raise ValueError("DMD2Method requires role 'teacher'")
|
||||
if "critic" not in role_models:
|
||||
raise ValueError("DMD2Method requires role 'critic'")
|
||||
|
||||
self.teacher = role_models["teacher"]
|
||||
self.critic = role_models["critic"]
|
||||
|
||||
if not self.student._trainable:
|
||||
raise ValueError("DMD2Method requires student to be trainable")
|
||||
if self.teacher._trainable:
|
||||
raise ValueError("DMD2Method requires teacher to be "
|
||||
"non-trainable")
|
||||
if not self.critic._trainable:
|
||||
raise ValueError("DMD2Method requires critic to be trainable")
|
||||
self._cfg_uncond = self._parse_cfg_uncond()
|
||||
self._rollout_mode = self._parse_rollout_mode()
|
||||
self._denoising_step_list: torch.Tensor | None = (None)
|
||||
|
||||
# Initialize preprocessors on student.
|
||||
self.student.init_preprocessors(self.training_config)
|
||||
|
||||
self._init_optimizers_and_schedulers()
|
||||
|
||||
@property
|
||||
def _optimizer_dict(self, ) -> dict[str, torch.optim.Optimizer]:
|
||||
return {
|
||||
"student": self._student_optimizer,
|
||||
"critic": self._critic_optimizer,
|
||||
}
|
||||
|
||||
@property
|
||||
def _lr_scheduler_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"student": self._student_lr_scheduler,
|
||||
"critic": self._critic_lr_scheduler,
|
||||
}
|
||||
|
||||
# TrainingMethod override: single_train_step
|
||||
def single_train_step(
|
||||
self,
|
||||
batch: dict[str, Any],
|
||||
iteration: int,
|
||||
) -> tuple[
|
||||
dict[str, torch.Tensor],
|
||||
dict[str, Any],
|
||||
dict[str, LogScalar],
|
||||
]:
|
||||
latents_source: Literal["data", "zeros"] = "data"
|
||||
if self._rollout_mode == "simulate":
|
||||
latents_source = "zeros"
|
||||
|
||||
training_batch = self.student.prepare_batch(
|
||||
batch,
|
||||
generator=self.cuda_generator,
|
||||
latents_source=latents_source,
|
||||
)
|
||||
|
||||
update_student = self._should_update_student(iteration)
|
||||
|
||||
generator_loss = torch.zeros(
|
||||
(),
|
||||
device=training_batch.latents.device,
|
||||
dtype=training_batch.latents.dtype,
|
||||
)
|
||||
student_ctx = None
|
||||
if update_student:
|
||||
generator_pred_x0 = self._student_rollout(training_batch, with_grad=True)
|
||||
student_ctx = (
|
||||
training_batch.timesteps,
|
||||
training_batch.attn_metadata_vsa,
|
||||
)
|
||||
generator_loss = self._dmd_loss(generator_pred_x0, training_batch)
|
||||
|
||||
(
|
||||
fake_score_loss,
|
||||
critic_ctx,
|
||||
critic_outputs,
|
||||
) = self._critic_flow_matching_loss(training_batch)
|
||||
|
||||
total_loss = generator_loss + fake_score_loss
|
||||
loss_map = {
|
||||
"total_loss": total_loss,
|
||||
"generator_loss": generator_loss,
|
||||
"fake_score_loss": fake_score_loss,
|
||||
}
|
||||
|
||||
outputs: dict[str, Any] = dict(critic_outputs)
|
||||
outputs["_fv_backward"] = {
|
||||
"update_student": update_student,
|
||||
"student_ctx": student_ctx,
|
||||
"critic_ctx": critic_ctx,
|
||||
}
|
||||
metrics: dict[str, LogScalar] = {"update_student": float(update_student)}
|
||||
return loss_map, outputs, metrics
|
||||
|
||||
# TrainingMethod override: backward
|
||||
def backward(
|
||||
self,
|
||||
loss_map: dict[str, torch.Tensor],
|
||||
outputs: dict[str, Any],
|
||||
*,
|
||||
grad_accum_rounds: int = 1,
|
||||
) -> None:
|
||||
grad_accum_rounds = max(1, int(grad_accum_rounds))
|
||||
backward_ctx = outputs.get("_fv_backward")
|
||||
if not isinstance(backward_ctx, dict):
|
||||
super().backward(
|
||||
loss_map,
|
||||
outputs,
|
||||
grad_accum_rounds=grad_accum_rounds,
|
||||
)
|
||||
return
|
||||
|
||||
update_student = bool(backward_ctx.get("update_student", False))
|
||||
if update_student:
|
||||
student_ctx = backward_ctx.get("student_ctx")
|
||||
if student_ctx is None:
|
||||
raise RuntimeError("Missing student backward context")
|
||||
self.student.backward(
|
||||
loss_map["generator_loss"],
|
||||
student_ctx,
|
||||
grad_accum_rounds=grad_accum_rounds,
|
||||
)
|
||||
|
||||
critic_ctx = backward_ctx.get("critic_ctx")
|
||||
if critic_ctx is None:
|
||||
raise RuntimeError("Missing critic backward context")
|
||||
self.critic.backward(
|
||||
loss_map["fake_score_loss"],
|
||||
critic_ctx,
|
||||
grad_accum_rounds=grad_accum_rounds,
|
||||
)
|
||||
|
||||
# TrainingMethod override: get_optimizers
|
||||
def get_optimizers(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> list[torch.optim.Optimizer]:
|
||||
optimizers: list[torch.optim.Optimizer] = []
|
||||
optimizers.append(self._critic_optimizer)
|
||||
if self._should_update_student(iteration):
|
||||
optimizers.append(self._student_optimizer)
|
||||
return optimizers
|
||||
|
||||
# TrainingMethod override: get_lr_schedulers
|
||||
def get_lr_schedulers(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> list[Any]:
|
||||
schedulers: list[Any] = []
|
||||
schedulers.append(self._critic_lr_scheduler)
|
||||
if self._should_update_student(iteration):
|
||||
schedulers.append(self._student_lr_scheduler)
|
||||
return schedulers
|
||||
|
||||
# TrainingMethod override: get_grad_clip_targets
|
||||
def get_grad_clip_targets(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> dict[str, torch.nn.Module]:
|
||||
targets: dict[str, torch.nn.Module] = {}
|
||||
if self._should_update_student(iteration):
|
||||
targets["student"] = (self.student.transformer)
|
||||
targets["critic"] = self.critic.transformer
|
||||
return targets
|
||||
|
||||
def _parse_rollout_mode(self, ) -> Literal["simulate", "data_latent"]:
|
||||
raw = self.method_config.get("rollout_mode", None)
|
||||
if raw is None:
|
||||
raise ValueError("method_config.rollout_mode must be set "
|
||||
"for DMD2")
|
||||
if not isinstance(raw, str):
|
||||
raise ValueError("method_config.rollout_mode must be a "
|
||||
"string, "
|
||||
f"got {type(raw).__name__}")
|
||||
mode = raw.strip().lower()
|
||||
if mode in ("simulate", "sim"):
|
||||
return "simulate"
|
||||
if mode in ("data_latent", "data", "vae_latent"):
|
||||
return "data_latent"
|
||||
raise ValueError("method_config.rollout_mode must be one of "
|
||||
"{simulate, data_latent}, got "
|
||||
f"{raw!r}")
|
||||
|
||||
def _parse_cfg_uncond(self, ) -> dict[str, Any] | None:
|
||||
raw = self.method_config.get("cfg_uncond", None)
|
||||
if raw is None:
|
||||
return None
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("method_config.cfg_uncond must be a dict "
|
||||
f"when set, got {type(raw).__name__}")
|
||||
|
||||
cfg: dict[str, Any] = dict(raw)
|
||||
|
||||
on_missing_raw = cfg.get("on_missing", "error")
|
||||
if on_missing_raw is None:
|
||||
on_missing_raw = "error"
|
||||
if not isinstance(on_missing_raw, str):
|
||||
raise ValueError("method_config.cfg_uncond.on_missing must "
|
||||
"be a string, got "
|
||||
f"{type(on_missing_raw).__name__}")
|
||||
on_missing = on_missing_raw.strip().lower()
|
||||
if on_missing not in {"error", "ignore"}:
|
||||
raise ValueError("method_config.cfg_uncond.on_missing must "
|
||||
"be one of {error, ignore}, got "
|
||||
f"{on_missing_raw!r}")
|
||||
cfg["on_missing"] = on_missing
|
||||
|
||||
for channel, policy_raw in list(cfg.items()):
|
||||
if channel == "on_missing":
|
||||
continue
|
||||
if policy_raw is None:
|
||||
continue
|
||||
if not isinstance(policy_raw, str):
|
||||
raise ValueError("method_config.cfg_uncond values must "
|
||||
"be strings, got "
|
||||
f"{channel}="
|
||||
f"{type(policy_raw).__name__}")
|
||||
policy = policy_raw.strip().lower()
|
||||
allowed = {"keep", "zero", "drop"}
|
||||
if channel == "text":
|
||||
allowed = {*allowed, "negative_prompt"}
|
||||
if policy not in allowed:
|
||||
raise ValueError("method_config.cfg_uncond values must "
|
||||
"be one of "
|
||||
f"{sorted(allowed)}, got "
|
||||
f"{channel}={policy_raw!r}")
|
||||
cfg[channel] = policy
|
||||
|
||||
return cfg
|
||||
|
||||
def _init_optimizers_and_schedulers(self) -> None:
|
||||
tc = self.training_config
|
||||
|
||||
# Student optimizer/scheduler.
|
||||
student_lr = float(tc.optimizer.learning_rate)
|
||||
student_betas = tc.optimizer.betas
|
||||
student_sched = str(tc.optimizer.lr_scheduler)
|
||||
student_params = [p for p in self.student.transformer.parameters() if p.requires_grad]
|
||||
(
|
||||
self._student_optimizer,
|
||||
self._student_lr_scheduler,
|
||||
) = build_optimizer_and_scheduler(
|
||||
params=student_params,
|
||||
optimizer_config=tc.optimizer,
|
||||
loop_config=tc.loop,
|
||||
learning_rate=student_lr,
|
||||
betas=student_betas,
|
||||
scheduler_name=student_sched,
|
||||
)
|
||||
|
||||
# Critic optimizer/scheduler — must be set in
|
||||
# method config.
|
||||
critic_lr_raw = get_optional_float(
|
||||
self.method_config,
|
||||
"fake_score_learning_rate",
|
||||
where="method.fake_score_learning_rate",
|
||||
)
|
||||
if critic_lr_raw is None or critic_lr_raw == 0.0:
|
||||
raise ValueError("method.fake_score_learning_rate must "
|
||||
"be set to a positive value")
|
||||
critic_lr = float(critic_lr_raw)
|
||||
|
||||
critic_betas_raw = self.method_config.get("fake_score_betas", None)
|
||||
if critic_betas_raw is None:
|
||||
raise ValueError("method.fake_score_betas must be set "
|
||||
"(e.g. [0.0, 0.999])")
|
||||
critic_betas = parse_betas(
|
||||
critic_betas_raw,
|
||||
where="method.fake_score_betas",
|
||||
)
|
||||
|
||||
critic_sched_raw = self.method_config.get("fake_score_lr_scheduler", None)
|
||||
if critic_sched_raw is None:
|
||||
raise ValueError("method.fake_score_lr_scheduler must "
|
||||
"be set (e.g. 'constant')")
|
||||
critic_sched = str(critic_sched_raw)
|
||||
critic_params = [p for p in self.critic.transformer.parameters() if p.requires_grad]
|
||||
(
|
||||
self._critic_optimizer,
|
||||
self._critic_lr_scheduler,
|
||||
) = build_optimizer_and_scheduler(
|
||||
params=critic_params,
|
||||
optimizer_config=tc.optimizer,
|
||||
loop_config=tc.loop,
|
||||
learning_rate=critic_lr,
|
||||
betas=critic_betas,
|
||||
scheduler_name=critic_sched,
|
||||
)
|
||||
|
||||
def _should_update_student(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> bool:
|
||||
interval = get_optional_int(
|
||||
self.method_config,
|
||||
"generator_update_interval",
|
||||
where="method.generator_update_interval",
|
||||
)
|
||||
if interval is None:
|
||||
interval = 1
|
||||
if interval <= 0:
|
||||
return True
|
||||
return iteration % interval == 0
|
||||
|
||||
def _get_denoising_step_list(
|
||||
self,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
if (self._denoising_step_list is not None and self._denoising_step_list.device == device):
|
||||
return self._denoising_step_list
|
||||
|
||||
raw = self.method_config.get("dmd_denoising_steps", None)
|
||||
if not isinstance(raw, list) or not raw:
|
||||
raise ValueError("method_config.dmd_denoising_steps must "
|
||||
"be set for DMD2 distillation")
|
||||
|
||||
steps = torch.tensor(
|
||||
[int(s) for s in raw],
|
||||
dtype=torch.long,
|
||||
device=device,
|
||||
)
|
||||
|
||||
warp = self.method_config.get("warp_denoising_step", None)
|
||||
if warp is None:
|
||||
warp = False
|
||||
if bool(warp):
|
||||
timesteps = torch.cat((
|
||||
self.student.noise_scheduler.timesteps.to("cpu"),
|
||||
torch.tensor([0], dtype=torch.float32),
|
||||
)).to(device)
|
||||
steps = timesteps[1000 - steps]
|
||||
|
||||
self._denoising_step_list = steps
|
||||
return steps
|
||||
|
||||
def _sample_rollout_timestep(
|
||||
self,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
step_list = self._get_denoising_step_list(device)
|
||||
index = torch.randint(
|
||||
0,
|
||||
len(step_list),
|
||||
[1],
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
return step_list[index]
|
||||
|
||||
def _student_rollout(
|
||||
self,
|
||||
batch: Any,
|
||||
*,
|
||||
with_grad: bool,
|
||||
) -> torch.Tensor:
|
||||
latents = batch.latents
|
||||
device = latents.device
|
||||
dtype = latents.dtype
|
||||
step_list = self._get_denoising_step_list(device)
|
||||
|
||||
if self._rollout_mode != "simulate":
|
||||
timestep = self._sample_rollout_timestep(device)
|
||||
noise = torch.randn(
|
||||
latents.shape,
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
noisy_latents = self.student.add_noise(latents, noise, timestep)
|
||||
pred_x0 = self.student.predict_x0(
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="vsa",
|
||||
)
|
||||
batch.dmd_latent_vis_dict["generator_timestep"] = timestep
|
||||
return pred_x0
|
||||
|
||||
target_timestep_idx = torch.randint(
|
||||
0,
|
||||
len(step_list),
|
||||
[1],
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
target_timestep_idx_int = int(target_timestep_idx.item())
|
||||
target_timestep = step_list[target_timestep_idx]
|
||||
|
||||
current_noise_latents = torch.randn(
|
||||
latents.shape,
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
current_noise_latents_copy = (current_noise_latents.clone())
|
||||
|
||||
max_target_idx = len(step_list) - 1
|
||||
noise_latents: list[torch.Tensor] = []
|
||||
noise_latent_index = target_timestep_idx_int - 1
|
||||
|
||||
if max_target_idx > 0:
|
||||
with torch.no_grad():
|
||||
for step_idx in range(max_target_idx):
|
||||
current_timestep = step_list[step_idx]
|
||||
current_timestep_tensor = (current_timestep * torch.ones(
|
||||
1,
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
))
|
||||
|
||||
pred_clean = self.student.predict_x0(
|
||||
current_noise_latents,
|
||||
current_timestep_tensor,
|
||||
batch,
|
||||
conditional=True,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="vsa",
|
||||
)
|
||||
|
||||
next_timestep = step_list[step_idx + 1]
|
||||
next_timestep_tensor = (next_timestep * torch.ones(
|
||||
1,
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
))
|
||||
noise = torch.randn(
|
||||
latents.shape,
|
||||
device=device,
|
||||
dtype=pred_clean.dtype,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
current_noise_latents = (self.student.add_noise(
|
||||
pred_clean,
|
||||
noise,
|
||||
next_timestep_tensor,
|
||||
))
|
||||
noise_latents.append(current_noise_latents.clone())
|
||||
|
||||
if noise_latent_index >= 0:
|
||||
if noise_latent_index >= len(noise_latents):
|
||||
raise RuntimeError("noise_latent_index is out of bounds")
|
||||
noisy_input = noise_latents[noise_latent_index]
|
||||
else:
|
||||
noisy_input = current_noise_latents_copy
|
||||
|
||||
if with_grad:
|
||||
pred_x0 = self.student.predict_x0(
|
||||
noisy_input,
|
||||
target_timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="vsa",
|
||||
)
|
||||
else:
|
||||
with torch.no_grad():
|
||||
pred_x0 = self.student.predict_x0(
|
||||
noisy_input,
|
||||
target_timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="vsa",
|
||||
)
|
||||
|
||||
batch.dmd_latent_vis_dict["generator_timestep"] = target_timestep.float().detach()
|
||||
return pred_x0
|
||||
|
||||
def _critic_flow_matching_loss(
|
||||
self,
|
||||
batch: Any,
|
||||
) -> tuple[torch.Tensor, Any, dict[str, Any]]:
|
||||
with torch.no_grad():
|
||||
generator_pred_x0 = self._student_rollout(batch, with_grad=False)
|
||||
|
||||
device = generator_pred_x0.device
|
||||
fake_score_timestep = torch.randint(
|
||||
0,
|
||||
int(self.student.num_train_timesteps),
|
||||
[1],
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
fake_score_timestep = (self.student.shift_and_clamp_timestep(fake_score_timestep))
|
||||
|
||||
noise = torch.randn(
|
||||
generator_pred_x0.shape,
|
||||
device=device,
|
||||
dtype=generator_pred_x0.dtype,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
noisy_x0 = self.student.add_noise(generator_pred_x0, noise, fake_score_timestep)
|
||||
|
||||
pred_noise = self.critic.predict_noise(
|
||||
noisy_x0,
|
||||
fake_score_timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="dense",
|
||||
)
|
||||
target = noise - generator_pred_x0
|
||||
flow_matching_loss = torch.mean((pred_noise - target)**2)
|
||||
|
||||
batch.fake_score_latent_vis_dict = {
|
||||
"generator_pred_video": generator_pred_x0,
|
||||
"fake_score_timestep": fake_score_timestep,
|
||||
}
|
||||
outputs = {"fake_score_latent_vis_dict": (batch.fake_score_latent_vis_dict)}
|
||||
return (
|
||||
flow_matching_loss,
|
||||
(batch.timesteps, batch.attn_metadata),
|
||||
outputs,
|
||||
)
|
||||
|
||||
def _dmd_loss(
|
||||
self,
|
||||
generator_pred_x0: torch.Tensor,
|
||||
batch: Any,
|
||||
) -> torch.Tensor:
|
||||
guidance_scale = get_optional_float(
|
||||
self.method_config,
|
||||
"real_score_guidance_scale",
|
||||
where="method.real_score_guidance_scale",
|
||||
)
|
||||
if guidance_scale is None:
|
||||
guidance_scale = 1.0
|
||||
device = generator_pred_x0.device
|
||||
|
||||
with torch.no_grad():
|
||||
timestep = torch.randint(
|
||||
0,
|
||||
int(self.student.num_train_timesteps),
|
||||
[1],
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
timestep = (self.student.shift_and_clamp_timestep(timestep))
|
||||
|
||||
noise = torch.randn(
|
||||
generator_pred_x0.shape,
|
||||
device=device,
|
||||
dtype=generator_pred_x0.dtype,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
noisy_latents = self.student.add_noise(generator_pred_x0, noise, timestep)
|
||||
|
||||
faker_x0 = self.critic.predict_x0(
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="dense",
|
||||
)
|
||||
real_cond_x0 = self.teacher.predict_x0(
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="dense",
|
||||
)
|
||||
real_uncond_x0 = self.teacher.predict_x0(
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=False,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="dense",
|
||||
)
|
||||
real_cfg_x0 = real_uncond_x0 + (real_cond_x0 - real_uncond_x0) * guidance_scale
|
||||
|
||||
denom = torch.abs(generator_pred_x0 - real_cfg_x0).mean()
|
||||
grad = (faker_x0 - real_cfg_x0) / denom
|
||||
grad = torch.nan_to_num(grad)
|
||||
|
||||
loss = 0.5 * F.mse_loss(
|
||||
generator_pred_x0.float(),
|
||||
(generator_pred_x0.float() - grad.float()).detach(),
|
||||
)
|
||||
return loss
|
||||
@@ -0,0 +1,576 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Self-Forcing distillation method (algorithm layer)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
from fastvideo.train.models.base import (
|
||||
CausalModelBase,
|
||||
ModelBase,
|
||||
)
|
||||
from fastvideo.train.methods.distribution_matching.dmd2 import (
|
||||
DMD2Method, )
|
||||
from fastvideo.train.utils.config import (
|
||||
get_optional_float,
|
||||
get_optional_int,
|
||||
)
|
||||
from fastvideo.models.schedulers.scheduling_self_forcing_flow_match import (
|
||||
SelfForcingFlowMatchScheduler, )
|
||||
from fastvideo.models.utils import pred_noise_to_pred_video
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.pipelines import TrainingBatch
|
||||
|
||||
|
||||
def _require_bool(raw: Any, *, where: str) -> bool:
|
||||
if isinstance(raw, bool):
|
||||
return raw
|
||||
raise ValueError(f"Expected bool at {where}, got {type(raw).__name__}")
|
||||
|
||||
|
||||
def _require_str(raw: Any, *, where: str) -> str:
|
||||
if not isinstance(raw, str) or not raw.strip():
|
||||
raise ValueError(f"Expected non-empty string at {where}")
|
||||
return raw
|
||||
|
||||
|
||||
class SelfForcingMethod(DMD2Method):
|
||||
"""Self-Forcing DMD2 (distribution matching) method.
|
||||
|
||||
Requires a causal student implementing ``CausalModelBase``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
cfg: Any,
|
||||
role_models: dict[str, ModelBase],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
cfg=cfg,
|
||||
role_models=role_models,
|
||||
)
|
||||
|
||||
# Validate causal student.
|
||||
if not isinstance(self.student, CausalModelBase):
|
||||
raise ValueError("SelfForcingMethod requires a causal student "
|
||||
"implementing CausalModelBase.")
|
||||
|
||||
if self._rollout_mode != "simulate":
|
||||
raise ValueError("SelfForcingMethod only supports "
|
||||
"method_config.rollout_mode='simulate'")
|
||||
|
||||
mcfg = self.method_config
|
||||
|
||||
chunk_size = get_optional_int(
|
||||
mcfg,
|
||||
"chunk_size",
|
||||
where="method_config.chunk_size",
|
||||
)
|
||||
if chunk_size is None:
|
||||
chunk_size = 3
|
||||
if chunk_size <= 0:
|
||||
raise ValueError("method_config.chunk_size must be a positive "
|
||||
f"integer, got {chunk_size}")
|
||||
self._chunk_size = int(chunk_size)
|
||||
|
||||
sample_type_raw = mcfg.get("student_sample_type", "sde")
|
||||
sample_type = _require_str(
|
||||
sample_type_raw,
|
||||
where="method_config.student_sample_type",
|
||||
)
|
||||
sample_type = sample_type.strip().lower()
|
||||
if sample_type not in {"sde", "ode"}:
|
||||
raise ValueError("method_config.student_sample_type must be one "
|
||||
f"of {{sde, ode}}, got {sample_type_raw!r}")
|
||||
self._student_sample_type: Literal["sde", "ode"] = (
|
||||
sample_type # type: ignore[assignment]
|
||||
)
|
||||
|
||||
same_step_raw = mcfg.get("same_step_across_blocks", False)
|
||||
if same_step_raw is None:
|
||||
same_step_raw = False
|
||||
self._same_step_across_blocks = _require_bool(
|
||||
same_step_raw,
|
||||
where="method_config.same_step_across_blocks",
|
||||
)
|
||||
|
||||
last_step_raw = mcfg.get("last_step_only", False)
|
||||
if last_step_raw is None:
|
||||
last_step_raw = False
|
||||
self._last_step_only = _require_bool(
|
||||
last_step_raw,
|
||||
where="method_config.last_step_only",
|
||||
)
|
||||
|
||||
context_noise = get_optional_float(
|
||||
mcfg,
|
||||
"context_noise",
|
||||
where="method_config.context_noise",
|
||||
)
|
||||
if context_noise is None:
|
||||
context_noise = 0.0
|
||||
if context_noise < 0.0:
|
||||
raise ValueError("method_config.context_noise must be >= 0, "
|
||||
f"got {context_noise}")
|
||||
self._context_noise = float(context_noise)
|
||||
|
||||
enable_grad_raw = mcfg.get("enable_gradient_in_rollout", True)
|
||||
if enable_grad_raw is None:
|
||||
enable_grad_raw = True
|
||||
self._enable_gradient_in_rollout = _require_bool(
|
||||
enable_grad_raw,
|
||||
where="method_config.enable_gradient_in_rollout",
|
||||
)
|
||||
|
||||
start_grad_frame = get_optional_int(
|
||||
mcfg,
|
||||
"start_gradient_frame",
|
||||
where="method_config.start_gradient_frame",
|
||||
)
|
||||
if start_grad_frame is None:
|
||||
start_grad_frame = 0
|
||||
if start_grad_frame < 0:
|
||||
raise ValueError("method_config.start_gradient_frame must be "
|
||||
f">= 0, got {start_grad_frame}")
|
||||
self._start_gradient_frame = int(start_grad_frame)
|
||||
|
||||
shift = float(getattr(
|
||||
self.training_config.pipeline_config,
|
||||
"flow_shift",
|
||||
0.0,
|
||||
) or 0.0)
|
||||
self._sf_scheduler = SelfForcingFlowMatchScheduler(
|
||||
num_inference_steps=1000,
|
||||
num_train_timesteps=int(self.student.num_train_timesteps),
|
||||
shift=shift,
|
||||
sigma_min=0.0,
|
||||
extra_one_step=True,
|
||||
training=True,
|
||||
)
|
||||
|
||||
self._sf_denoising_step_list: torch.Tensor | None = None
|
||||
|
||||
def _get_denoising_step_list(self, device: torch.device) -> torch.Tensor:
|
||||
if (self._sf_denoising_step_list is not None and self._sf_denoising_step_list.device == device):
|
||||
return self._sf_denoising_step_list
|
||||
|
||||
raw = self.method_config.get("dmd_denoising_steps", None)
|
||||
if not isinstance(raw, list) or not raw:
|
||||
raise ValueError("method_config.dmd_denoising_steps must be set "
|
||||
"for self_forcing")
|
||||
steps = torch.tensor(
|
||||
[int(s) for s in raw],
|
||||
dtype=torch.long,
|
||||
device=device,
|
||||
)
|
||||
|
||||
warp = self.method_config.get("warp_denoising_step", None)
|
||||
if warp is None:
|
||||
warp = False
|
||||
if bool(warp):
|
||||
timesteps = torch.cat((
|
||||
self._sf_scheduler.timesteps.to("cpu"),
|
||||
torch.tensor([0], dtype=torch.float32),
|
||||
)).to(device)
|
||||
steps = timesteps[int(self.student.num_train_timesteps) - steps]
|
||||
|
||||
self._sf_denoising_step_list = steps
|
||||
return steps
|
||||
|
||||
def _predict_x0_with_scheduler(
|
||||
self,
|
||||
model: ModelBase,
|
||||
noisy_latents: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
batch: TrainingBatch,
|
||||
*,
|
||||
conditional: bool,
|
||||
attn_kind: Literal["dense", "vsa"],
|
||||
) -> torch.Tensor:
|
||||
pred_noise = model.predict_noise(
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=conditional,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind=attn_kind,
|
||||
)
|
||||
pred_x0 = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=noisy_latents.flatten(0, 1),
|
||||
timestep=timestep,
|
||||
scheduler=self._sf_scheduler,
|
||||
).unflatten(0, pred_noise.shape[:2])
|
||||
return pred_x0
|
||||
|
||||
def _sf_add_noise(
|
||||
self,
|
||||
clean_latents: torch.Tensor,
|
||||
noise: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
b, t = clean_latents.shape[:2]
|
||||
noisy = self._sf_scheduler.add_noise(
|
||||
clean_latents.flatten(0, 1),
|
||||
noise.flatten(0, 1),
|
||||
timestep,
|
||||
).unflatten(0, (b, t))
|
||||
return noisy
|
||||
|
||||
def _timestep_to_sigma(self, timestep: torch.Tensor) -> torch.Tensor:
|
||||
sigmas = self._sf_scheduler.sigmas.to(device=timestep.device, dtype=torch.float32)
|
||||
timesteps = self._sf_scheduler.timesteps.to(device=timestep.device, dtype=torch.float32)
|
||||
t = timestep.to(device=timestep.device, dtype=torch.float32)
|
||||
if t.ndim == 2:
|
||||
t = t.flatten(0, 1)
|
||||
elif t.ndim == 1 and t.numel() == 1:
|
||||
t = t.expand(1)
|
||||
elif t.ndim != 1:
|
||||
raise ValueError("Invalid timestep shape: "
|
||||
f"{tuple(timestep.shape)}")
|
||||
idx = torch.argmin(
|
||||
(timesteps.unsqueeze(0) - t.unsqueeze(1)).abs(),
|
||||
dim=1,
|
||||
)
|
||||
return sigmas[idx]
|
||||
|
||||
def _sample_exit_indices(
|
||||
self,
|
||||
*,
|
||||
num_blocks: int,
|
||||
num_steps: int,
|
||||
device: torch.device,
|
||||
) -> list[int]:
|
||||
if num_blocks <= 0:
|
||||
return []
|
||||
if num_steps <= 0:
|
||||
raise ValueError("num_steps must be positive")
|
||||
|
||||
shape = ((1, ) if self._same_step_across_blocks else (num_blocks, ))
|
||||
|
||||
if not dist.is_initialized() or dist.get_rank() == 0:
|
||||
if self._last_step_only:
|
||||
indices = torch.full(
|
||||
shape,
|
||||
num_steps - 1,
|
||||
dtype=torch.long,
|
||||
device=device,
|
||||
)
|
||||
else:
|
||||
indices = torch.randint(
|
||||
low=0,
|
||||
high=num_steps,
|
||||
size=shape,
|
||||
device=device,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
else:
|
||||
indices = torch.empty(shape, dtype=torch.long, device=device)
|
||||
|
||||
if dist.is_initialized():
|
||||
dist.broadcast(indices, src=0)
|
||||
|
||||
if self._same_step_across_blocks:
|
||||
return [int(indices.item()) for _ in range(num_blocks)]
|
||||
return [int(i) for i in indices.tolist()]
|
||||
|
||||
def _student_rollout(self, batch: Any, *, with_grad: bool) -> torch.Tensor:
|
||||
if not isinstance(self.student, CausalModelBase):
|
||||
raise ValueError("SelfForcingMethod requires a causal student "
|
||||
"implementing CausalModelBase.")
|
||||
return self._student_rollout_streaming(batch, with_grad=with_grad)
|
||||
|
||||
def _student_rollout_streaming(self, batch: Any, *, with_grad: bool) -> torch.Tensor:
|
||||
assert isinstance(self.student, CausalModelBase)
|
||||
latents = batch.latents
|
||||
if latents is None:
|
||||
raise RuntimeError("TrainingBatch.latents is required for "
|
||||
"self-forcing rollout")
|
||||
if latents.ndim != 5:
|
||||
raise ValueError("TrainingBatch.latents must be [B, T, C, H, W]"
|
||||
f", got shape={tuple(latents.shape)}")
|
||||
|
||||
device = latents.device
|
||||
dtype = latents.dtype
|
||||
batch_size = int(latents.shape[0])
|
||||
num_frames = int(latents.shape[1])
|
||||
|
||||
denoising_steps = self._get_denoising_step_list(device)
|
||||
num_steps = int(denoising_steps.numel())
|
||||
|
||||
noise_full = torch.randn_like(latents, device=device, dtype=dtype)
|
||||
|
||||
chunk = int(self._chunk_size)
|
||||
if chunk <= 0:
|
||||
raise ValueError("chunk_size must be positive")
|
||||
|
||||
remaining = num_frames % chunk
|
||||
num_blocks = num_frames // chunk
|
||||
if num_blocks == 0:
|
||||
num_blocks = 1
|
||||
remaining = num_frames
|
||||
|
||||
exit_indices = self._sample_exit_indices(
|
||||
num_blocks=num_blocks,
|
||||
num_steps=num_steps,
|
||||
device=device,
|
||||
)
|
||||
|
||||
denoised_blocks: list[torch.Tensor] = []
|
||||
|
||||
cache_tag = "pos"
|
||||
self.student.clear_caches(cache_tag=cache_tag)
|
||||
|
||||
for block_idx in range(num_blocks):
|
||||
if block_idx == 0:
|
||||
start = 0
|
||||
end = remaining + chunk if remaining else chunk
|
||||
else:
|
||||
start = remaining + block_idx * chunk
|
||||
end = remaining + (block_idx + 1) * chunk
|
||||
start = int(start)
|
||||
end = int(min(end, num_frames))
|
||||
if start >= end:
|
||||
break
|
||||
|
||||
noisy_block = noise_full[:, start:end]
|
||||
exit_idx = int(exit_indices[block_idx])
|
||||
|
||||
for step_idx, current_timestep in enumerate(denoising_steps):
|
||||
exit_flag = step_idx == exit_idx
|
||||
|
||||
timestep_block = (current_timestep * torch.ones(
|
||||
(batch_size, end - start),
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
))
|
||||
|
||||
enable_grad = (bool(with_grad) and bool(self._enable_gradient_in_rollout) and torch.is_grad_enabled()
|
||||
and start >= int(self._start_gradient_frame))
|
||||
|
||||
if not exit_flag:
|
||||
with torch.no_grad():
|
||||
pred_noise = (self.student.predict_noise_streaming(
|
||||
noisy_block,
|
||||
timestep_block,
|
||||
batch,
|
||||
conditional=True,
|
||||
cache_tag=cache_tag,
|
||||
store_kv=False,
|
||||
cur_start_frame=start,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="vsa",
|
||||
))
|
||||
if pred_noise is None:
|
||||
raise RuntimeError("predict_noise_streaming "
|
||||
"returned None "
|
||||
"(store_kv=False)")
|
||||
pred_x0_chunk = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=(noisy_block.flatten(0, 1)),
|
||||
timestep=timestep_block,
|
||||
scheduler=self._sf_scheduler,
|
||||
).unflatten(0, pred_noise.shape[:2])
|
||||
|
||||
if step_idx + 1 >= num_steps:
|
||||
break
|
||||
next_timestep = denoising_steps[step_idx + 1]
|
||||
if self._student_sample_type == "sde":
|
||||
noisy_block = self._sf_add_noise(
|
||||
pred_x0_chunk,
|
||||
torch.randn_like(pred_x0_chunk),
|
||||
next_timestep * torch.ones(
|
||||
(batch_size, end - start),
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
),
|
||||
)
|
||||
else:
|
||||
sigma_cur = self._timestep_to_sigma(timestep_block).view(batch_size, end - start, 1, 1, 1)
|
||||
sigma_next = self._timestep_to_sigma(next_timestep * torch.ones(
|
||||
(batch_size, end - start),
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
)).view(batch_size, end - start, 1, 1, 1)
|
||||
eps = (noisy_block - (1 - sigma_cur) * pred_x0_chunk) / sigma_cur.clamp_min(1e-8)
|
||||
noisy_block = ((1 - sigma_next) * pred_x0_chunk + sigma_next * eps)
|
||||
continue
|
||||
|
||||
with torch.set_grad_enabled(enable_grad):
|
||||
pred_noise = (self.student.predict_noise_streaming(
|
||||
noisy_block,
|
||||
timestep_block,
|
||||
batch,
|
||||
conditional=True,
|
||||
cache_tag=cache_tag,
|
||||
store_kv=False,
|
||||
cur_start_frame=start,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="vsa",
|
||||
))
|
||||
if pred_noise is None:
|
||||
raise RuntimeError("predict_noise_streaming returned "
|
||||
"None (store_kv=False)")
|
||||
pred_x0_chunk = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=(noisy_block.flatten(0, 1)),
|
||||
timestep=timestep_block,
|
||||
scheduler=self._sf_scheduler,
|
||||
).unflatten(0, pred_noise.shape[:2])
|
||||
break
|
||||
|
||||
denoised_blocks.append(pred_x0_chunk)
|
||||
|
||||
with torch.no_grad():
|
||||
if self._context_noise > 0.0:
|
||||
context_timestep = torch.ones(
|
||||
(batch_size, end - start),
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
) * float(self._context_noise)
|
||||
context_latents = self._sf_add_noise(
|
||||
pred_x0_chunk.detach(),
|
||||
torch.randn_like(pred_x0_chunk),
|
||||
context_timestep,
|
||||
)
|
||||
else:
|
||||
context_timestep = torch.zeros(
|
||||
(batch_size, end - start),
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
context_latents = pred_x0_chunk.detach()
|
||||
|
||||
_ = self.student.predict_noise_streaming(
|
||||
context_latents,
|
||||
context_timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
cache_tag=cache_tag,
|
||||
store_kv=True,
|
||||
cur_start_frame=start,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="vsa",
|
||||
)
|
||||
|
||||
if not denoised_blocks:
|
||||
raise RuntimeError("Self-forcing rollout produced no blocks")
|
||||
|
||||
self.student.clear_caches(cache_tag=cache_tag)
|
||||
return torch.cat(denoised_blocks, dim=1)
|
||||
|
||||
def _critic_flow_matching_loss(self, batch: Any) -> tuple[torch.Tensor, Any, dict[str, Any]]:
|
||||
with torch.no_grad():
|
||||
generator_pred_x0 = self._student_rollout(batch, with_grad=False)
|
||||
|
||||
device = generator_pred_x0.device
|
||||
fake_score_timestep = torch.randint(
|
||||
0,
|
||||
int(self.student.num_train_timesteps),
|
||||
[1],
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
fake_score_timestep = (self.student.shift_and_clamp_timestep(fake_score_timestep))
|
||||
|
||||
noise = torch.randn(
|
||||
generator_pred_x0.shape,
|
||||
device=device,
|
||||
dtype=generator_pred_x0.dtype,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
noisy_x0 = self._sf_add_noise(generator_pred_x0, noise, fake_score_timestep)
|
||||
|
||||
pred_noise = self.critic.predict_noise(
|
||||
noisy_x0,
|
||||
fake_score_timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
cfg_uncond=self._cfg_uncond,
|
||||
attn_kind="dense",
|
||||
)
|
||||
target = noise - generator_pred_x0
|
||||
flow_matching_loss = torch.mean((pred_noise - target)**2)
|
||||
|
||||
batch.fake_score_latent_vis_dict = {
|
||||
"generator_pred_video": generator_pred_x0,
|
||||
"fake_score_timestep": fake_score_timestep,
|
||||
}
|
||||
outputs = {"fake_score_latent_vis_dict": (batch.fake_score_latent_vis_dict)}
|
||||
return (
|
||||
flow_matching_loss,
|
||||
(batch.timesteps, batch.attn_metadata),
|
||||
outputs,
|
||||
)
|
||||
|
||||
def _dmd_loss(
|
||||
self,
|
||||
generator_pred_x0: torch.Tensor,
|
||||
batch: Any,
|
||||
) -> torch.Tensor:
|
||||
guidance_scale = get_optional_float(
|
||||
self.method_config,
|
||||
"real_score_guidance_scale",
|
||||
where="method.real_score_guidance_scale",
|
||||
)
|
||||
if guidance_scale is None:
|
||||
guidance_scale = 1.0
|
||||
device = generator_pred_x0.device
|
||||
|
||||
with torch.no_grad():
|
||||
timestep = torch.randint(
|
||||
0,
|
||||
int(self.student.num_train_timesteps),
|
||||
[1],
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
timestep = self.student.shift_and_clamp_timestep(timestep)
|
||||
|
||||
noise = torch.randn(
|
||||
generator_pred_x0.shape,
|
||||
device=device,
|
||||
dtype=generator_pred_x0.dtype,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
noisy_latents = self._sf_add_noise(generator_pred_x0, noise, timestep)
|
||||
|
||||
faker_x0 = self._predict_x0_with_scheduler(
|
||||
self.critic,
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
attn_kind="dense",
|
||||
)
|
||||
real_cond_x0 = self._predict_x0_with_scheduler(
|
||||
self.teacher,
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=True,
|
||||
attn_kind="dense",
|
||||
)
|
||||
real_uncond_x0 = self._predict_x0_with_scheduler(
|
||||
self.teacher,
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=False,
|
||||
attn_kind="dense",
|
||||
)
|
||||
real_cfg_x0 = real_uncond_x0 + (real_cond_x0 - real_uncond_x0) * guidance_scale
|
||||
|
||||
denom = torch.abs(generator_pred_x0 - real_cfg_x0).mean()
|
||||
grad = (faker_x0 - real_cfg_x0) / denom
|
||||
grad = torch.nan_to_num(grad)
|
||||
|
||||
loss = 0.5 * torch.mean((generator_pred_x0.float() - (generator_pred_x0.float() - grad.float()).detach())**2)
|
||||
return loss
|
||||
@@ -0,0 +1,28 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.methods.fine_tuning.dfsft import DiffusionForcingSFTMethod
|
||||
from fastvideo.train.methods.fine_tuning.finetune import FineTuneMethod
|
||||
|
||||
__all__ = [
|
||||
"DiffusionForcingSFTMethod",
|
||||
"FineTuneMethod",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
# Lazy import to avoid circular imports during registry bring-up.
|
||||
if name == "DiffusionForcingSFTMethod":
|
||||
from fastvideo.train.methods.fine_tuning.dfsft import (
|
||||
DiffusionForcingSFTMethod, )
|
||||
|
||||
return DiffusionForcingSFTMethod
|
||||
if name == "FineTuneMethod":
|
||||
from fastvideo.train.methods.fine_tuning.finetune import FineTuneMethod
|
||||
|
||||
return FineTuneMethod
|
||||
raise AttributeError(name)
|
||||
@@ -0,0 +1,322 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Diffusion-forcing SFT method (DFSFT; algorithm layer)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from fastvideo.train.methods.base import TrainingMethod, LogScalar
|
||||
from fastvideo.train.models.base import ModelBase
|
||||
from fastvideo.train.utils.optimizer import (
|
||||
build_optimizer_and_scheduler, )
|
||||
|
||||
|
||||
class DiffusionForcingSFTMethod(TrainingMethod):
|
||||
"""Diffusion-forcing SFT (DFSFT): train only ``student``
|
||||
with inhomogeneous timesteps.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
cfg: Any,
|
||||
role_models: dict[str, ModelBase],
|
||||
) -> None:
|
||||
super().__init__(cfg=cfg, role_models=role_models)
|
||||
|
||||
if "student" not in role_models:
|
||||
raise ValueError("DFSFT requires role 'student'")
|
||||
if not self.student._trainable:
|
||||
raise ValueError("DFSFT requires student to be trainable")
|
||||
self._attn_kind: Literal["dense", "vsa"] = (self._infer_attn_kind())
|
||||
|
||||
self._chunk_size = self._parse_chunk_size(self.method_config.get("chunk_size", None))
|
||||
self._timestep_index_range = (self._parse_timestep_index_range())
|
||||
|
||||
# Initialize preprocessors on student.
|
||||
self.student.init_preprocessors(self.training_config)
|
||||
|
||||
self._init_optimizers_and_schedulers()
|
||||
|
||||
@property
|
||||
def _optimizer_dict(self) -> dict[str, Any]:
|
||||
return {"student": self._student_optimizer}
|
||||
|
||||
@property
|
||||
def _lr_scheduler_dict(self) -> dict[str, Any]:
|
||||
return {"student": self._student_lr_scheduler}
|
||||
|
||||
# TrainingMethod override: single_train_step
|
||||
def single_train_step(
|
||||
self,
|
||||
batch: dict[str, Any],
|
||||
iteration: int,
|
||||
) -> tuple[
|
||||
dict[str, torch.Tensor],
|
||||
dict[str, Any],
|
||||
dict[str, LogScalar],
|
||||
]:
|
||||
del iteration
|
||||
training_batch = self.student.prepare_batch(
|
||||
batch,
|
||||
generator=self.cuda_generator,
|
||||
latents_source="data",
|
||||
)
|
||||
|
||||
if training_batch.latents is None:
|
||||
raise RuntimeError("prepare_batch() must set TrainingBatch.latents")
|
||||
|
||||
clean_latents = training_batch.latents
|
||||
if not torch.is_tensor(clean_latents):
|
||||
raise TypeError("TrainingBatch.latents must be a torch.Tensor")
|
||||
if clean_latents.ndim != 5:
|
||||
raise ValueError("TrainingBatch.latents must be "
|
||||
"[B, T, C, H, W], got "
|
||||
f"shape={tuple(clean_latents.shape)}")
|
||||
|
||||
batch_size, num_latents = (
|
||||
int(clean_latents.shape[0]),
|
||||
int(clean_latents.shape[1]),
|
||||
)
|
||||
|
||||
expected_chunk = getattr(
|
||||
self.student.transformer,
|
||||
"num_frame_per_block",
|
||||
None,
|
||||
)
|
||||
if (expected_chunk is not None and int(expected_chunk) != int(self._chunk_size)):
|
||||
raise ValueError("DFSFT chunk_size must match "
|
||||
"transformer.num_frame_per_block for "
|
||||
f"causal training (got {self._chunk_size}, "
|
||||
f"expected {expected_chunk}).")
|
||||
|
||||
timestep_indices = self._sample_t_inhom_indices(
|
||||
batch_size=batch_size,
|
||||
num_latents=num_latents,
|
||||
device=clean_latents.device,
|
||||
)
|
||||
sp_size = int(self.training_config.distributed.sp_size)
|
||||
sp_group = getattr(self.student, "sp_group", None)
|
||||
if (sp_size > 1 and sp_group is not None and hasattr(sp_group, "broadcast")):
|
||||
sp_group.broadcast(timestep_indices, src=0)
|
||||
|
||||
scheduler = self.student.noise_scheduler
|
||||
if scheduler is None:
|
||||
raise ValueError("DFSFT requires student.noise_scheduler")
|
||||
|
||||
schedule_timesteps = scheduler.timesteps.to(device=clean_latents.device, dtype=torch.float32)
|
||||
schedule_sigmas = scheduler.sigmas.to(
|
||||
device=clean_latents.device,
|
||||
dtype=clean_latents.dtype,
|
||||
)
|
||||
t_inhom = schedule_timesteps[timestep_indices]
|
||||
|
||||
# Override the homogeneous timesteps from prepare_batch
|
||||
# so that set_forward_context (in predict_noise and
|
||||
# backward) receives the correct per-chunk timesteps.
|
||||
training_batch.timesteps = t_inhom
|
||||
|
||||
noise = getattr(training_batch, "noise", None)
|
||||
if noise is None:
|
||||
noise = torch.randn_like(clean_latents)
|
||||
else:
|
||||
if not torch.is_tensor(noise):
|
||||
raise TypeError("TrainingBatch.noise must be a "
|
||||
"torch.Tensor when set")
|
||||
noise = noise.permute(0, 2, 1, 3, 4).to(dtype=clean_latents.dtype)
|
||||
|
||||
noisy_latents = self.student.add_noise(
|
||||
clean_latents,
|
||||
noise,
|
||||
t_inhom.flatten(),
|
||||
)
|
||||
|
||||
pred = self.student.predict_noise(
|
||||
noisy_latents,
|
||||
t_inhom,
|
||||
training_batch,
|
||||
conditional=True,
|
||||
attn_kind=self._attn_kind,
|
||||
)
|
||||
|
||||
if bool(self.training_config.model.precondition_outputs):
|
||||
sigmas = schedule_sigmas[timestep_indices]
|
||||
sigmas = sigmas.unsqueeze(-1).unsqueeze(-1).unsqueeze(-1)
|
||||
pred_x0 = noisy_latents - pred * sigmas
|
||||
loss = F.mse_loss(pred_x0.float(), clean_latents.float())
|
||||
else:
|
||||
target = noise - clean_latents
|
||||
loss = F.mse_loss(pred.float(), target.float())
|
||||
|
||||
if self._attn_kind == "vsa":
|
||||
attn_metadata = training_batch.attn_metadata_vsa
|
||||
else:
|
||||
attn_metadata = training_batch.attn_metadata
|
||||
|
||||
loss_map = {"total_loss": loss, "dfsft_loss": loss}
|
||||
outputs: dict[str, Any] = {
|
||||
"_fv_backward": (
|
||||
training_batch.timesteps,
|
||||
attn_metadata,
|
||||
)
|
||||
}
|
||||
metrics: dict[str, LogScalar] = {}
|
||||
return loss_map, outputs, metrics
|
||||
|
||||
# TrainingMethod override: backward
|
||||
def backward(
|
||||
self,
|
||||
loss_map: dict[str, torch.Tensor],
|
||||
outputs: dict[str, Any],
|
||||
*,
|
||||
grad_accum_rounds: int = 1,
|
||||
) -> None:
|
||||
grad_accum_rounds = max(1, int(grad_accum_rounds))
|
||||
ctx = outputs.get("_fv_backward")
|
||||
if ctx is None:
|
||||
super().backward(
|
||||
loss_map,
|
||||
outputs,
|
||||
grad_accum_rounds=grad_accum_rounds,
|
||||
)
|
||||
return
|
||||
self.student.backward(
|
||||
loss_map["total_loss"],
|
||||
ctx,
|
||||
grad_accum_rounds=grad_accum_rounds,
|
||||
)
|
||||
|
||||
# TrainingMethod override: get_optimizers
|
||||
def get_optimizers(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> list[torch.optim.Optimizer]:
|
||||
del iteration
|
||||
return [self._student_optimizer]
|
||||
|
||||
# TrainingMethod override: get_lr_schedulers
|
||||
def get_lr_schedulers(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> list[Any]:
|
||||
del iteration
|
||||
return [self._student_lr_scheduler]
|
||||
|
||||
def _parse_chunk_size(self, raw: Any) -> int:
|
||||
if raw in (None, ""):
|
||||
return 3
|
||||
if isinstance(raw, bool):
|
||||
raise ValueError("method_config.chunk_size must be an int, "
|
||||
"got bool")
|
||||
if isinstance(raw, float) and not raw.is_integer():
|
||||
raise ValueError("method_config.chunk_size must be an int, "
|
||||
"got float")
|
||||
if isinstance(raw, str) and not raw.strip():
|
||||
raise ValueError("method_config.chunk_size must be an int, "
|
||||
"got empty string")
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError("method_config.chunk_size must be an int, "
|
||||
f"got {type(raw).__name__}") from e
|
||||
if value <= 0:
|
||||
raise ValueError("method_config.chunk_size must be > 0")
|
||||
return value
|
||||
|
||||
def _parse_ratio(
|
||||
self,
|
||||
raw: Any,
|
||||
*,
|
||||
where: str,
|
||||
default: float,
|
||||
) -> float:
|
||||
if raw in (None, ""):
|
||||
return float(default)
|
||||
if isinstance(raw, bool):
|
||||
raise ValueError(f"{where} must be a number/string, got bool")
|
||||
if isinstance(raw, int | float):
|
||||
return float(raw)
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return float(raw)
|
||||
raise ValueError(f"{where} must be a number/string, "
|
||||
f"got {type(raw).__name__}")
|
||||
|
||||
def _parse_timestep_index_range(self, ) -> tuple[int, int]:
|
||||
scheduler = self.student.noise_scheduler
|
||||
if scheduler is None:
|
||||
raise ValueError("DFSFT requires student.noise_scheduler")
|
||||
num_steps = int(getattr(scheduler, "config", scheduler).num_train_timesteps)
|
||||
|
||||
min_ratio = self._parse_ratio(
|
||||
self.method_config.get("min_timestep_ratio", None),
|
||||
where="method.min_timestep_ratio",
|
||||
default=0.0,
|
||||
)
|
||||
max_ratio = self._parse_ratio(
|
||||
self.method_config.get("max_timestep_ratio", None),
|
||||
where="method.max_timestep_ratio",
|
||||
default=1.0,
|
||||
)
|
||||
|
||||
if not (0.0 <= min_ratio <= 1.0 and 0.0 <= max_ratio <= 1.0):
|
||||
raise ValueError("DFSFT timestep ratios must be in [0,1], "
|
||||
f"got min={min_ratio}, max={max_ratio}")
|
||||
if max_ratio < min_ratio:
|
||||
raise ValueError("method_config.max_timestep_ratio must be "
|
||||
">= min_timestep_ratio")
|
||||
|
||||
min_index = int(min_ratio * num_steps)
|
||||
max_index = int(max_ratio * num_steps)
|
||||
min_index = max(0, min(min_index, num_steps - 1))
|
||||
max_index = max(0, min(max_index, num_steps - 1))
|
||||
|
||||
if max_index <= min_index:
|
||||
max_index = min(num_steps - 1, min_index + 1)
|
||||
|
||||
return min_index, max_index + 1
|
||||
|
||||
def _init_optimizers_and_schedulers(self) -> None:
|
||||
tc = self.training_config
|
||||
student_lr = float(tc.optimizer.learning_rate)
|
||||
if student_lr <= 0.0:
|
||||
raise ValueError("training.learning_rate must be > 0 "
|
||||
"for dfsft")
|
||||
|
||||
student_betas = tc.optimizer.betas
|
||||
student_sched = str(tc.optimizer.lr_scheduler)
|
||||
student_params = [p for p in self.student.transformer.parameters() if p.requires_grad]
|
||||
(
|
||||
self._student_optimizer,
|
||||
self._student_lr_scheduler,
|
||||
) = build_optimizer_and_scheduler(
|
||||
params=student_params,
|
||||
optimizer_config=tc.optimizer,
|
||||
loop_config=tc.loop,
|
||||
learning_rate=student_lr,
|
||||
betas=student_betas,
|
||||
scheduler_name=student_sched,
|
||||
)
|
||||
|
||||
def _sample_t_inhom_indices(
|
||||
self,
|
||||
*,
|
||||
batch_size: int,
|
||||
num_latents: int,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
chunk_size = self._chunk_size
|
||||
num_chunks = ((num_latents + chunk_size - 1) // chunk_size)
|
||||
low, high = self._timestep_index_range
|
||||
chunk_indices = torch.randint(
|
||||
low=low,
|
||||
high=high,
|
||||
size=(batch_size, num_chunks),
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
generator=self.cuda_generator,
|
||||
)
|
||||
expanded = chunk_indices.repeat_interleave(chunk_size, dim=1)
|
||||
return expanded[:, :num_latents]
|
||||
@@ -0,0 +1,180 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Supervised finetuning method (algorithm layer)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from fastvideo.train.methods.base import TrainingMethod, LogScalar
|
||||
from fastvideo.train.models.base import ModelBase
|
||||
from fastvideo.train.utils.optimizer import (
|
||||
build_optimizer_and_scheduler, )
|
||||
|
||||
|
||||
class FineTuneMethod(TrainingMethod):
|
||||
"""Supervised finetuning: only ``student`` participates."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
cfg: Any,
|
||||
role_models: dict[str, ModelBase],
|
||||
) -> None:
|
||||
super().__init__(cfg=cfg, role_models=role_models)
|
||||
|
||||
if "student" not in role_models:
|
||||
raise ValueError("FineTuneMethod requires role 'student'")
|
||||
if not self.student._trainable:
|
||||
raise ValueError("FineTuneMethod requires student to be "
|
||||
"trainable")
|
||||
self._attn_kind: Literal["dense", "vsa"] = (self._infer_attn_kind())
|
||||
|
||||
# Initialize preprocessors on student.
|
||||
self.student.init_preprocessors(self.training_config)
|
||||
|
||||
self._init_optimizers_and_schedulers()
|
||||
|
||||
@property
|
||||
def _optimizer_dict(self) -> dict[str, Any]:
|
||||
return {"student": self._student_optimizer}
|
||||
|
||||
@property
|
||||
def _lr_scheduler_dict(self) -> dict[str, Any]:
|
||||
return {"student": self._student_lr_scheduler}
|
||||
|
||||
# TrainingMethod override: single_train_step
|
||||
def single_train_step(
|
||||
self,
|
||||
batch: dict[str, Any],
|
||||
iteration: int,
|
||||
) -> tuple[
|
||||
dict[str, torch.Tensor],
|
||||
dict[str, Any],
|
||||
dict[str, LogScalar],
|
||||
]:
|
||||
del iteration
|
||||
training_batch = self.student.prepare_batch(
|
||||
batch,
|
||||
generator=self.cuda_generator,
|
||||
latents_source="data",
|
||||
)
|
||||
|
||||
if training_batch.latents is None:
|
||||
raise RuntimeError("prepare_batch() must set "
|
||||
"TrainingBatch.latents")
|
||||
if training_batch.noisy_model_input is None:
|
||||
raise RuntimeError("prepare_batch() must set "
|
||||
"TrainingBatch.noisy_model_input")
|
||||
if training_batch.noise is None:
|
||||
raise RuntimeError("prepare_batch() must set "
|
||||
"TrainingBatch.noise")
|
||||
if training_batch.sigmas is None:
|
||||
raise RuntimeError("prepare_batch() must set "
|
||||
"TrainingBatch.sigmas")
|
||||
if training_batch.timesteps is None:
|
||||
raise RuntimeError("prepare_batch() must set "
|
||||
"TrainingBatch.timesteps")
|
||||
|
||||
clean_latents = training_batch.latents
|
||||
noisy_latents = (training_batch.noisy_model_input.permute(0, 2, 1, 3, 4))
|
||||
noise = training_batch.noise.permute(0, 2, 1, 3, 4)
|
||||
sigmas = training_batch.sigmas
|
||||
timesteps = training_batch.timesteps
|
||||
|
||||
pred = self.student.predict_noise(
|
||||
noisy_latents,
|
||||
timesteps,
|
||||
training_batch,
|
||||
conditional=True,
|
||||
attn_kind=self._attn_kind,
|
||||
)
|
||||
|
||||
if bool(self.training_config.model.precondition_outputs):
|
||||
pred_x0 = noisy_latents - pred * sigmas
|
||||
loss = F.mse_loss(pred_x0.float(), clean_latents.float())
|
||||
else:
|
||||
target = noise - clean_latents
|
||||
loss = F.mse_loss(pred.float(), target.float())
|
||||
|
||||
if self._attn_kind == "vsa":
|
||||
attn_metadata = training_batch.attn_metadata_vsa
|
||||
else:
|
||||
attn_metadata = training_batch.attn_metadata
|
||||
|
||||
loss_map = {
|
||||
"total_loss": loss,
|
||||
"finetune_loss": loss,
|
||||
}
|
||||
outputs: dict[str, Any] = {
|
||||
"_fv_backward": (
|
||||
training_batch.timesteps,
|
||||
attn_metadata,
|
||||
)
|
||||
}
|
||||
metrics: dict[str, LogScalar] = {}
|
||||
return loss_map, outputs, metrics
|
||||
|
||||
# TrainingMethod override: backward
|
||||
def backward(
|
||||
self,
|
||||
loss_map: dict[str, torch.Tensor],
|
||||
outputs: dict[str, Any],
|
||||
*,
|
||||
grad_accum_rounds: int = 1,
|
||||
) -> None:
|
||||
grad_accum_rounds = max(1, int(grad_accum_rounds))
|
||||
ctx = outputs.get("_fv_backward")
|
||||
if ctx is None:
|
||||
super().backward(
|
||||
loss_map,
|
||||
outputs,
|
||||
grad_accum_rounds=grad_accum_rounds,
|
||||
)
|
||||
return
|
||||
self.student.backward(
|
||||
loss_map["total_loss"],
|
||||
ctx,
|
||||
grad_accum_rounds=grad_accum_rounds,
|
||||
)
|
||||
|
||||
# TrainingMethod override: get_optimizers
|
||||
def get_optimizers(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> list[torch.optim.Optimizer]:
|
||||
del iteration
|
||||
return [self._student_optimizer]
|
||||
|
||||
# TrainingMethod override: get_lr_schedulers
|
||||
def get_lr_schedulers(
|
||||
self,
|
||||
iteration: int,
|
||||
) -> list[Any]:
|
||||
del iteration
|
||||
return [self._student_lr_scheduler]
|
||||
|
||||
def _init_optimizers_and_schedulers(self) -> None:
|
||||
tc = self.training_config
|
||||
|
||||
student_lr = float(tc.optimizer.learning_rate)
|
||||
if student_lr <= 0.0:
|
||||
raise ValueError("training.learning_rate must be > 0 "
|
||||
"for finetune")
|
||||
|
||||
student_betas = tc.optimizer.betas
|
||||
student_sched = str(tc.optimizer.lr_scheduler)
|
||||
student_params = [p for p in self.student.transformer.parameters() if p.requires_grad]
|
||||
(
|
||||
self._student_optimizer,
|
||||
self._student_lr_scheduler,
|
||||
) = build_optimizer_and_scheduler(
|
||||
params=student_params,
|
||||
optimizer_config=tc.optimizer,
|
||||
loop_config=tc.loop,
|
||||
learning_rate=student_lr,
|
||||
betas=student_betas,
|
||||
scheduler_name=student_sched,
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
__all__: list[str] = []
|
||||
@@ -0,0 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Model build plugins for Phase 2/2.9 distillation.
|
||||
|
||||
These are "model plugins" selected by ``recipe.family`` / ``roles.<role>.family``.
|
||||
"""
|
||||
@@ -0,0 +1,195 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Literal, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.models.utils import pred_noise_to_pred_video
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
TrainingConfig, )
|
||||
from fastvideo.pipelines import TrainingBatch
|
||||
|
||||
|
||||
class ModelBase(ABC):
|
||||
"""Per-role model instance.
|
||||
|
||||
Every role (student, teacher, critic, …) gets its own ``ModelBase``
|
||||
instance. Each instance owns its own ``transformer`` and
|
||||
``noise_scheduler``. Heavyweight resources (VAE, dataloader, RNG
|
||||
seeds) are loaded lazily via :meth:`init_preprocessors`, which the
|
||||
method calls **only on the student**.
|
||||
"""
|
||||
|
||||
transformer: torch.nn.Module
|
||||
noise_scheduler: Any
|
||||
_trainable: bool
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def init_preprocessors( # noqa: B027
|
||||
self,
|
||||
training_config: TrainingConfig,
|
||||
) -> None:
|
||||
"""Load VAE, build dataloader, seed RNGs.
|
||||
|
||||
Called only on the student by the method's ``__init__``.
|
||||
Default is a no-op so teacher/critic instances skip this.
|
||||
"""
|
||||
|
||||
def on_train_start(self) -> None: # noqa: B027
|
||||
"""Called once before the training loop begins."""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Timestep helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def num_train_timesteps(self) -> int:
|
||||
"""Return the scheduler's training timestep horizon."""
|
||||
return int(self.noise_scheduler.num_train_timesteps)
|
||||
|
||||
def shift_and_clamp_timestep(self, timestep: torch.Tensor) -> torch.Tensor:
|
||||
"""Apply model/pipeline timestep shifting and clamp."""
|
||||
return timestep
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Runtime primitives
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@abstractmethod
|
||||
def prepare_batch(
|
||||
self,
|
||||
raw_batch: dict[str, Any],
|
||||
*,
|
||||
generator: torch.Generator,
|
||||
latents_source: Literal["data", "zeros"] = "data",
|
||||
) -> TrainingBatch:
|
||||
"""Convert a dataloader batch into forward primitives."""
|
||||
|
||||
@abstractmethod
|
||||
def add_noise(
|
||||
self,
|
||||
clean_latents: torch.Tensor,
|
||||
noise: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Apply forward-process noise at *timestep*."""
|
||||
|
||||
@abstractmethod
|
||||
def predict_noise(
|
||||
self,
|
||||
noisy_latents: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
batch: TrainingBatch,
|
||||
*,
|
||||
conditional: bool,
|
||||
cfg_uncond: dict[str, Any] | None = None,
|
||||
attn_kind: Literal["dense", "vsa"] = "dense",
|
||||
) -> torch.Tensor:
|
||||
"""Predict noise/flow for the given noisy latents."""
|
||||
|
||||
def predict_x0(
|
||||
self,
|
||||
noisy_latents: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
batch: TrainingBatch,
|
||||
*,
|
||||
conditional: bool,
|
||||
cfg_uncond: dict[str, Any] | None = None,
|
||||
attn_kind: Literal["dense", "vsa"] = "dense",
|
||||
) -> torch.Tensor:
|
||||
"""Predict x0 via ``predict_noise`` + conversion."""
|
||||
pred_noise = self.predict_noise(
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=conditional,
|
||||
cfg_uncond=cfg_uncond,
|
||||
attn_kind=attn_kind,
|
||||
)
|
||||
return pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=noisy_latents.flatten(0, 1),
|
||||
timestep=timestep,
|
||||
scheduler=self.noise_scheduler,
|
||||
).unflatten(0, pred_noise.shape[:2])
|
||||
|
||||
@abstractmethod
|
||||
def backward(
|
||||
self,
|
||||
loss: torch.Tensor,
|
||||
ctx: Any,
|
||||
*,
|
||||
grad_accum_rounds: int,
|
||||
) -> None:
|
||||
"""Backward that may restore forward-context."""
|
||||
|
||||
|
||||
class CausalModelBase(ModelBase):
|
||||
"""Extension for causal / streaming model plugins.
|
||||
|
||||
Cache state is internal to the model instance and keyed by
|
||||
*cache_tag* (no role handle needed).
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def clear_caches(self, *, cache_tag: str = "pos") -> None:
|
||||
"""Clear internal caches before starting a new rollout."""
|
||||
|
||||
@abstractmethod
|
||||
def predict_noise_streaming(
|
||||
self,
|
||||
noisy_latents: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
batch: TrainingBatch,
|
||||
*,
|
||||
conditional: bool,
|
||||
cache_tag: str = "pos",
|
||||
store_kv: bool = False,
|
||||
cur_start_frame: int = 0,
|
||||
cfg_uncond: dict[str, Any] | None = None,
|
||||
attn_kind: Literal["dense", "vsa"] = "dense",
|
||||
) -> torch.Tensor | None:
|
||||
"""Streaming predict-noise that may update internal caches."""
|
||||
|
||||
def predict_x0_streaming(
|
||||
self,
|
||||
noisy_latents: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
batch: TrainingBatch,
|
||||
*,
|
||||
conditional: bool,
|
||||
cache_tag: str = "pos",
|
||||
store_kv: bool = False,
|
||||
cur_start_frame: int = 0,
|
||||
cfg_uncond: dict[str, Any] | None = None,
|
||||
attn_kind: Literal["dense", "vsa"] = "dense",
|
||||
) -> torch.Tensor | None:
|
||||
"""Predict x0 streaming via
|
||||
``predict_noise_streaming`` + conversion."""
|
||||
pred_noise = self.predict_noise_streaming(
|
||||
noisy_latents,
|
||||
timestep,
|
||||
batch,
|
||||
conditional=conditional,
|
||||
cache_tag=cache_tag,
|
||||
store_kv=store_kv,
|
||||
cur_start_frame=cur_start_frame,
|
||||
cfg_uncond=cfg_uncond,
|
||||
attn_kind=attn_kind,
|
||||
)
|
||||
if pred_noise is None:
|
||||
return None
|
||||
return pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=noisy_latents.flatten(0, 1),
|
||||
timestep=timestep,
|
||||
scheduler=self.noise_scheduler,
|
||||
).unflatten(0, pred_noise.shape[:2])
|
||||
@@ -0,0 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Wan model plugin package."""
|
||||
|
||||
from fastvideo.train.models.wan.wan import (
|
||||
WanModel as WanModel, )
|
||||
from fastvideo.train.models.wan.wan_causal import (
|
||||
WanCausalModel as WanCausalModel, )
|
||||
@@ -0,0 +1,671 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Wan model plugin (per-role instance)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import gc
|
||||
from typing import Any, Literal, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
import fastvideo.envs as envs
|
||||
from fastvideo.configs.sample import SamplingParam
|
||||
from fastvideo.distributed import (
|
||||
get_local_torch_device,
|
||||
get_sp_group,
|
||||
get_world_group,
|
||||
)
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.models.schedulers.scheduling_flow_match_euler_discrete import (
|
||||
FlowMatchEulerDiscreteScheduler, )
|
||||
from fastvideo.pipelines import TrainingBatch
|
||||
from fastvideo.pipelines.basic.wan.wan_pipeline import (
|
||||
WanPipeline, )
|
||||
from fastvideo.pipelines.pipeline_batch_info import (
|
||||
ForwardBatch, )
|
||||
from fastvideo.training.activation_checkpoint import (
|
||||
apply_activation_checkpointing, )
|
||||
from fastvideo.training.training_utils import (
|
||||
compute_density_for_timestep_sampling,
|
||||
get_sigmas,
|
||||
normalize_dit_input,
|
||||
shift_timestep,
|
||||
)
|
||||
from fastvideo.utils import (
|
||||
is_vmoba_available,
|
||||
is_vsa_available,
|
||||
)
|
||||
|
||||
from fastvideo.train.models.base import ModelBase
|
||||
from fastvideo.train.utils.module_state import (
|
||||
apply_trainable, )
|
||||
from fastvideo.train.utils.moduleloader import (
|
||||
load_module_from_path, )
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
TrainingConfig, )
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.video_sparse_attn import (
|
||||
VideoSparseAttentionMetadataBuilder, )
|
||||
from fastvideo.attention.backends.vmoba import (
|
||||
VideoMobaAttentionMetadataBuilder, )
|
||||
except Exception:
|
||||
VideoSparseAttentionMetadataBuilder = None # type: ignore[assignment]
|
||||
VideoMobaAttentionMetadataBuilder = None # type: ignore[assignment]
|
||||
|
||||
|
||||
class WanModel(ModelBase):
|
||||
"""Wan per-role model: owns transformer + noise_scheduler."""
|
||||
|
||||
_transformer_cls_name: str = "WanTransformer3DModel"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
init_from: str,
|
||||
training_config: TrainingConfig,
|
||||
trainable: bool = True,
|
||||
disable_custom_init_weights: bool = False,
|
||||
flow_shift: float = 3.0,
|
||||
enable_gradient_checkpointing_type: str
|
||||
| None = None,
|
||||
transformer_override_safetensor: str
|
||||
| None = None,
|
||||
) -> None:
|
||||
self._init_from = str(init_from)
|
||||
self._trainable = bool(trainable)
|
||||
|
||||
self.transformer = self._load_transformer(
|
||||
init_from=self._init_from,
|
||||
trainable=self._trainable,
|
||||
disable_custom_init_weights=(disable_custom_init_weights),
|
||||
enable_gradient_checkpointing_type=(enable_gradient_checkpointing_type),
|
||||
training_config=training_config,
|
||||
transformer_override_safetensor=(transformer_override_safetensor),
|
||||
)
|
||||
|
||||
self.noise_scheduler = (FlowMatchEulerDiscreteScheduler(shift=float(flow_shift)))
|
||||
|
||||
# Filled by init_preprocessors (student only).
|
||||
self.vae: Any = None
|
||||
self.training_config: TrainingConfig = training_config
|
||||
self.dataloader: Any = None
|
||||
self.validator: Any = None
|
||||
self.start_step: int = 0
|
||||
|
||||
self.world_group: Any = None
|
||||
self.sp_group: Any = None
|
||||
self.device: Any = get_local_torch_device()
|
||||
|
||||
self.negative_prompt_embeds: (torch.Tensor | None) = None
|
||||
self.negative_prompt_attention_mask: (torch.Tensor | None) = None
|
||||
|
||||
# Timestep mechanics.
|
||||
self.timestep_shift: float = float(flow_shift)
|
||||
self.num_train_timestep: int = int(self.noise_scheduler.num_train_timesteps)
|
||||
self.min_timestep: int = 0
|
||||
self.max_timestep: int = self.num_train_timestep
|
||||
|
||||
def _load_transformer(
|
||||
self,
|
||||
*,
|
||||
init_from: str,
|
||||
trainable: bool,
|
||||
disable_custom_init_weights: bool,
|
||||
enable_gradient_checkpointing_type: str | None,
|
||||
training_config: TrainingConfig,
|
||||
transformer_override_safetensor: str | None = None,
|
||||
) -> torch.nn.Module:
|
||||
transformer = load_module_from_path(
|
||||
model_path=init_from,
|
||||
module_type="transformer",
|
||||
training_config=training_config,
|
||||
disable_custom_init_weights=(disable_custom_init_weights),
|
||||
override_transformer_cls_name=(self._transformer_cls_name),
|
||||
transformer_override_safetensor=(transformer_override_safetensor),
|
||||
)
|
||||
transformer = apply_trainable(transformer, trainable=trainable)
|
||||
# Fall back to training_config.model if not set on the
|
||||
# model YAML section directly.
|
||||
ckpt_type = (enable_gradient_checkpointing_type or getattr(
|
||||
getattr(training_config, "model", None),
|
||||
"enable_gradient_checkpointing_type",
|
||||
None,
|
||||
))
|
||||
if trainable and ckpt_type:
|
||||
transformer = apply_activation_checkpointing(
|
||||
transformer,
|
||||
checkpointing_type=ckpt_type,
|
||||
)
|
||||
return transformer
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def init_preprocessors(self, training_config: TrainingConfig) -> None:
|
||||
self.vae = load_module_from_path(
|
||||
model_path=str(training_config.model_path),
|
||||
module_type="vae",
|
||||
training_config=training_config,
|
||||
)
|
||||
|
||||
self.world_group = get_world_group()
|
||||
self.sp_group = get_sp_group()
|
||||
|
||||
self._init_timestep_mechanics()
|
||||
|
||||
from fastvideo.dataset.dataloader.schema import (
|
||||
pyarrow_schema_t2v, )
|
||||
from fastvideo.train.utils.dataloader import (
|
||||
build_parquet_t2v_train_dataloader, )
|
||||
|
||||
text_len = (
|
||||
training_config.pipeline_config.text_encoder_configs[ # type: ignore[union-attr]
|
||||
0].arch_config.text_len)
|
||||
self.dataloader = build_parquet_t2v_train_dataloader(
|
||||
training_config.data,
|
||||
text_len=int(text_len),
|
||||
parquet_schema=pyarrow_schema_t2v,
|
||||
)
|
||||
self.start_step = 0
|
||||
|
||||
@property
|
||||
def num_train_timesteps(self) -> int:
|
||||
return int(self.num_train_timestep)
|
||||
|
||||
def shift_and_clamp_timestep(self, timestep: torch.Tensor) -> torch.Tensor:
|
||||
timestep = shift_timestep(
|
||||
timestep,
|
||||
self.timestep_shift,
|
||||
self.num_train_timestep,
|
||||
)
|
||||
return timestep.clamp(self.min_timestep, self.max_timestep)
|
||||
|
||||
def on_train_start(self) -> None:
|
||||
self.ensure_negative_conditioning()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Runtime primitives
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def prepare_batch(
|
||||
self,
|
||||
raw_batch: dict[str, Any],
|
||||
*,
|
||||
generator: torch.Generator,
|
||||
latents_source: Literal["data", "zeros"] = "data",
|
||||
) -> TrainingBatch:
|
||||
self.ensure_negative_conditioning()
|
||||
assert self.training_config is not None
|
||||
tc = self.training_config
|
||||
|
||||
dtype = self._get_training_dtype()
|
||||
device = self.device
|
||||
|
||||
training_batch = TrainingBatch()
|
||||
encoder_hidden_states = raw_batch["text_embedding"]
|
||||
encoder_attention_mask = raw_batch["text_attention_mask"]
|
||||
infos = raw_batch.get("info_list")
|
||||
|
||||
if latents_source == "zeros":
|
||||
batch_size = encoder_hidden_states.shape[0]
|
||||
vae_config = (
|
||||
tc.pipeline_config.vae_config.arch_config # type: ignore[union-attr]
|
||||
)
|
||||
num_channels = vae_config.z_dim
|
||||
spatial_compression_ratio = (vae_config.spatial_compression_ratio)
|
||||
latent_height = (tc.data.num_height // spatial_compression_ratio)
|
||||
latent_width = (tc.data.num_width // spatial_compression_ratio)
|
||||
latents = torch.zeros(
|
||||
batch_size,
|
||||
num_channels,
|
||||
tc.data.num_latent_t,
|
||||
latent_height,
|
||||
latent_width,
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
)
|
||||
elif latents_source == "data":
|
||||
if "vae_latent" not in raw_batch:
|
||||
raise ValueError("vae_latent not found in batch "
|
||||
"and latents_source='data'")
|
||||
latents = raw_batch["vae_latent"]
|
||||
latents = latents[:, :, :tc.data.num_latent_t]
|
||||
latents = latents.to(device, dtype=dtype)
|
||||
else:
|
||||
raise ValueError(f"Unknown latents_source: "
|
||||
f"{latents_source!r}")
|
||||
|
||||
training_batch.latents = latents
|
||||
training_batch.encoder_hidden_states = (encoder_hidden_states.to(device, dtype=dtype))
|
||||
training_batch.encoder_attention_mask = (encoder_attention_mask.to(device, dtype=dtype))
|
||||
training_batch.infos = infos
|
||||
|
||||
training_batch.latents = normalize_dit_input("wan", training_batch.latents, self.vae)
|
||||
training_batch = self._prepare_dit_inputs(training_batch, generator)
|
||||
training_batch = self._build_attention_metadata(training_batch)
|
||||
|
||||
training_batch.attn_metadata_vsa = copy.deepcopy(training_batch.attn_metadata)
|
||||
if training_batch.attn_metadata is not None:
|
||||
training_batch.attn_metadata.VSA_sparsity = 0.0 # type: ignore[attr-defined]
|
||||
|
||||
return training_batch
|
||||
|
||||
def add_noise(
|
||||
self,
|
||||
clean_latents: torch.Tensor,
|
||||
noise: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
b, t = clean_latents.shape[:2]
|
||||
noisy = self.noise_scheduler.add_noise(
|
||||
clean_latents.flatten(0, 1),
|
||||
noise.flatten(0, 1),
|
||||
timestep,
|
||||
).unflatten(0, (b, t))
|
||||
return noisy
|
||||
|
||||
def predict_noise(
|
||||
self,
|
||||
noisy_latents: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
batch: TrainingBatch,
|
||||
*,
|
||||
conditional: bool,
|
||||
cfg_uncond: dict[str, Any] | None = None,
|
||||
attn_kind: Literal["dense", "vsa"] = "dense",
|
||||
) -> torch.Tensor:
|
||||
device_type = self.device.type
|
||||
dtype = noisy_latents.dtype
|
||||
if conditional:
|
||||
text_dict = batch.conditional_dict
|
||||
if text_dict is None:
|
||||
raise RuntimeError("Missing conditional_dict in "
|
||||
"TrainingBatch")
|
||||
else:
|
||||
text_dict = self._get_uncond_text_dict(batch, cfg_uncond=cfg_uncond)
|
||||
|
||||
if attn_kind == "dense":
|
||||
attn_metadata = batch.attn_metadata
|
||||
elif attn_kind == "vsa":
|
||||
attn_metadata = batch.attn_metadata_vsa
|
||||
else:
|
||||
raise ValueError(f"Unknown attn_kind: {attn_kind!r}")
|
||||
|
||||
with torch.autocast(device_type, dtype=dtype), set_forward_context(
|
||||
current_timestep=batch.timesteps,
|
||||
attn_metadata=attn_metadata,
|
||||
):
|
||||
input_kwargs = (self._build_distill_input_kwargs(noisy_latents, timestep, text_dict))
|
||||
transformer = self._get_transformer(timestep)
|
||||
pred_noise = transformer(**input_kwargs).permute(0, 2, 1, 3, 4)
|
||||
return pred_noise
|
||||
|
||||
def backward(
|
||||
self,
|
||||
loss: torch.Tensor,
|
||||
ctx: Any,
|
||||
*,
|
||||
grad_accum_rounds: int,
|
||||
) -> None:
|
||||
timesteps, attn_metadata = ctx
|
||||
with set_forward_context(
|
||||
current_timestep=timesteps,
|
||||
attn_metadata=attn_metadata,
|
||||
):
|
||||
(loss / max(1, int(grad_accum_rounds))).backward()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_training_dtype(self) -> torch.dtype:
|
||||
return torch.bfloat16
|
||||
|
||||
def _init_timestep_mechanics(self) -> None:
|
||||
assert self.training_config is not None
|
||||
tc = self.training_config
|
||||
self.timestep_shift = float(tc.pipeline_config.flow_shift # type: ignore[union-attr]
|
||||
)
|
||||
self.num_train_timestep = int(self.noise_scheduler.num_train_timesteps)
|
||||
# min/max timestep ratios now come from method_config;
|
||||
# default to full range.
|
||||
self.min_timestep = 0
|
||||
self.max_timestep = self.num_train_timestep
|
||||
|
||||
def ensure_negative_conditioning(self) -> None:
|
||||
if self.negative_prompt_embeds is not None:
|
||||
return
|
||||
|
||||
assert self.training_config is not None
|
||||
tc = self.training_config
|
||||
world_group = self.world_group
|
||||
device = self.device
|
||||
dtype = self._get_training_dtype()
|
||||
|
||||
from fastvideo.train.utils.moduleloader import (
|
||||
make_inference_args, )
|
||||
|
||||
neg_embeds: torch.Tensor | None = None
|
||||
neg_mask: torch.Tensor | None = None
|
||||
|
||||
if world_group.rank_in_group == 0:
|
||||
sampling_param = SamplingParam.from_pretrained(tc.model_path)
|
||||
negative_prompt = sampling_param.negative_prompt
|
||||
|
||||
inference_args = make_inference_args(tc, model_path=tc.model_path)
|
||||
|
||||
prompt_pipeline = WanPipeline.from_pretrained(
|
||||
tc.model_path,
|
||||
args=inference_args,
|
||||
inference_mode=True,
|
||||
loaded_modules={"transformer": self.transformer},
|
||||
tp_size=tc.distributed.tp_size,
|
||||
sp_size=tc.distributed.sp_size,
|
||||
num_gpus=tc.distributed.num_gpus,
|
||||
pin_cpu_memory=(tc.distributed.pin_cpu_memory),
|
||||
dit_cpu_offload=True,
|
||||
)
|
||||
|
||||
batch_negative = ForwardBatch(
|
||||
data_type="video",
|
||||
prompt=negative_prompt,
|
||||
prompt_embeds=[],
|
||||
prompt_attention_mask=[],
|
||||
)
|
||||
result_batch = prompt_pipeline.prompt_encoding_stage( # type: ignore[attr-defined]
|
||||
batch_negative,
|
||||
inference_args,
|
||||
)
|
||||
|
||||
neg_embeds = result_batch.prompt_embeds[0].to(device=device, dtype=dtype)
|
||||
neg_mask = (result_batch.prompt_attention_mask[0].to(device=device, dtype=dtype))
|
||||
|
||||
del prompt_pipeline
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
meta = torch.zeros((2, ), device=device, dtype=torch.int64)
|
||||
if world_group.rank_in_group == 0:
|
||||
assert neg_embeds is not None
|
||||
assert neg_mask is not None
|
||||
meta[0] = neg_embeds.ndim
|
||||
meta[1] = neg_mask.ndim
|
||||
world_group.broadcast(meta, src=0)
|
||||
embed_ndim, mask_ndim = (
|
||||
int(meta[0].item()),
|
||||
int(meta[1].item()),
|
||||
)
|
||||
|
||||
max_ndim = 8
|
||||
embed_shape = torch.full((max_ndim, ), -1, device=device, dtype=torch.int64)
|
||||
mask_shape = torch.full((max_ndim, ), -1, device=device, dtype=torch.int64)
|
||||
if world_group.rank_in_group == 0:
|
||||
assert neg_embeds is not None
|
||||
assert neg_mask is not None
|
||||
embed_shape[:embed_ndim] = torch.tensor(
|
||||
list(neg_embeds.shape),
|
||||
device=device,
|
||||
dtype=torch.int64,
|
||||
)
|
||||
mask_shape[:mask_ndim] = torch.tensor(
|
||||
list(neg_mask.shape),
|
||||
device=device,
|
||||
dtype=torch.int64,
|
||||
)
|
||||
world_group.broadcast(embed_shape, src=0)
|
||||
world_group.broadcast(mask_shape, src=0)
|
||||
|
||||
embed_sizes = tuple(int(x) for x in embed_shape[:embed_ndim].tolist())
|
||||
mask_sizes = tuple(int(x) for x in mask_shape[:mask_ndim].tolist())
|
||||
|
||||
if world_group.rank_in_group != 0:
|
||||
neg_embeds = torch.empty(embed_sizes, device=device, dtype=dtype)
|
||||
neg_mask = torch.empty(mask_sizes, device=device, dtype=dtype)
|
||||
assert neg_embeds is not None
|
||||
assert neg_mask is not None
|
||||
|
||||
world_group.broadcast(neg_embeds, src=0)
|
||||
world_group.broadcast(neg_mask, src=0)
|
||||
|
||||
self.negative_prompt_embeds = neg_embeds
|
||||
self.negative_prompt_attention_mask = neg_mask
|
||||
|
||||
def _sample_timesteps(
|
||||
self,
|
||||
batch_size: int,
|
||||
device: torch.device,
|
||||
generator: torch.Generator,
|
||||
) -> torch.Tensor:
|
||||
assert self.training_config is not None
|
||||
tc = self.training_config
|
||||
|
||||
u = compute_density_for_timestep_sampling(
|
||||
weighting_scheme=tc.model.weighting_scheme,
|
||||
batch_size=batch_size,
|
||||
generator=generator,
|
||||
device=device,
|
||||
logit_mean=tc.model.logit_mean,
|
||||
logit_std=tc.model.logit_std,
|
||||
mode_scale=tc.model.mode_scale,
|
||||
)
|
||||
indices = (u * self.noise_scheduler.config.num_train_timesteps).long()
|
||||
return self.noise_scheduler.timesteps[indices.cpu()].to(device=device)
|
||||
|
||||
def _build_attention_metadata(self, training_batch: TrainingBatch) -> TrainingBatch:
|
||||
assert self.training_config is not None
|
||||
tc = self.training_config
|
||||
latents_shape = training_batch.raw_latent_shape
|
||||
patch_size = (
|
||||
tc.pipeline_config.dit_config.patch_size # type: ignore[union-attr]
|
||||
)
|
||||
assert latents_shape is not None
|
||||
assert training_batch.timesteps is not None
|
||||
|
||||
if (envs.FASTVIDEO_ATTENTION_BACKEND == "VIDEO_SPARSE_ATTN"):
|
||||
if (not is_vsa_available() or VideoSparseAttentionMetadataBuilder is None):
|
||||
raise ImportError("FASTVIDEO_ATTENTION_BACKEND is "
|
||||
"VIDEO_SPARSE_ATTN, but "
|
||||
"fastvideo_kernel is not correctly "
|
||||
"installed or detected.")
|
||||
training_batch.attn_metadata = VideoSparseAttentionMetadataBuilder().build( # type: ignore[misc]
|
||||
raw_latent_shape=latents_shape[2:5],
|
||||
current_timestep=(training_batch.timesteps),
|
||||
patch_size=patch_size,
|
||||
VSA_sparsity=tc.vsa_sparsity,
|
||||
device=self.device,
|
||||
)
|
||||
elif (envs.FASTVIDEO_ATTENTION_BACKEND == "VMOBA_ATTN"):
|
||||
if (not is_vmoba_available() or VideoMobaAttentionMetadataBuilder is None):
|
||||
raise ImportError("FASTVIDEO_ATTENTION_BACKEND is "
|
||||
"VMOBA_ATTN, but fastvideo_kernel "
|
||||
"(or flash_attn>=2.7.4) is not "
|
||||
"correctly installed.")
|
||||
moba_params = tc.model.moba_config.copy()
|
||||
moba_params.update({
|
||||
"current_timestep": (training_batch.timesteps),
|
||||
"raw_latent_shape": (training_batch.raw_latent_shape[2:5]),
|
||||
"patch_size": patch_size,
|
||||
"device": self.device,
|
||||
})
|
||||
training_batch.attn_metadata = VideoMobaAttentionMetadataBuilder().build(**
|
||||
moba_params) # type: ignore[misc]
|
||||
else:
|
||||
training_batch.attn_metadata = None
|
||||
|
||||
return training_batch
|
||||
|
||||
def _prepare_dit_inputs(
|
||||
self,
|
||||
training_batch: TrainingBatch,
|
||||
generator: torch.Generator,
|
||||
) -> TrainingBatch:
|
||||
assert self.training_config is not None
|
||||
tc = self.training_config
|
||||
latents = training_batch.latents
|
||||
assert isinstance(latents, torch.Tensor)
|
||||
batch_size = latents.shape[0]
|
||||
|
||||
noise = torch.randn(
|
||||
latents.shape,
|
||||
generator=generator,
|
||||
device=latents.device,
|
||||
dtype=latents.dtype,
|
||||
)
|
||||
timesteps = self._sample_timesteps(
|
||||
batch_size,
|
||||
latents.device,
|
||||
generator,
|
||||
)
|
||||
if int(tc.distributed.sp_size or 1) > 1:
|
||||
self.sp_group.broadcast(timesteps, src=0)
|
||||
|
||||
sigmas = get_sigmas(
|
||||
self.noise_scheduler,
|
||||
latents.device,
|
||||
timesteps,
|
||||
n_dim=latents.ndim,
|
||||
dtype=latents.dtype,
|
||||
)
|
||||
noisy_model_input = ((1.0 - sigmas) * latents + sigmas * noise)
|
||||
|
||||
training_batch.noisy_model_input = (noisy_model_input)
|
||||
training_batch.timesteps = timesteps
|
||||
training_batch.sigmas = sigmas
|
||||
training_batch.noise = noise
|
||||
training_batch.raw_latent_shape = latents.shape
|
||||
|
||||
training_batch.conditional_dict = {
|
||||
"encoder_hidden_states": (training_batch.encoder_hidden_states),
|
||||
"encoder_attention_mask": (training_batch.encoder_attention_mask),
|
||||
}
|
||||
|
||||
if (self.negative_prompt_embeds is not None and self.negative_prompt_attention_mask is not None):
|
||||
neg_embeds = self.negative_prompt_embeds
|
||||
neg_mask = (self.negative_prompt_attention_mask)
|
||||
if (neg_embeds.shape[0] == 1 and batch_size > 1):
|
||||
neg_embeds = neg_embeds.expand(batch_size, *neg_embeds.shape[1:]).contiguous()
|
||||
if (neg_mask.shape[0] == 1 and batch_size > 1):
|
||||
neg_mask = neg_mask.expand(batch_size, *neg_mask.shape[1:]).contiguous()
|
||||
training_batch.unconditional_dict = {
|
||||
"encoder_hidden_states": neg_embeds,
|
||||
"encoder_attention_mask": neg_mask,
|
||||
}
|
||||
|
||||
training_batch.latents = (training_batch.latents.permute(0, 2, 1, 3, 4))
|
||||
return training_batch
|
||||
|
||||
def _build_distill_input_kwargs(
|
||||
self,
|
||||
noise_input: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
text_dict: dict[str, torch.Tensor] | None,
|
||||
) -> dict[str, Any]:
|
||||
if text_dict is None:
|
||||
raise ValueError("text_dict cannot be None for "
|
||||
"Wan distillation")
|
||||
return {
|
||||
"hidden_states": noise_input.permute(0, 2, 1, 3, 4),
|
||||
"encoder_hidden_states": text_dict["encoder_hidden_states"],
|
||||
"encoder_attention_mask": text_dict["encoder_attention_mask"],
|
||||
"timestep": timestep,
|
||||
"return_dict": False,
|
||||
}
|
||||
|
||||
def _get_transformer(self, timestep: torch.Tensor) -> torch.nn.Module:
|
||||
return self.transformer
|
||||
|
||||
def _get_uncond_text_dict(
|
||||
self,
|
||||
batch: TrainingBatch,
|
||||
*,
|
||||
cfg_uncond: dict[str, Any] | None,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
if cfg_uncond is None:
|
||||
text_dict = getattr(batch, "unconditional_dict", None)
|
||||
if text_dict is None:
|
||||
raise RuntimeError("Missing unconditional_dict; "
|
||||
"ensure_negative_conditioning() "
|
||||
"may have failed")
|
||||
return text_dict
|
||||
|
||||
on_missing_raw = cfg_uncond.get("on_missing", "error")
|
||||
if not isinstance(on_missing_raw, str):
|
||||
raise ValueError("method_config.cfg_uncond.on_missing "
|
||||
"must be a string, got "
|
||||
f"{type(on_missing_raw).__name__}")
|
||||
on_missing = on_missing_raw.strip().lower()
|
||||
if on_missing not in {"error", "ignore"}:
|
||||
raise ValueError("method_config.cfg_uncond.on_missing "
|
||||
"must be one of {error, ignore}, got "
|
||||
f"{on_missing_raw!r}")
|
||||
|
||||
for channel, policy_raw in cfg_uncond.items():
|
||||
if channel in {"on_missing", "text"}:
|
||||
continue
|
||||
if policy_raw is None:
|
||||
continue
|
||||
if not isinstance(policy_raw, str):
|
||||
raise ValueError("method_config.cfg_uncond values "
|
||||
"must be strings, got "
|
||||
f"{channel}="
|
||||
f"{type(policy_raw).__name__}")
|
||||
policy = policy_raw.strip().lower()
|
||||
if policy == "keep":
|
||||
continue
|
||||
if on_missing == "ignore":
|
||||
continue
|
||||
raise ValueError("WanModel does not support "
|
||||
"cfg_uncond channel "
|
||||
f"{channel!r} (policy={policy!r}). "
|
||||
"Set cfg_uncond.on_missing=ignore or "
|
||||
"remove the channel.")
|
||||
|
||||
text_policy_raw = cfg_uncond.get("text", None)
|
||||
if text_policy_raw is None:
|
||||
text_policy = "negative_prompt"
|
||||
elif not isinstance(text_policy_raw, str):
|
||||
raise ValueError("method_config.cfg_uncond.text must be "
|
||||
"a string, got "
|
||||
f"{type(text_policy_raw).__name__}")
|
||||
else:
|
||||
text_policy = (text_policy_raw.strip().lower())
|
||||
|
||||
if text_policy in {"negative_prompt"}:
|
||||
text_dict = getattr(batch, "unconditional_dict", None)
|
||||
if text_dict is None:
|
||||
raise RuntimeError("Missing unconditional_dict; "
|
||||
"ensure_negative_conditioning() "
|
||||
"may have failed")
|
||||
return text_dict
|
||||
if text_policy == "keep":
|
||||
if batch.conditional_dict is None:
|
||||
raise RuntimeError("Missing conditional_dict in "
|
||||
"TrainingBatch")
|
||||
return batch.conditional_dict
|
||||
if text_policy == "zero":
|
||||
if batch.conditional_dict is None:
|
||||
raise RuntimeError("Missing conditional_dict in "
|
||||
"TrainingBatch")
|
||||
cond = batch.conditional_dict
|
||||
enc = cond["encoder_hidden_states"]
|
||||
mask = cond["encoder_attention_mask"]
|
||||
if not torch.is_tensor(enc) or not torch.is_tensor(mask):
|
||||
raise TypeError("conditional_dict must contain "
|
||||
"tensor text inputs")
|
||||
return {
|
||||
"encoder_hidden_states": (torch.zeros_like(enc)),
|
||||
"encoder_attention_mask": (torch.zeros_like(mask)),
|
||||
}
|
||||
if text_policy == "drop":
|
||||
raise ValueError("cfg_uncond.text=drop is not supported "
|
||||
"for Wan. Use "
|
||||
"{negative_prompt, keep, zero}.")
|
||||
raise ValueError("cfg_uncond.text must be one of "
|
||||
"{negative_prompt, keep, zero, drop}, got "
|
||||
f"{text_policy_raw!r}")
|
||||
@@ -0,0 +1,440 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Wan causal model plugin (per-role instance, streaming/cache)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
|
||||
from fastvideo.train.models.base import CausalModelBase
|
||||
from fastvideo.train.models.wan.wan import WanModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
TrainingConfig, )
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _StreamingCaches:
|
||||
kv_cache: list[dict[str, Any]]
|
||||
crossattn_cache: list[dict[str, Any]] | None
|
||||
frame_seq_length: int
|
||||
local_attn_size: int
|
||||
sliding_window_num_frames: int
|
||||
batch_size: int
|
||||
dtype: torch.dtype
|
||||
device: torch.device
|
||||
|
||||
|
||||
class WanCausalModel(WanModel, CausalModelBase):
|
||||
"""Wan per-role model with causal/streaming primitives."""
|
||||
|
||||
_transformer_cls_name: str = ("CausalWanTransformer3DModel")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
init_from: str,
|
||||
training_config: TrainingConfig,
|
||||
trainable: bool = True,
|
||||
disable_custom_init_weights: bool = False,
|
||||
flow_shift: float = 3.0,
|
||||
enable_gradient_checkpointing_type: str
|
||||
| None = None,
|
||||
transformer_override_safetensor: str
|
||||
| None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
init_from=init_from,
|
||||
training_config=training_config,
|
||||
trainable=trainable,
|
||||
disable_custom_init_weights=(disable_custom_init_weights),
|
||||
flow_shift=flow_shift,
|
||||
enable_gradient_checkpointing_type=(enable_gradient_checkpointing_type),
|
||||
transformer_override_safetensor=(transformer_override_safetensor),
|
||||
)
|
||||
self._streaming_caches: (dict[tuple[int, str], _StreamingCaches]) = {}
|
||||
|
||||
# --- CausalModelBase override: clear_caches ---
|
||||
def clear_caches(
|
||||
self,
|
||||
*,
|
||||
cache_tag: str = "pos",
|
||||
) -> None:
|
||||
self._streaming_caches.pop((id(self), str(cache_tag)), None)
|
||||
|
||||
# --- CausalModelBase override: predict_noise_streaming ---
|
||||
def predict_noise_streaming(
|
||||
self,
|
||||
noisy_latents: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
batch: Any,
|
||||
*,
|
||||
conditional: bool,
|
||||
cache_tag: str = "pos",
|
||||
store_kv: bool = False,
|
||||
cur_start_frame: int = 0,
|
||||
cfg_uncond: dict[str, Any] | None = None,
|
||||
attn_kind: Literal["dense", "vsa"] = "dense",
|
||||
) -> torch.Tensor | None:
|
||||
if attn_kind == "dense":
|
||||
attn_metadata = batch.attn_metadata
|
||||
elif attn_kind == "vsa":
|
||||
attn_metadata = batch.attn_metadata_vsa
|
||||
else:
|
||||
raise ValueError(f"Unknown attn_kind: {attn_kind!r}")
|
||||
|
||||
cache_tag = str(cache_tag)
|
||||
cur_start_frame = int(cur_start_frame)
|
||||
if cur_start_frame < 0:
|
||||
raise ValueError("cur_start_frame must be >= 0")
|
||||
|
||||
batch_size = int(noisy_latents.shape[0])
|
||||
num_frames = int(noisy_latents.shape[1])
|
||||
timestep_full = self._ensure_per_frame_timestep(
|
||||
timestep=timestep,
|
||||
batch_size=batch_size,
|
||||
num_frames=num_frames,
|
||||
device=noisy_latents.device,
|
||||
)
|
||||
|
||||
transformer = self._get_transformer(timestep_full)
|
||||
caches = self._get_or_init_streaming_caches(
|
||||
cache_tag=cache_tag,
|
||||
transformer=transformer,
|
||||
noisy_latents=noisy_latents,
|
||||
)
|
||||
|
||||
frame_seq_length = int(caches.frame_seq_length)
|
||||
kv_cache = caches.kv_cache
|
||||
crossattn_cache = caches.crossattn_cache
|
||||
|
||||
if (self._should_snapshot_streaming_cache() and torch.is_grad_enabled()):
|
||||
kv_cache = self._snapshot_kv_cache_indices(kv_cache)
|
||||
|
||||
model_kwargs: dict[str, Any] = {
|
||||
"kv_cache": kv_cache,
|
||||
"crossattn_cache": crossattn_cache,
|
||||
"current_start": (cur_start_frame * frame_seq_length),
|
||||
"start_frame": cur_start_frame,
|
||||
"is_cache": bool(store_kv),
|
||||
}
|
||||
|
||||
device_type = self.device.type
|
||||
dtype = noisy_latents.dtype
|
||||
|
||||
if conditional:
|
||||
text_dict = batch.conditional_dict
|
||||
if text_dict is None:
|
||||
raise RuntimeError("Missing conditional_dict in "
|
||||
"TrainingBatch")
|
||||
else:
|
||||
text_dict = self._get_uncond_text_dict(batch, cfg_uncond=cfg_uncond)
|
||||
|
||||
with (
|
||||
torch.autocast(device_type, dtype=dtype),
|
||||
set_forward_context(
|
||||
current_timestep=batch.timesteps,
|
||||
attn_metadata=attn_metadata,
|
||||
),
|
||||
):
|
||||
input_kwargs = (self._build_distill_input_kwargs(
|
||||
noisy_latents,
|
||||
timestep_full,
|
||||
text_dict,
|
||||
))
|
||||
input_kwargs["timestep"] = (timestep_full.to(
|
||||
device=self.device,
|
||||
dtype=torch.long,
|
||||
))
|
||||
input_kwargs.update(model_kwargs)
|
||||
|
||||
if store_kv:
|
||||
with torch.no_grad():
|
||||
_ = transformer(**input_kwargs)
|
||||
return None
|
||||
|
||||
pred_noise = transformer(**input_kwargs, ).permute(0, 2, 1, 3, 4)
|
||||
return pred_noise
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _ensure_per_frame_timestep(
|
||||
self,
|
||||
*,
|
||||
timestep: torch.Tensor,
|
||||
batch_size: int,
|
||||
num_frames: int,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
if timestep.ndim == 0:
|
||||
return (timestep.view(1, 1).expand(batch_size, num_frames).to(device=device))
|
||||
if timestep.ndim == 1:
|
||||
if int(timestep.shape[0]) == batch_size:
|
||||
return (timestep.view(batch_size, 1).expand(batch_size, num_frames).to(device=device))
|
||||
raise ValueError("streaming timestep must be scalar, "
|
||||
"[B], or [B, T]; got shape="
|
||||
f"{tuple(timestep.shape)}")
|
||||
if timestep.ndim == 2:
|
||||
return timestep.to(device=device)
|
||||
raise ValueError("streaming timestep must be scalar, "
|
||||
"[B], or [B, T]; got ndim="
|
||||
f"{int(timestep.ndim)}")
|
||||
|
||||
def _get_or_init_streaming_caches(
|
||||
self,
|
||||
*,
|
||||
cache_tag: str,
|
||||
transformer: torch.nn.Module,
|
||||
noisy_latents: torch.Tensor,
|
||||
) -> _StreamingCaches:
|
||||
key = (id(self), cache_tag)
|
||||
cached = self._streaming_caches.get(key)
|
||||
|
||||
batch_size = int(noisy_latents.shape[0])
|
||||
dtype = noisy_latents.dtype
|
||||
device = noisy_latents.device
|
||||
|
||||
frame_seq_length = (self._compute_frame_seq_length(transformer, noisy_latents))
|
||||
local_attn_size = self._get_local_attn_size(transformer)
|
||||
sliding_window_num_frames = (self._get_sliding_window_num_frames(transformer))
|
||||
|
||||
meta = (
|
||||
frame_seq_length,
|
||||
local_attn_size,
|
||||
sliding_window_num_frames,
|
||||
batch_size,
|
||||
dtype,
|
||||
device,
|
||||
)
|
||||
|
||||
if cached is not None:
|
||||
cached_meta = (
|
||||
cached.frame_seq_length,
|
||||
cached.local_attn_size,
|
||||
cached.sliding_window_num_frames,
|
||||
cached.batch_size,
|
||||
cached.dtype,
|
||||
cached.device,
|
||||
)
|
||||
if cached_meta == meta:
|
||||
return cached
|
||||
|
||||
kv_cache = self._initialize_kv_cache(
|
||||
transformer=transformer,
|
||||
batch_size=batch_size,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
frame_seq_length=frame_seq_length,
|
||||
local_attn_size=local_attn_size,
|
||||
sliding_window_num_frames=(sliding_window_num_frames),
|
||||
checkpoint_safe=(self._should_use_checkpoint_safe_kv_cache()),
|
||||
)
|
||||
crossattn_cache = (self._initialize_crossattn_cache(
|
||||
transformer=transformer,
|
||||
device=device,
|
||||
))
|
||||
|
||||
caches = _StreamingCaches(
|
||||
kv_cache=kv_cache,
|
||||
crossattn_cache=crossattn_cache,
|
||||
frame_seq_length=frame_seq_length,
|
||||
local_attn_size=local_attn_size,
|
||||
sliding_window_num_frames=(sliding_window_num_frames),
|
||||
batch_size=batch_size,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
)
|
||||
self._streaming_caches[key] = caches
|
||||
return caches
|
||||
|
||||
def _compute_frame_seq_length(
|
||||
self,
|
||||
transformer: torch.nn.Module,
|
||||
noisy_latents: torch.Tensor,
|
||||
) -> int:
|
||||
latent_seq_length = (int(noisy_latents.shape[-1]) * int(noisy_latents.shape[-2]))
|
||||
patch_size = getattr(transformer, "patch_size", None)
|
||||
if patch_size is None:
|
||||
patch_size = getattr(
|
||||
getattr(
|
||||
getattr(transformer, "config", None),
|
||||
"arch_config",
|
||||
None,
|
||||
),
|
||||
"patch_size",
|
||||
None,
|
||||
)
|
||||
if patch_size is None:
|
||||
raise ValueError("Unable to determine "
|
||||
"transformer.patch_size "
|
||||
"for causal streaming")
|
||||
patch_ratio = (int(patch_size[-1]) * int(patch_size[-2]))
|
||||
if patch_ratio <= 0:
|
||||
raise ValueError("Invalid patch_size for causal "
|
||||
"streaming")
|
||||
return latent_seq_length // patch_ratio
|
||||
|
||||
def _get_sliding_window_num_frames(
|
||||
self,
|
||||
transformer: torch.nn.Module,
|
||||
) -> int:
|
||||
cfg = getattr(transformer, "config", None)
|
||||
arch_cfg = getattr(cfg, "arch_config", None)
|
||||
value = (getattr(
|
||||
arch_cfg,
|
||||
"sliding_window_num_frames",
|
||||
None,
|
||||
) if arch_cfg is not None else None)
|
||||
if value is None:
|
||||
return 15
|
||||
return int(value)
|
||||
|
||||
def _get_local_attn_size(
|
||||
self,
|
||||
transformer: torch.nn.Module,
|
||||
) -> int:
|
||||
try:
|
||||
value = getattr(transformer, "local_attn_size", -1)
|
||||
except Exception:
|
||||
value = -1
|
||||
if value is None:
|
||||
return -1
|
||||
return int(value)
|
||||
|
||||
def _initialize_kv_cache(
|
||||
self,
|
||||
*,
|
||||
transformer: torch.nn.Module,
|
||||
batch_size: int,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
frame_seq_length: int,
|
||||
local_attn_size: int,
|
||||
sliding_window_num_frames: int,
|
||||
checkpoint_safe: bool,
|
||||
) -> list[dict[str, Any]]:
|
||||
num_blocks = len(getattr(transformer, "blocks", []))
|
||||
if num_blocks <= 0:
|
||||
raise ValueError("Unexpected transformer.blocks "
|
||||
"for causal streaming")
|
||||
|
||||
try:
|
||||
num_attention_heads = int(transformer.num_attention_heads) # type: ignore[attr-defined]
|
||||
except AttributeError as e:
|
||||
raise ValueError("Transformer is missing "
|
||||
"num_attention_heads") from e
|
||||
|
||||
try:
|
||||
attention_head_dim = int(transformer.attention_head_dim) # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
try:
|
||||
hidden_size = int(transformer.hidden_size) # type: ignore[attr-defined]
|
||||
except AttributeError as e:
|
||||
raise ValueError("Transformer is missing "
|
||||
"attention_head_dim and "
|
||||
"hidden_size") from e
|
||||
attention_head_dim = (hidden_size // max(1, num_attention_heads))
|
||||
|
||||
if local_attn_size != -1:
|
||||
kv_cache_size = (int(local_attn_size) * int(frame_seq_length))
|
||||
else:
|
||||
kv_cache_size = (int(frame_seq_length) * int(sliding_window_num_frames))
|
||||
|
||||
if checkpoint_safe:
|
||||
tc = getattr(self, "training_config", None)
|
||||
total_frames = int(tc.data.num_frames if tc is not None else 0)
|
||||
if total_frames <= 0:
|
||||
raise ValueError("training.num_frames must be set "
|
||||
"to enable checkpoint-safe "
|
||||
"streaming KV cache; got "
|
||||
f"{total_frames}")
|
||||
kv_cache_size = max(
|
||||
kv_cache_size,
|
||||
int(frame_seq_length) * total_frames,
|
||||
)
|
||||
|
||||
kv_cache: list[dict[str, Any]] = []
|
||||
for _ in range(num_blocks):
|
||||
kv_cache.append({
|
||||
"k":
|
||||
torch.zeros(
|
||||
[
|
||||
batch_size,
|
||||
kv_cache_size,
|
||||
num_attention_heads,
|
||||
attention_head_dim,
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
),
|
||||
"v":
|
||||
torch.zeros(
|
||||
[
|
||||
batch_size,
|
||||
kv_cache_size,
|
||||
num_attention_heads,
|
||||
attention_head_dim,
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
),
|
||||
"global_end_index":
|
||||
torch.zeros((), dtype=torch.long, device=device),
|
||||
"local_end_index":
|
||||
torch.zeros((), dtype=torch.long, device=device),
|
||||
})
|
||||
|
||||
return kv_cache
|
||||
|
||||
def _should_use_checkpoint_safe_kv_cache(self, ) -> bool:
|
||||
tc = getattr(self, "training_config", None)
|
||||
if tc is not None:
|
||||
checkpointing_type = (tc.model.enable_gradient_checkpointing_type)
|
||||
else:
|
||||
checkpointing_type = None
|
||||
return (bool(checkpointing_type) and bool(self._trainable))
|
||||
|
||||
def _should_snapshot_streaming_cache(self, ) -> bool:
|
||||
return (self._should_use_checkpoint_safe_kv_cache())
|
||||
|
||||
def _snapshot_kv_cache_indices(
|
||||
self,
|
||||
kv_cache: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
snapshot: list[dict[str, Any]] = []
|
||||
for block_cache in kv_cache:
|
||||
global_end_index = block_cache.get("global_end_index")
|
||||
local_end_index = block_cache.get("local_end_index")
|
||||
if (not isinstance(global_end_index, torch.Tensor) or not isinstance(local_end_index, torch.Tensor)):
|
||||
raise ValueError("Unexpected kv_cache index "
|
||||
"tensors; expected tensors at "
|
||||
"kv_cache[*].{global_end_index, "
|
||||
"local_end_index}")
|
||||
|
||||
copied = dict(block_cache)
|
||||
copied["global_end_index"] = (global_end_index.detach().clone())
|
||||
copied["local_end_index"] = (local_end_index.detach().clone())
|
||||
snapshot.append(copied)
|
||||
return snapshot
|
||||
|
||||
def _initialize_crossattn_cache(
|
||||
self,
|
||||
*,
|
||||
transformer: torch.nn.Module,
|
||||
device: torch.device,
|
||||
) -> list[dict[str, Any]] | None:
|
||||
num_blocks = len(getattr(transformer, "blocks", []))
|
||||
if num_blocks <= 0:
|
||||
return None
|
||||
return [{
|
||||
"is_init": False,
|
||||
"k": torch.empty(0, device=device),
|
||||
"v": torch.empty(0, device=device),
|
||||
} for _ in range(num_blocks)]
|
||||
@@ -0,0 +1,196 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
from fastvideo.distributed import get_sp_group, get_world_group
|
||||
from fastvideo.train.callbacks.callback import CallbackDict
|
||||
from fastvideo.train.methods.base import TrainingMethod
|
||||
from fastvideo.train.utils.tracking import build_tracker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
TrainingConfig, )
|
||||
|
||||
|
||||
def _coerce_log_scalar(value: Any, *, where: str) -> float:
|
||||
if isinstance(value, torch.Tensor):
|
||||
if value.numel() != 1:
|
||||
raise ValueError(f"Expected scalar tensor at {where}, "
|
||||
f"got shape={tuple(value.shape)}")
|
||||
return float(value.detach().item())
|
||||
if isinstance(value, float | int):
|
||||
return float(value)
|
||||
raise TypeError(f"Expected a scalar (float/int/Tensor) at "
|
||||
f"{where}, got {type(value).__name__}")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TrainLoopState:
|
||||
step: int
|
||||
accum_iter: int
|
||||
|
||||
|
||||
class Trainer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
training_config: TrainingConfig,
|
||||
*,
|
||||
config: dict[str, Any] | None = None,
|
||||
callback_configs: dict[str, dict[str, Any]]
|
||||
| None = None,
|
||||
) -> None:
|
||||
self.training_config = training_config
|
||||
self.world_group = get_world_group()
|
||||
self.sp_group = get_sp_group()
|
||||
self.global_rank = self.world_group.rank
|
||||
self.local_rank = self.world_group.local_rank
|
||||
self.tracker = build_tracker(
|
||||
training_config.tracker,
|
||||
training_config.checkpoint,
|
||||
config=config,
|
||||
)
|
||||
self.callbacks = CallbackDict(
|
||||
callback_configs or {},
|
||||
training_config,
|
||||
)
|
||||
|
||||
def _iter_dataloader(self, dataloader: Any) -> Iterator[dict[str, Any]]:
|
||||
data_iter = iter(dataloader)
|
||||
while True:
|
||||
batch = next(data_iter, None)
|
||||
if batch is None:
|
||||
data_iter = iter(dataloader)
|
||||
batch = next(data_iter)
|
||||
yield batch
|
||||
|
||||
def run(
|
||||
self,
|
||||
method: TrainingMethod,
|
||||
*,
|
||||
dataloader: Any,
|
||||
max_steps: int,
|
||||
start_step: int = 0,
|
||||
checkpoint_manager: Any | None = None,
|
||||
) -> None:
|
||||
tc = self.training_config
|
||||
grad_accum = max(
|
||||
1,
|
||||
int(tc.loop.gradient_accumulation_steps or 1),
|
||||
)
|
||||
|
||||
method.set_tracker(self.tracker)
|
||||
method.on_train_start()
|
||||
self.callbacks.on_train_start(
|
||||
method,
|
||||
iteration=start_step,
|
||||
)
|
||||
|
||||
resume_from_checkpoint = (tc.checkpoint.resume_from_checkpoint or "")
|
||||
if checkpoint_manager is not None:
|
||||
if resume_from_checkpoint:
|
||||
method.seed_optimizer_state_for_resume()
|
||||
resumed_step = (checkpoint_manager.maybe_resume(resume_from_checkpoint=(resume_from_checkpoint)))
|
||||
if resumed_step is not None:
|
||||
start_step = int(resumed_step)
|
||||
self.callbacks.on_validation_begin(
|
||||
method,
|
||||
iteration=start_step,
|
||||
)
|
||||
method.optimizers_zero_grad(start_step)
|
||||
|
||||
data_stream = self._iter_dataloader(dataloader)
|
||||
|
||||
# Restore the RNG snapshot LAST — after dcp.load,
|
||||
# after iter(dataloader), after everything that may
|
||||
# have advanced the RNG as a side-effect.
|
||||
if (checkpoint_manager is not None and resume_from_checkpoint):
|
||||
checkpoint_manager.load_rng_snapshot(resume_from_checkpoint, )
|
||||
progress = tqdm(
|
||||
range(start_step + 1, max_steps + 1),
|
||||
initial=start_step,
|
||||
desc="Steps",
|
||||
disable=self.local_rank > 0,
|
||||
)
|
||||
for step in progress:
|
||||
t0 = time.perf_counter()
|
||||
|
||||
loss_sums: dict[str, float] = {}
|
||||
metric_sums: dict[str, float] = {}
|
||||
for accum_iter in range(grad_accum):
|
||||
batch = next(data_stream)
|
||||
loss_map, outputs, step_metrics = method.single_train_step(
|
||||
batch,
|
||||
step,
|
||||
)
|
||||
|
||||
method.backward(
|
||||
loss_map,
|
||||
outputs,
|
||||
grad_accum_rounds=grad_accum,
|
||||
)
|
||||
|
||||
for k, v in loss_map.items():
|
||||
if isinstance(v, torch.Tensor):
|
||||
loss_sums[k] = loss_sums.get(k, 0.0) + float(v.detach().item())
|
||||
for k, v in step_metrics.items():
|
||||
if k in loss_sums:
|
||||
raise ValueError(f"Metric key {k!r} collides "
|
||||
"with loss key. Use a "
|
||||
"different name (e.g. prefix "
|
||||
"with 'train/').")
|
||||
metric_sums[k] = metric_sums.get(k, 0.0) + _coerce_log_scalar(
|
||||
v,
|
||||
where=("method.single_train_step()"
|
||||
f".metrics[{k!r}]"),
|
||||
)
|
||||
|
||||
self.callbacks.on_before_optimizer_step(
|
||||
method,
|
||||
iteration=step,
|
||||
)
|
||||
method.optimizers_schedulers_step(step)
|
||||
method.optimizers_zero_grad(step)
|
||||
|
||||
metrics = {k: v / grad_accum for k, v in loss_sums.items()}
|
||||
metrics.update({k: v / grad_accum for k, v in metric_sums.items()})
|
||||
metrics["step_time_sec"] = (time.perf_counter() - t0)
|
||||
metrics["vsa_sparsity"] = float(tc.vsa_sparsity)
|
||||
if self.global_rank == 0 and metrics:
|
||||
self.tracker.log(metrics, step)
|
||||
|
||||
self.callbacks.on_training_step_end(
|
||||
method,
|
||||
metrics,
|
||||
iteration=step,
|
||||
)
|
||||
|
||||
if checkpoint_manager is not None:
|
||||
checkpoint_manager.maybe_save(step)
|
||||
|
||||
self.callbacks.on_validation_begin(
|
||||
method,
|
||||
iteration=step,
|
||||
)
|
||||
self.callbacks.on_validation_end(
|
||||
method,
|
||||
iteration=step,
|
||||
)
|
||||
|
||||
self.callbacks.on_train_end(
|
||||
method,
|
||||
iteration=max_steps,
|
||||
)
|
||||
|
||||
if checkpoint_manager is not None:
|
||||
checkpoint_manager.save_final(max_steps)
|
||||
|
||||
self.tracker.finish()
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Distillation utilities shared across families/methods/entrypoints."""
|
||||
@@ -0,0 +1,56 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Assembly: build method + dataloader from a ``_target_``-based config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from fastvideo.train.utils.instantiate import (
|
||||
instantiate,
|
||||
resolve_target,
|
||||
)
|
||||
from fastvideo.train.utils.config import RunConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
TrainingConfig, )
|
||||
from fastvideo.train.methods.base import TrainingMethod
|
||||
|
||||
|
||||
def build_from_config(cfg: RunConfig, ) -> tuple[TrainingConfig, TrainingMethod, Any, int]:
|
||||
"""Build method + dataloader from a v3 run config.
|
||||
|
||||
1. Instantiate each model in ``cfg.models`` via ``_target_``.
|
||||
2. Resolve the method class from ``cfg.method["_target_"]``
|
||||
and construct it with ``(cfg=cfg, role_models=...)``.
|
||||
3. Return ``(training_args, method, dataloader, start_step)``.
|
||||
"""
|
||||
from fastvideo.train.models.base import ModelBase
|
||||
|
||||
# --- 1. Build role model instances ---
|
||||
role_models: dict[str, ModelBase] = {}
|
||||
for role, model_cfg in cfg.models.items():
|
||||
model = instantiate(model_cfg, training_config=cfg.training)
|
||||
if not isinstance(model, ModelBase):
|
||||
raise TypeError(f"models.{role}._target_ must resolve to a "
|
||||
f"ModelBase subclass, got {type(model).__name__}")
|
||||
role_models[role] = model
|
||||
|
||||
# --- 2. Build method ---
|
||||
method_cfg = dict(cfg.method)
|
||||
method_target = str(method_cfg.pop("_target_"))
|
||||
method_cls = resolve_target(method_target)
|
||||
|
||||
# The student model provides the dataloader.
|
||||
student = role_models.get("student")
|
||||
|
||||
method = method_cls(
|
||||
cfg=cfg,
|
||||
role_models=role_models,
|
||||
)
|
||||
|
||||
# --- 3. Gather dataloader and start_step ---
|
||||
dataloader = (getattr(student, "dataloader", None) if student is not None else None)
|
||||
start_step = int(getattr(student, "start_step", 0) if student is not None else 0)
|
||||
|
||||
return cfg.training, method, dataloader, start_step
|
||||
@@ -0,0 +1,380 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.distributed.checkpoint as dcp
|
||||
from fastvideo.logger import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_CHECKPOINT_DIR_RE = re.compile(r"^checkpoint-(\d+)$")
|
||||
|
||||
|
||||
def _is_stateful(obj: Any) -> bool:
|
||||
return callable(getattr(obj, "state_dict", None)) and callable(getattr(obj, "load_state_dict", None))
|
||||
|
||||
|
||||
def _rank() -> int:
|
||||
if dist.is_available() and dist.is_initialized():
|
||||
return int(dist.get_rank())
|
||||
return 0
|
||||
|
||||
|
||||
def _barrier() -> None:
|
||||
if dist.is_available() and dist.is_initialized():
|
||||
dist.barrier()
|
||||
|
||||
|
||||
def _parse_step_from_dir(checkpoint_dir: Path) -> int:
|
||||
match = _CHECKPOINT_DIR_RE.match(checkpoint_dir.name)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid checkpoint directory name {checkpoint_dir.name!r}; "
|
||||
"expected 'checkpoint-<step>'")
|
||||
return int(match.group(1))
|
||||
|
||||
|
||||
def _find_latest_checkpoint(output_dir: Path) -> Path | None:
|
||||
if not output_dir.exists():
|
||||
return None
|
||||
|
||||
candidates: list[tuple[int, Path]] = []
|
||||
for child in output_dir.iterdir():
|
||||
if not child.is_dir():
|
||||
continue
|
||||
if not _CHECKPOINT_DIR_RE.match(child.name):
|
||||
continue
|
||||
if not (child / "dcp").is_dir():
|
||||
continue
|
||||
try:
|
||||
step = _parse_step_from_dir(child)
|
||||
except Exception:
|
||||
continue
|
||||
candidates.append((step, child))
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda x: x[0])
|
||||
return candidates[-1][1]
|
||||
|
||||
|
||||
def _resolve_resume_checkpoint(resume_from_checkpoint: str, *, output_dir: str) -> Path | None:
|
||||
"""Resolve a user-provided resume path to a concrete checkpoint dir.
|
||||
|
||||
Accepted values:
|
||||
- "latest" (auto-pick latest checkpoint-*/dcp under output_dir,
|
||||
or ``None`` if no checkpoint exists yet — starts from scratch)
|
||||
- /path/to/output_dir/checkpoint-<step>
|
||||
- /path/to/output_dir/checkpoint-<step>/dcp
|
||||
- /path/to/output_dir (auto-pick latest checkpoint-*/dcp)
|
||||
"""
|
||||
|
||||
if str(resume_from_checkpoint).strip().lower() == "latest":
|
||||
out = Path(os.path.expanduser(str(output_dir))).resolve()
|
||||
latest = _find_latest_checkpoint(out)
|
||||
if latest is None:
|
||||
logger.info(
|
||||
"resume_from_checkpoint='latest' but no "
|
||||
"checkpoints found under %s; starting from "
|
||||
"scratch.",
|
||||
out,
|
||||
)
|
||||
return latest
|
||||
|
||||
raw = os.path.expanduser(str(resume_from_checkpoint))
|
||||
path = Path(raw).resolve()
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"resume_from_checkpoint not found: {path}")
|
||||
|
||||
if path.is_dir() and path.name == "dcp":
|
||||
path = path.parent
|
||||
|
||||
if path.is_dir() and _CHECKPOINT_DIR_RE.match(path.name):
|
||||
if not (path / "dcp").is_dir():
|
||||
raise FileNotFoundError(f"Missing dcp dir under checkpoint: {path / 'dcp'}")
|
||||
return path
|
||||
|
||||
# Treat as output_dir -> pick latest.
|
||||
latest = _find_latest_checkpoint(path)
|
||||
if latest is not None:
|
||||
return latest
|
||||
|
||||
# Give a clearer error message.
|
||||
out = Path(os.path.expanduser(str(output_dir))).resolve()
|
||||
raise ValueError("Could not resolve resume checkpoint. Expected a checkpoint directory "
|
||||
f"named 'checkpoint-<step>' (with 'dcp/' inside), or an output_dir "
|
||||
f"containing such checkpoints. Got: {path} (output_dir={out}).")
|
||||
|
||||
|
||||
class _RoleModuleContainer(torch.nn.Module):
|
||||
"""Ephemeral container to expose multiple role modules as a single
|
||||
``nn.Module``.
|
||||
|
||||
Used by ``OptimizerWrapper`` which expects a single root module
|
||||
covering all parameters owned by the optimizer.
|
||||
"""
|
||||
|
||||
def __init__(self, modules: dict[str, torch.nn.Module]) -> None:
|
||||
super().__init__()
|
||||
for name, module in modules.items():
|
||||
self.add_module(name, module)
|
||||
|
||||
|
||||
class _CallbackStateWrapper:
|
||||
"""Wraps a CallbackDict for DCP save/load."""
|
||||
|
||||
def __init__(self, callbacks: Any) -> None:
|
||||
self._callbacks = callbacks
|
||||
|
||||
def state_dict(self) -> dict[str, Any]:
|
||||
return self._callbacks.state_dict()
|
||||
|
||||
def load_state_dict(
|
||||
self,
|
||||
state_dict: dict[str, Any],
|
||||
) -> None:
|
||||
self._callbacks.load_state_dict(state_dict)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CheckpointConfig:
|
||||
save_steps: int
|
||||
keep_last: int
|
||||
|
||||
|
||||
class CheckpointManager:
|
||||
"""Role-based checkpoint manager for training runtime.
|
||||
|
||||
- Checkpoint policy lives in YAML (via TrainingArgs fields).
|
||||
- Resume path is typically provided via CLI (``--resume-from-checkpoint``).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
method: Any,
|
||||
dataloader: Any,
|
||||
output_dir: str,
|
||||
config: CheckpointConfig,
|
||||
callbacks: Any | None = None,
|
||||
raw_config: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
self.method = method
|
||||
self.dataloader = dataloader
|
||||
self.output_dir = str(output_dir)
|
||||
self.config = config
|
||||
self._callbacks = callbacks
|
||||
self._raw_config = raw_config
|
||||
self._last_saved_step: int | None = None
|
||||
|
||||
def _build_states(self) -> dict[str, Any]:
|
||||
states: dict[str, Any] = self.method.checkpoint_state()
|
||||
|
||||
# Dataloader (optional but recommended for exact resume).
|
||||
if _is_stateful(self.dataloader):
|
||||
states["dataloader"] = self.dataloader
|
||||
|
||||
# Callback state (e.g. EMA shadow weights, validation RNG).
|
||||
if self._callbacks is not None and _is_stateful(self._callbacks):
|
||||
states["callbacks"] = _CallbackStateWrapper(self._callbacks, )
|
||||
|
||||
return states
|
||||
|
||||
def _checkpoint_dir(self, step: int) -> Path:
|
||||
return Path(self.output_dir) / f"checkpoint-{step}"
|
||||
|
||||
def _dcp_dir(self, step: int) -> Path:
|
||||
return self._checkpoint_dir(step) / "dcp"
|
||||
|
||||
def maybe_save(self, step: int) -> None:
|
||||
save_steps = int(self.config.save_steps or 0)
|
||||
if save_steps <= 0:
|
||||
return
|
||||
if step % save_steps != 0:
|
||||
return
|
||||
if self._last_saved_step == step:
|
||||
return
|
||||
self.save(step)
|
||||
|
||||
def save_final(self, step: int) -> None:
|
||||
save_steps = int(self.config.save_steps or 0)
|
||||
if save_steps <= 0:
|
||||
return
|
||||
self.save(step)
|
||||
|
||||
def save(self, step: int) -> None:
|
||||
checkpoint_dir = self._checkpoint_dir(step)
|
||||
dcp_dir = self._dcp_dir(step)
|
||||
os.makedirs(dcp_dir, exist_ok=True)
|
||||
|
||||
states = self._build_states()
|
||||
if _rank() == 0:
|
||||
logger.info(
|
||||
"Saving checkpoint to %s",
|
||||
checkpoint_dir,
|
||||
)
|
||||
self._write_metadata(checkpoint_dir, step)
|
||||
dcp.save(states, checkpoint_id=str(dcp_dir))
|
||||
_barrier()
|
||||
|
||||
# Save RNG state AFTER dcp.save so it captures the
|
||||
# exact state the continuous run continues with.
|
||||
# dcp.save triggers FSDP all-gather ops that can
|
||||
# advance the RNG between when DCP captures it and
|
||||
# when the save completes.
|
||||
self._save_rng_snapshot(checkpoint_dir)
|
||||
_barrier()
|
||||
|
||||
self._last_saved_step = step
|
||||
|
||||
self._cleanup_old_checkpoints()
|
||||
|
||||
def _write_metadata(
|
||||
self,
|
||||
checkpoint_dir: Path,
|
||||
step: int,
|
||||
) -> None:
|
||||
metadata: dict[str, Any] = {"step": step}
|
||||
if self._raw_config is not None:
|
||||
metadata["config"] = self._raw_config
|
||||
meta_path = checkpoint_dir / "metadata.json"
|
||||
with open(meta_path, "w", encoding="utf-8") as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
@staticmethod
|
||||
def load_metadata(checkpoint_dir: str | Path, ) -> dict[str, Any]:
|
||||
"""Read ``metadata.json`` from a checkpoint dir."""
|
||||
meta_path = Path(checkpoint_dir) / "metadata.json"
|
||||
if not meta_path.is_file():
|
||||
raise FileNotFoundError(f"No metadata.json in {checkpoint_dir}")
|
||||
with open(meta_path, encoding="utf-8") as f:
|
||||
return json.load(f) # type: ignore[no-any-return]
|
||||
|
||||
def _save_rng_snapshot(self, checkpoint_dir: Path) -> None:
|
||||
"""Save per-rank RNG state to a separate file.
|
||||
|
||||
Called AFTER ``dcp.save`` so the snapshot reflects
|
||||
the exact state the continuous run continues with.
|
||||
Each rank saves its own file because CUDA RNG and
|
||||
custom generators differ across ranks.
|
||||
"""
|
||||
rank = _rank()
|
||||
rng: dict[str, Any] = {
|
||||
"torch_rng": torch.get_rng_state(),
|
||||
"python_rng": random.getstate(),
|
||||
"numpy_rng": np.random.get_state(),
|
||||
}
|
||||
rng["cuda_rng"] = torch.cuda.get_rng_state()
|
||||
rng["gen_cuda"] = self.method.cuda_generator.get_state()
|
||||
torch.save(
|
||||
rng,
|
||||
checkpoint_dir / f"rng_state_rank{rank}.pt",
|
||||
)
|
||||
|
||||
def load_rng_snapshot(
|
||||
self,
|
||||
checkpoint_path: str,
|
||||
) -> None:
|
||||
"""Restore per-rank RNG state from the snapshot file.
|
||||
|
||||
Must be called AFTER ``dcp.load`` **and** after
|
||||
``iter(dataloader)`` so no later operation can
|
||||
clobber the restored state.
|
||||
"""
|
||||
resolved = _resolve_resume_checkpoint(
|
||||
checkpoint_path,
|
||||
output_dir=self.output_dir,
|
||||
)
|
||||
if resolved is None:
|
||||
return
|
||||
rank = _rank()
|
||||
rng_path = resolved / f"rng_state_rank{rank}.pt"
|
||||
if not rng_path.is_file():
|
||||
# Fall back to legacy single-file snapshot.
|
||||
rng_path = resolved / "rng_state.pt"
|
||||
if not rng_path.is_file():
|
||||
logger.warning(
|
||||
"No rng_state in %s; skipping "
|
||||
"RNG snapshot restore.",
|
||||
resolved,
|
||||
)
|
||||
return
|
||||
|
||||
rng = torch.load(
|
||||
rng_path,
|
||||
map_location="cpu",
|
||||
weights_only=False,
|
||||
)
|
||||
if "torch_rng" in rng:
|
||||
torch.set_rng_state(rng["torch_rng"])
|
||||
if "python_rng" in rng:
|
||||
random.setstate(rng["python_rng"])
|
||||
if "numpy_rng" in rng:
|
||||
np.random.set_state(rng["numpy_rng"])
|
||||
|
||||
torch.cuda.set_rng_state(rng["cuda_rng"])
|
||||
self.method.cuda_generator.set_state(rng["gen_cuda"])
|
||||
logger.info(
|
||||
"Restored RNG snapshot from %s",
|
||||
rng_path,
|
||||
)
|
||||
|
||||
def maybe_resume(self, *, resume_from_checkpoint: str | None) -> int | None:
|
||||
if not resume_from_checkpoint:
|
||||
return None
|
||||
|
||||
resolved = _resolve_resume_checkpoint(
|
||||
resume_from_checkpoint,
|
||||
output_dir=self.output_dir,
|
||||
)
|
||||
if resolved is None:
|
||||
return None
|
||||
step = _parse_step_from_dir(resolved)
|
||||
|
||||
states = self._build_states()
|
||||
logger.info("Loading Phase 2 checkpoint from %s", resolved)
|
||||
dcp.load(states, checkpoint_id=str(resolved / "dcp"))
|
||||
_barrier()
|
||||
logger.info("Checkpoint loaded; resuming from step=%s", step)
|
||||
return step
|
||||
|
||||
def _cleanup_old_checkpoints(self) -> None:
|
||||
keep_last = int(self.config.keep_last or 0)
|
||||
if keep_last <= 0:
|
||||
return
|
||||
|
||||
if _rank() != 0:
|
||||
_barrier()
|
||||
return
|
||||
|
||||
output_dir = Path(self.output_dir)
|
||||
candidates: list[tuple[int, Path]] = []
|
||||
for child in output_dir.iterdir():
|
||||
if not child.is_dir():
|
||||
continue
|
||||
if not _CHECKPOINT_DIR_RE.match(child.name):
|
||||
continue
|
||||
try:
|
||||
step = _parse_step_from_dir(child)
|
||||
except Exception:
|
||||
continue
|
||||
candidates.append((step, child))
|
||||
|
||||
candidates.sort(key=lambda x: x[0])
|
||||
to_delete = candidates[:-keep_last] if len(candidates) > keep_last else []
|
||||
for step, path in to_delete:
|
||||
logger.info("Removing old checkpoint (keep_last=%s): %s", keep_last, path)
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
_barrier()
|
||||
@@ -0,0 +1,506 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Training run config (``_target_`` based YAML)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.train.utils.training_config import (
|
||||
CheckpointConfig,
|
||||
DataConfig,
|
||||
TrainingConfig,
|
||||
DistributedConfig,
|
||||
ModelTrainingConfig,
|
||||
OptimizerConfig,
|
||||
TrackerConfig,
|
||||
TrainingLoopConfig,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RunConfig:
|
||||
"""Parsed run config loaded from YAML."""
|
||||
|
||||
models: dict[str, dict[str, Any]]
|
||||
method: dict[str, Any]
|
||||
training: TrainingConfig
|
||||
callbacks: dict[str, dict[str, Any]]
|
||||
raw: dict[str, Any]
|
||||
|
||||
def resolved_config(self) -> dict[str, Any]:
|
||||
"""Return a fully-resolved config dict with defaults.
|
||||
|
||||
Suitable for logging to W&B so that every parameter
|
||||
(including defaults) is visible.
|
||||
"""
|
||||
import dataclasses
|
||||
|
||||
def _safe_asdict(obj: Any) -> Any:
|
||||
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
||||
return {
|
||||
f.name: _safe_asdict(getattr(obj, f.name))
|
||||
for f in dataclasses.fields(obj) if not callable(getattr(obj, f.name))
|
||||
}
|
||||
if isinstance(obj, dict):
|
||||
return {k: _safe_asdict(v) for k, v in obj.items()}
|
||||
if isinstance(obj, list | tuple):
|
||||
return type(obj)(_safe_asdict(v) for v in obj)
|
||||
return obj
|
||||
|
||||
resolved: dict[str, Any] = {}
|
||||
resolved["models"] = dict(self.models)
|
||||
resolved["method"] = dict(self.method)
|
||||
resolved["training"] = _safe_asdict(self.training)
|
||||
resolved["callbacks"] = dict(self.callbacks)
|
||||
return resolved
|
||||
|
||||
|
||||
# ---- parsing helpers (kept for use by methods) ----
|
||||
|
||||
|
||||
def _resolve_existing_file(path: str) -> str:
|
||||
if not path:
|
||||
return path
|
||||
expanded = os.path.expanduser(path)
|
||||
resolved = Path(expanded).resolve()
|
||||
if not resolved.exists():
|
||||
raise FileNotFoundError(f"Config file not found: {resolved}")
|
||||
if not resolved.is_file():
|
||||
raise ValueError(f"Expected a file path, got: {resolved}")
|
||||
return str(resolved)
|
||||
|
||||
|
||||
def _require_mapping(raw: Any, *, where: str) -> dict[str, Any]:
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError(f"Expected mapping at {where}, "
|
||||
f"got {type(raw).__name__}")
|
||||
return raw
|
||||
|
||||
|
||||
def _require_str(raw: Any, *, where: str) -> str:
|
||||
if not isinstance(raw, str) or not raw.strip():
|
||||
raise ValueError(f"Expected non-empty string at {where}")
|
||||
return raw
|
||||
|
||||
|
||||
def get_optional_int(mapping: dict[str, Any], key: str, *, where: str) -> int | None:
|
||||
raw = mapping.get(key)
|
||||
if raw is None:
|
||||
return None
|
||||
if isinstance(raw, bool):
|
||||
raise ValueError(f"Expected int at {where}, got bool")
|
||||
if isinstance(raw, int):
|
||||
return int(raw)
|
||||
if isinstance(raw, float) and raw.is_integer():
|
||||
return int(raw)
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return int(raw)
|
||||
raise ValueError(f"Expected int at {where}, "
|
||||
f"got {type(raw).__name__}")
|
||||
|
||||
|
||||
def get_optional_float(mapping: dict[str, Any], key: str, *, where: str) -> float | None:
|
||||
raw = mapping.get(key)
|
||||
if raw is None:
|
||||
return None
|
||||
if isinstance(raw, bool):
|
||||
raise ValueError(f"Expected float at {where}, got bool")
|
||||
if isinstance(raw, int | float):
|
||||
return float(raw)
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return float(raw)
|
||||
raise ValueError(f"Expected float at {where}, "
|
||||
f"got {type(raw).__name__}")
|
||||
|
||||
|
||||
def parse_betas(raw: Any, *, where: str) -> tuple[float, float]:
|
||||
if raw is None:
|
||||
raise ValueError(f"Missing betas for {where}")
|
||||
if isinstance(raw, tuple | list) and len(raw) == 2:
|
||||
return float(raw[0]), float(raw[1])
|
||||
if isinstance(raw, str):
|
||||
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"Expected betas as 'b1,b2' at {where}, "
|
||||
f"got {raw!r}")
|
||||
return float(parts[0]), float(parts[1])
|
||||
raise ValueError(f"Expected betas as 'b1,b2' at {where}, "
|
||||
f"got {type(raw).__name__}")
|
||||
|
||||
|
||||
# ---- config convenience helpers ----
|
||||
|
||||
|
||||
def require_positive_int(
|
||||
mapping: dict[str, Any],
|
||||
key: str,
|
||||
*,
|
||||
default: int | None = None,
|
||||
where: str | None = None,
|
||||
) -> int:
|
||||
"""Read an int that must be > 0."""
|
||||
loc = where or key
|
||||
raw = mapping.get(key)
|
||||
if raw is None:
|
||||
if default is not None:
|
||||
return default
|
||||
raise ValueError(f"Missing required key {loc!r}")
|
||||
val = get_optional_int(mapping, key, where=loc)
|
||||
if val is None or val <= 0:
|
||||
raise ValueError(f"{loc} must be a positive integer, got {raw!r}")
|
||||
return val
|
||||
|
||||
|
||||
def require_non_negative_int(
|
||||
mapping: dict[str, Any],
|
||||
key: str,
|
||||
*,
|
||||
default: int | None = None,
|
||||
where: str | None = None,
|
||||
) -> int:
|
||||
"""Read an int that must be >= 0."""
|
||||
loc = where or key
|
||||
raw = mapping.get(key)
|
||||
if raw is None:
|
||||
if default is not None:
|
||||
return default
|
||||
raise ValueError(f"Missing required key {loc!r}")
|
||||
val = get_optional_int(mapping, key, where=loc)
|
||||
if val is None or val < 0:
|
||||
raise ValueError(f"{loc} must be a non-negative integer, "
|
||||
f"got {raw!r}")
|
||||
return val
|
||||
|
||||
|
||||
def require_non_negative_float(
|
||||
mapping: dict[str, Any],
|
||||
key: str,
|
||||
*,
|
||||
default: float | None = None,
|
||||
where: str | None = None,
|
||||
) -> float:
|
||||
"""Read a float that must be >= 0."""
|
||||
loc = where or key
|
||||
raw = mapping.get(key)
|
||||
if raw is None:
|
||||
if default is not None:
|
||||
return default
|
||||
raise ValueError(f"Missing required key {loc!r}")
|
||||
val = get_optional_float(mapping, key, where=loc)
|
||||
if val is None or val < 0.0:
|
||||
raise ValueError(f"{loc} must be a non-negative float, "
|
||||
f"got {raw!r}")
|
||||
return val
|
||||
|
||||
|
||||
def require_choice(
|
||||
mapping: dict[str, Any],
|
||||
key: str,
|
||||
choices: set[str] | frozenset[str],
|
||||
*,
|
||||
default: str | None = None,
|
||||
where: str | None = None,
|
||||
) -> str:
|
||||
"""Read a string that must be one of *choices*."""
|
||||
loc = where or key
|
||||
raw = mapping.get(key)
|
||||
if raw is None:
|
||||
if default is not None:
|
||||
if default not in choices:
|
||||
raise ValueError(f"Default {default!r} not in {choices}")
|
||||
return default
|
||||
raise ValueError(f"Missing required key {loc!r}")
|
||||
if not isinstance(raw, str) or not raw.strip():
|
||||
raise ValueError(f"{loc} must be a non-empty string, "
|
||||
f"got {type(raw).__name__}")
|
||||
val = raw.strip().lower()
|
||||
if val not in choices:
|
||||
raise ValueError(f"{loc} must be one of {sorted(choices)}, "
|
||||
f"got {raw!r}")
|
||||
return val
|
||||
|
||||
|
||||
def require_bool(
|
||||
mapping: dict[str, Any],
|
||||
key: str,
|
||||
*,
|
||||
default: bool | None = None,
|
||||
where: str | None = None,
|
||||
) -> bool:
|
||||
"""Read a bool value."""
|
||||
loc = where or key
|
||||
raw = mapping.get(key)
|
||||
if raw is None:
|
||||
if default is not None:
|
||||
return default
|
||||
raise ValueError(f"Missing required key {loc!r}")
|
||||
if not isinstance(raw, bool):
|
||||
raise ValueError(f"{loc} must be a bool, "
|
||||
f"got {type(raw).__name__}")
|
||||
return raw
|
||||
|
||||
|
||||
def _parse_pipeline_config(
|
||||
cfg: dict[str, Any],
|
||||
*,
|
||||
models: dict[str, dict[str, Any]],
|
||||
) -> Any:
|
||||
"""Resolve PipelineConfig from the ``pipeline:`` YAML key."""
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
|
||||
pipeline_raw = cfg.get("pipeline")
|
||||
if pipeline_raw is None:
|
||||
return None
|
||||
|
||||
# Derive model_path from models.student.init_from —
|
||||
# needed by PipelineConfig.from_kwargs.
|
||||
model_path: str | None = None
|
||||
student_cfg = models.get("student")
|
||||
if student_cfg is not None:
|
||||
init_from = student_cfg.get("init_from")
|
||||
if init_from is not None:
|
||||
model_path = str(init_from)
|
||||
|
||||
kwargs: dict[str, Any] = {"pipeline_config": pipeline_raw}
|
||||
if model_path is not None:
|
||||
kwargs["model_path"] = model_path
|
||||
|
||||
if isinstance(pipeline_raw, str):
|
||||
kwargs["pipeline_config"] = _resolve_existing_file(pipeline_raw)
|
||||
|
||||
return PipelineConfig.from_kwargs(kwargs)
|
||||
|
||||
|
||||
def _build_training_config(
|
||||
t: dict[str, Any],
|
||||
*,
|
||||
models: dict[str, dict[str, Any]],
|
||||
pipeline_config: Any,
|
||||
) -> TrainingConfig:
|
||||
"""Build TrainingConfig from nested training: YAML."""
|
||||
d = dict(t.get("distributed", {}) or {})
|
||||
da = dict(t.get("data", {}) or {})
|
||||
o = dict(t.get("optimizer", {}) or {})
|
||||
lo = dict(t.get("loop", {}) or {})
|
||||
ck = dict(t.get("checkpoint", {}) or {})
|
||||
tr = dict(t.get("tracker", {}) or {})
|
||||
vs = dict(t.get("vsa", {}) or {})
|
||||
m = dict(t.get("model", {}) or {})
|
||||
|
||||
num_gpus = int(d.get("num_gpus", 1) or 1)
|
||||
|
||||
betas_raw = o.get("betas", "0.9,0.999")
|
||||
betas = parse_betas(betas_raw, where="training.optimizer.betas")
|
||||
|
||||
model_path = str(t.get("model_path", "") or "")
|
||||
if not model_path:
|
||||
student_cfg = models.get("student")
|
||||
if student_cfg is not None:
|
||||
init_from = student_cfg.get("init_from")
|
||||
if init_from is not None:
|
||||
model_path = str(init_from)
|
||||
|
||||
return TrainingConfig(
|
||||
distributed=DistributedConfig(
|
||||
num_gpus=num_gpus,
|
||||
tp_size=int(d.get("tp_size", 1) or 1),
|
||||
sp_size=int(d.get("sp_size", num_gpus) or num_gpus),
|
||||
hsdp_replicate_dim=int(d.get("hsdp_replicate_dim", 1) or 1),
|
||||
hsdp_shard_dim=int(d.get("hsdp_shard_dim", num_gpus) or num_gpus),
|
||||
pin_cpu_memory=bool(d.get("pin_cpu_memory", False)),
|
||||
),
|
||||
data=DataConfig(
|
||||
data_path=str(da.get("data_path", "") or ""),
|
||||
train_batch_size=int(da.get("train_batch_size", 1) or 1),
|
||||
dataloader_num_workers=int(da.get("dataloader_num_workers", 0) or 0),
|
||||
training_cfg_rate=float(da.get("training_cfg_rate", 0.0) or 0.0),
|
||||
seed=int(da.get("seed", 0) or 0),
|
||||
num_height=int(da.get("num_height", 0) or 0),
|
||||
num_width=int(da.get("num_width", 0) or 0),
|
||||
num_latent_t=int(da.get("num_latent_t", 0) or 0),
|
||||
num_frames=int(da.get("num_frames", 0) or 0),
|
||||
),
|
||||
optimizer=OptimizerConfig(
|
||||
learning_rate=float(o.get("learning_rate", 0.0) or 0.0),
|
||||
betas=betas,
|
||||
weight_decay=float(o.get("weight_decay", 0.0) or 0.0),
|
||||
lr_scheduler=str(o.get("lr_scheduler", "constant") or "constant"),
|
||||
lr_warmup_steps=int(o.get("lr_warmup_steps", 0) or 0),
|
||||
lr_num_cycles=int(o.get("lr_num_cycles", 0) or 0),
|
||||
lr_power=float(o.get("lr_power", 0.0) or 0.0),
|
||||
min_lr_ratio=float(o.get("min_lr_ratio", 0.5) or 0.5),
|
||||
),
|
||||
loop=TrainingLoopConfig(
|
||||
max_train_steps=int(lo.get("max_train_steps", 0) or 0),
|
||||
gradient_accumulation_steps=int(lo.get("gradient_accumulation_steps", 1) or 1),
|
||||
),
|
||||
checkpoint=CheckpointConfig(
|
||||
output_dir=str(ck.get("output_dir", "") or ""),
|
||||
resume_from_checkpoint=str(ck.get("resume_from_checkpoint", "") or ""),
|
||||
training_state_checkpointing_steps=int(ck.get("training_state_checkpointing_steps", 0) or 0),
|
||||
checkpoints_total_limit=int(ck.get("checkpoints_total_limit", 0) or 0),
|
||||
),
|
||||
tracker=TrackerConfig(
|
||||
trackers=list(tr.get("trackers", []) or []),
|
||||
project_name=str(tr.get("project_name", "fastvideo") or "fastvideo"),
|
||||
run_name=str(tr.get("run_name", "") or ""),
|
||||
),
|
||||
vsa_sparsity=float(vs.get("sparsity", 0.0) or 0.0),
|
||||
model=ModelTrainingConfig(
|
||||
weighting_scheme=str(m.get("weighting_scheme", "uniform") or "uniform"),
|
||||
logit_mean=float(m.get("logit_mean", 0.0) or 0.0),
|
||||
logit_std=float(m.get("logit_std", 1.0) or 1.0),
|
||||
mode_scale=float(m.get("mode_scale", 1.0) or 1.0),
|
||||
precondition_outputs=bool(m.get("precondition_outputs", False)),
|
||||
moba_config=dict(m.get("moba_config", {}) or {}),
|
||||
enable_gradient_checkpointing_type=(m.get("enable_gradient_checkpointing_type")),
|
||||
),
|
||||
pipeline_config=pipeline_config,
|
||||
model_path=model_path,
|
||||
dit_precision=str(t.get("dit_precision", "fp32") or "fp32"),
|
||||
)
|
||||
|
||||
|
||||
def _parse_cli_overrides(overrides: list[str], ) -> dict[str, Any]:
|
||||
"""Parse ``--dotted.key value`` CLI overrides.
|
||||
|
||||
Returns a flat dict mapping dotted keys to parsed
|
||||
values. Values are auto-cast: ``true``/``false`` to
|
||||
bool, integers/floats to numbers, and YAML list
|
||||
literals (``[a, b]``) to lists.
|
||||
"""
|
||||
result: dict[str, Any] = {}
|
||||
i = 0
|
||||
while i < len(overrides):
|
||||
arg = overrides[i]
|
||||
if not arg.startswith("--"):
|
||||
raise ValueError(f"Expected --dotted.key, got {arg!r}")
|
||||
key = arg[2:] # strip leading --
|
||||
if "=" in key:
|
||||
key, raw_val = key.split("=", 1)
|
||||
else:
|
||||
i += 1
|
||||
if i >= len(overrides):
|
||||
raise ValueError(f"Missing value for override {arg!r}")
|
||||
raw_val = overrides[i]
|
||||
result[key] = _cast_value(raw_val)
|
||||
i += 1
|
||||
return result
|
||||
|
||||
|
||||
def _cast_value(raw: str) -> Any:
|
||||
"""Best-effort cast a CLI string to a Python value."""
|
||||
if raw.lower() == "true":
|
||||
return True
|
||||
if raw.lower() == "false":
|
||||
return False
|
||||
if raw.lower() in ("none", "null"):
|
||||
return None
|
||||
# Try int
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
pass
|
||||
# Try float
|
||||
try:
|
||||
return float(raw)
|
||||
except ValueError:
|
||||
pass
|
||||
# Try YAML list literal like [1, 2]
|
||||
if raw.startswith("[") and raw.endswith("]"):
|
||||
try:
|
||||
return yaml.safe_load(raw)
|
||||
except yaml.YAMLError:
|
||||
pass
|
||||
return raw
|
||||
|
||||
|
||||
def _apply_overrides(
|
||||
cfg: dict[str, Any],
|
||||
overrides: dict[str, Any],
|
||||
) -> None:
|
||||
"""Apply dotted-key overrides to a nested dict."""
|
||||
for dotted_key, value in overrides.items():
|
||||
parts = dotted_key.split(".")
|
||||
d = cfg
|
||||
for part in parts[:-1]:
|
||||
if part not in d or not isinstance(d[part], dict):
|
||||
d[part] = {}
|
||||
d = d[part]
|
||||
d[parts[-1]] = value
|
||||
|
||||
|
||||
def load_run_config(
|
||||
path: str,
|
||||
overrides: list[str] | None = None,
|
||||
) -> RunConfig:
|
||||
"""Load a run config from YAML.
|
||||
|
||||
Expected top-level keys: ``models``, ``method``,
|
||||
``training`` (nested), and optionally ``callbacks``
|
||||
and ``pipeline``.
|
||||
|
||||
Args:
|
||||
path: Path to the YAML config file.
|
||||
overrides: Optional list of CLI override tokens,
|
||||
e.g. ``["--training.distributed.num_gpus", "4"]``.
|
||||
Dotted keys map to nested YAML paths.
|
||||
"""
|
||||
path = _resolve_existing_file(path)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
raw = yaml.safe_load(f)
|
||||
cfg = _require_mapping(raw, where=path)
|
||||
|
||||
# Apply CLI overrides before building typed config.
|
||||
if overrides:
|
||||
parsed = _parse_cli_overrides(overrides)
|
||||
_apply_overrides(cfg, parsed)
|
||||
logger.info("Applied CLI overrides: %s", parsed)
|
||||
|
||||
# --- models ---
|
||||
models_raw = _require_mapping(cfg.get("models"), where="models")
|
||||
models: dict[str, dict[str, Any]] = {}
|
||||
for role, model_cfg_raw in models_raw.items():
|
||||
role_str = _require_str(role, where="models.<role>")
|
||||
model_cfg = _require_mapping(model_cfg_raw, where=f"models.{role_str}")
|
||||
if "_target_" not in model_cfg:
|
||||
raise ValueError(f"models.{role_str} must have a "
|
||||
"'_target_' key")
|
||||
models[role_str] = dict(model_cfg)
|
||||
|
||||
# --- method ---
|
||||
method_raw = _require_mapping(cfg.get("method"), where="method")
|
||||
if "_target_" not in method_raw:
|
||||
raise ValueError("method must have a '_target_' key")
|
||||
method = dict(method_raw)
|
||||
|
||||
# --- callbacks ---
|
||||
callbacks_raw = cfg.get("callbacks", None)
|
||||
if callbacks_raw is None:
|
||||
callbacks: dict[str, dict[str, Any]] = {}
|
||||
else:
|
||||
callbacks = _require_mapping(callbacks_raw, where="callbacks")
|
||||
|
||||
# --- pipeline config ---
|
||||
pipeline_config = _parse_pipeline_config(cfg, models=models)
|
||||
|
||||
# --- training config ---
|
||||
training_raw = _require_mapping(cfg.get("training"), where="training")
|
||||
t = dict(training_raw)
|
||||
training = _build_training_config(t, models=models, pipeline_config=pipeline_config)
|
||||
|
||||
return RunConfig(
|
||||
models=models,
|
||||
method=method,
|
||||
training=training,
|
||||
callbacks=callbacks,
|
||||
raw=cfg,
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
DataConfig, )
|
||||
|
||||
|
||||
def build_parquet_t2v_train_dataloader(
|
||||
data_config: DataConfig,
|
||||
*,
|
||||
text_len: int,
|
||||
parquet_schema: Any,
|
||||
) -> Any:
|
||||
"""Build a parquet dataloader for T2V-style datasets."""
|
||||
|
||||
from fastvideo.dataset import (
|
||||
build_parquet_map_style_dataloader, )
|
||||
|
||||
_dataset, dataloader = (build_parquet_map_style_dataloader(
|
||||
data_config.data_path,
|
||||
data_config.train_batch_size,
|
||||
num_data_workers=(data_config.dataloader_num_workers),
|
||||
parquet_schema=parquet_schema,
|
||||
cfg_rate=data_config.training_cfg_rate,
|
||||
drop_last=True,
|
||||
text_padding_length=int(text_len),
|
||||
seed=int(data_config.seed or 0),
|
||||
))
|
||||
return dataloader
|
||||
@@ -0,0 +1,89 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""``_target_``-based instantiation utilities.
|
||||
|
||||
These helpers resolve a dotted Python path to a class and instantiate it,
|
||||
filtering constructor kwargs through ``inspect.signature`` so that only
|
||||
recognized parameters are forwarded. Unrecognized keys emit a warning
|
||||
rather than raising — this keeps YAML configs forward-compatible when
|
||||
a class drops a parameter in a later version.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import warnings
|
||||
from typing import Any
|
||||
|
||||
|
||||
def resolve_target(target: str) -> type:
|
||||
"""Import and return the class (or callable) at *target*.
|
||||
|
||||
*target* must be a fully-qualified dotted path, e.g.
|
||||
``"fastvideo.train.models.wan.wan.WanModel"``.
|
||||
"""
|
||||
if not isinstance(target, str) or not target.strip():
|
||||
raise ValueError(f"_target_ must be a non-empty dotted path string, "
|
||||
f"got {target!r}")
|
||||
target = target.strip()
|
||||
parts = target.rsplit(".", 1)
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"_target_ must contain at least one dot "
|
||||
f"(module.ClassName), got {target!r}")
|
||||
module_path, attr_name = parts
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
except ModuleNotFoundError as exc:
|
||||
raise ImportError(f"Cannot import module {module_path!r} "
|
||||
f"(from _target_={target!r})") from exc
|
||||
try:
|
||||
cls = getattr(module, attr_name)
|
||||
except AttributeError as exc:
|
||||
raise ImportError(f"Module {module_path!r} has no attribute "
|
||||
f"{attr_name!r} (from _target_={target!r})") from exc
|
||||
return cls
|
||||
|
||||
|
||||
def instantiate(cfg: dict[str, Any], **extra: Any) -> Any:
|
||||
"""Instantiate the class specified by ``cfg["_target_"]``.
|
||||
|
||||
All remaining keys in *cfg* (minus ``_target_``) plus any *extra*
|
||||
keyword arguments are forwarded to the constructor. Keys that do
|
||||
not match an ``__init__`` parameter are silently warned about and
|
||||
dropped, so callers can safely pass a superset.
|
||||
"""
|
||||
if not isinstance(cfg, dict):
|
||||
raise TypeError(f"instantiate() expects a dict with '_target_', "
|
||||
f"got {type(cfg).__name__}")
|
||||
target_str = cfg.get("_target_")
|
||||
if target_str is None:
|
||||
raise KeyError("Config dict is missing '_target_' key")
|
||||
|
||||
cls = resolve_target(str(target_str))
|
||||
kwargs: dict[str, Any] = {k: v for k, v in cfg.items() if k != "_target_"}
|
||||
kwargs.update(extra)
|
||||
|
||||
sig = inspect.signature(cls.__init__) # type: ignore[misc]
|
||||
params = sig.parameters
|
||||
has_var_keyword = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values())
|
||||
|
||||
if not has_var_keyword:
|
||||
valid_names = {
|
||||
name
|
||||
for name, p in params.items() if p.kind in (
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
inspect.Parameter.KEYWORD_ONLY,
|
||||
)
|
||||
}
|
||||
valid_names.discard("self")
|
||||
unrecognized = set(kwargs) - valid_names
|
||||
if unrecognized:
|
||||
warnings.warn(
|
||||
f"instantiate({target_str}): dropping unrecognized "
|
||||
f"kwargs {sorted(unrecognized)}",
|
||||
stacklevel=2,
|
||||
)
|
||||
for key in unrecognized:
|
||||
del kwargs[key]
|
||||
|
||||
return cls(**kwargs)
|
||||
@@ -0,0 +1,16 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def apply_trainable(module: torch.nn.Module, *, trainable: bool) -> torch.nn.Module:
|
||||
"""Apply train/eval mode + requires_grad based on a role's trainable flag."""
|
||||
|
||||
module.requires_grad_(bool(trainable))
|
||||
if trainable:
|
||||
module.train()
|
||||
else:
|
||||
module.eval()
|
||||
return module
|
||||
@@ -0,0 +1,148 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
from fastvideo.fastvideo_args import ExecutionMode, TrainingArgs
|
||||
from fastvideo.models.loader.component_loader import (
|
||||
PipelineComponentLoader, )
|
||||
from fastvideo.utils import (
|
||||
maybe_download_model,
|
||||
verify_model_config_and_directory,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
TrainingConfig, )
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# TrainingArgs builders (only place that creates FastVideoArgs)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_training_args(
|
||||
tc: TrainingConfig,
|
||||
*,
|
||||
model_path: str,
|
||||
) -> TrainingArgs:
|
||||
"""Build a TrainingArgs for PipelineComponentLoader."""
|
||||
pipeline_config = tc.pipeline_config or PipelineConfig()
|
||||
# Propagate dit_precision from TrainingConfig to PipelineConfig
|
||||
# so that TransformerLoader.load() picks up the correct
|
||||
# default_dtype (e.g. fp32 master weights for training).
|
||||
if tc.dit_precision and tc.dit_precision != pipeline_config.dit_precision:
|
||||
pipeline_config.dit_precision = tc.dit_precision
|
||||
return TrainingArgs(
|
||||
model_path=model_path,
|
||||
mode=ExecutionMode.DISTILLATION,
|
||||
inference_mode=False,
|
||||
pipeline_config=pipeline_config,
|
||||
num_gpus=tc.distributed.num_gpus,
|
||||
tp_size=tc.distributed.tp_size,
|
||||
sp_size=tc.distributed.sp_size,
|
||||
hsdp_replicate_dim=tc.distributed.hsdp_replicate_dim,
|
||||
hsdp_shard_dim=tc.distributed.hsdp_shard_dim,
|
||||
pin_cpu_memory=tc.distributed.pin_cpu_memory,
|
||||
dit_cpu_offload=False,
|
||||
dit_layerwise_offload=False,
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=False,
|
||||
image_encoder_cpu_offload=False,
|
||||
use_fsdp_inference=False,
|
||||
enable_torch_compile=False,
|
||||
)
|
||||
|
||||
|
||||
def make_inference_args(
|
||||
tc: TrainingConfig,
|
||||
*,
|
||||
model_path: str,
|
||||
) -> TrainingArgs:
|
||||
"""Build a TrainingArgs for inference (validation / pipelines)."""
|
||||
args = _make_training_args(tc, model_path=model_path)
|
||||
args.inference_mode = True
|
||||
args.mode = ExecutionMode.INFERENCE
|
||||
args.dit_cpu_offload = True
|
||||
args.VSA_sparsity = tc.vsa_sparsity
|
||||
return args
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Module loading
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_module_from_path(
|
||||
*,
|
||||
model_path: str,
|
||||
module_type: str,
|
||||
training_config: TrainingConfig,
|
||||
disable_custom_init_weights: bool = False,
|
||||
override_transformer_cls_name: str | None = None,
|
||||
transformer_override_safetensor: str | None = None,
|
||||
) -> torch.nn.Module:
|
||||
"""Load a single pipeline component module.
|
||||
|
||||
Accepts a ``TrainingConfig`` and internally builds the
|
||||
``TrainingArgs`` needed by ``PipelineComponentLoader``.
|
||||
"""
|
||||
fastvideo_args: Any = _make_training_args(training_config, model_path=model_path)
|
||||
|
||||
local_model_path = maybe_download_model(model_path)
|
||||
config = verify_model_config_and_directory(local_model_path)
|
||||
|
||||
if module_type not in config:
|
||||
raise ValueError(f"Module {module_type!r} not found in "
|
||||
f"config at {local_model_path}")
|
||||
|
||||
module_info = config[module_type]
|
||||
if module_info is None:
|
||||
raise ValueError(f"Module {module_type!r} has null value in "
|
||||
f"config at {local_model_path}")
|
||||
|
||||
transformers_or_diffusers, _architecture = module_info
|
||||
component_path = os.path.join(local_model_path, module_type)
|
||||
|
||||
old_override: str | None = None
|
||||
if override_transformer_cls_name is not None:
|
||||
old_override = getattr(
|
||||
fastvideo_args,
|
||||
"override_transformer_cls_name",
|
||||
None,
|
||||
)
|
||||
fastvideo_args.override_transformer_cls_name = str(override_transformer_cls_name)
|
||||
|
||||
if transformer_override_safetensor:
|
||||
fastvideo_args.init_weights_from_safetensors = str(transformer_override_safetensor)
|
||||
|
||||
if disable_custom_init_weights:
|
||||
fastvideo_args._loading_teacher_critic_model = True
|
||||
try:
|
||||
module = PipelineComponentLoader.load_module(
|
||||
module_name=module_type,
|
||||
component_model_path=component_path,
|
||||
transformers_or_diffusers=(transformers_or_diffusers),
|
||||
fastvideo_args=fastvideo_args,
|
||||
)
|
||||
finally:
|
||||
if disable_custom_init_weights and hasattr(fastvideo_args, "_loading_teacher_critic_model"):
|
||||
del fastvideo_args._loading_teacher_critic_model
|
||||
if override_transformer_cls_name is not None:
|
||||
if old_override is None:
|
||||
if hasattr(
|
||||
fastvideo_args,
|
||||
"override_transformer_cls_name",
|
||||
):
|
||||
fastvideo_args.override_transformer_cls_name = (None)
|
||||
else:
|
||||
fastvideo_args.override_transformer_cls_name = (old_override)
|
||||
|
||||
if not isinstance(module, torch.nn.Module):
|
||||
raise TypeError(f"Loaded {module_type!r} is not a "
|
||||
f"torch.nn.Module: {type(module)}")
|
||||
return module
|
||||
@@ -0,0 +1,72 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.training.training_utils import (
|
||||
clip_grad_norm_while_handling_failing_dtensor_cases,
|
||||
get_scheduler,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
OptimizerConfig,
|
||||
TrainingLoopConfig,
|
||||
)
|
||||
|
||||
|
||||
def build_optimizer_and_scheduler(
|
||||
*,
|
||||
params: list[torch.nn.Parameter],
|
||||
optimizer_config: OptimizerConfig,
|
||||
loop_config: TrainingLoopConfig,
|
||||
learning_rate: float,
|
||||
betas: tuple[float, float],
|
||||
scheduler_name: str,
|
||||
) -> tuple[torch.optim.Optimizer, object]:
|
||||
"""Build an AdamW optimizer and LR scheduler.
|
||||
|
||||
Returns ``(optimizer, lr_scheduler)`` so the caller can store them
|
||||
as method-level attributes.
|
||||
"""
|
||||
if not params:
|
||||
raise ValueError("No trainable parameters passed to "
|
||||
"build_optimizer_and_scheduler")
|
||||
|
||||
optimizer = torch.optim.AdamW(
|
||||
params,
|
||||
lr=float(learning_rate),
|
||||
betas=betas,
|
||||
weight_decay=float(optimizer_config.weight_decay),
|
||||
eps=1e-8,
|
||||
)
|
||||
|
||||
scheduler = get_scheduler(
|
||||
str(scheduler_name),
|
||||
optimizer=optimizer,
|
||||
num_warmup_steps=int(optimizer_config.lr_warmup_steps),
|
||||
num_training_steps=int(loop_config.max_train_steps),
|
||||
num_cycles=int(optimizer_config.lr_num_cycles),
|
||||
power=float(optimizer_config.lr_power),
|
||||
min_lr_ratio=float(optimizer_config.min_lr_ratio),
|
||||
last_epoch=-1,
|
||||
)
|
||||
|
||||
return optimizer, scheduler
|
||||
|
||||
|
||||
def clip_grad_norm_if_needed(
|
||||
module: torch.nn.Module,
|
||||
max_grad_norm: float,
|
||||
) -> float:
|
||||
if max_grad_norm <= 0.0:
|
||||
return 0.0
|
||||
grad_norm = (clip_grad_norm_while_handling_failing_dtensor_cases(
|
||||
[p for p in module.parameters()],
|
||||
max_grad_norm,
|
||||
foreach=None,
|
||||
))
|
||||
return (float(grad_norm.item()) if grad_norm is not None else 0.0)
|
||||
@@ -0,0 +1,51 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from fastvideo.distributed import get_world_group
|
||||
from fastvideo.training.trackers import (
|
||||
initialize_trackers,
|
||||
Trackers,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.train.utils.training_config import (
|
||||
CheckpointConfig,
|
||||
TrackerConfig,
|
||||
)
|
||||
|
||||
|
||||
def build_tracker(
|
||||
tracker_config: TrackerConfig,
|
||||
checkpoint_config: CheckpointConfig,
|
||||
*,
|
||||
config: dict[str, Any] | None,
|
||||
) -> Any:
|
||||
"""Build a tracker instance for a distillation run."""
|
||||
|
||||
world_group = get_world_group()
|
||||
|
||||
trackers = list(tracker_config.trackers)
|
||||
if not trackers and str(tracker_config.project_name):
|
||||
trackers.append(Trackers.WANDB.value)
|
||||
if world_group.rank != 0:
|
||||
trackers = []
|
||||
|
||||
tracker_log_dir = (checkpoint_config.output_dir or os.getcwd())
|
||||
if trackers:
|
||||
tracker_log_dir = os.path.join(tracker_log_dir, "tracker")
|
||||
|
||||
tracker_config_dict = config if trackers else None
|
||||
tracker_run_name = tracker_config.run_name or None
|
||||
project = (tracker_config.project_name or "fastvideo")
|
||||
|
||||
return initialize_trackers(
|
||||
trackers,
|
||||
experiment_name=project,
|
||||
config=tracker_config_dict,
|
||||
log_dir=tracker_log_dir,
|
||||
run_name=tracker_run_name,
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Typed training config — replaces TrainingArgs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DistributedConfig:
|
||||
num_gpus: int = 1
|
||||
tp_size: int = 1
|
||||
sp_size: int = 1
|
||||
hsdp_replicate_dim: int = 1
|
||||
hsdp_shard_dim: int = -1
|
||||
pin_cpu_memory: bool = False
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DataConfig:
|
||||
data_path: str = ""
|
||||
train_batch_size: int = 1
|
||||
dataloader_num_workers: int = 0
|
||||
training_cfg_rate: float = 0.0
|
||||
seed: int = 0
|
||||
num_height: int = 0
|
||||
num_width: int = 0
|
||||
num_latent_t: int = 0
|
||||
num_frames: int = 0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class OptimizerConfig:
|
||||
learning_rate: float = 0.0
|
||||
betas: tuple[float, float] = (0.9, 0.999)
|
||||
weight_decay: float = 0.0
|
||||
lr_scheduler: str = "constant"
|
||||
lr_warmup_steps: int = 0
|
||||
lr_num_cycles: int = 0
|
||||
lr_power: float = 0.0
|
||||
min_lr_ratio: float = 0.5
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TrainingLoopConfig:
|
||||
max_train_steps: int = 0
|
||||
gradient_accumulation_steps: int = 1
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CheckpointConfig:
|
||||
output_dir: str = ""
|
||||
resume_from_checkpoint: str = ""
|
||||
training_state_checkpointing_steps: int = 0
|
||||
checkpoints_total_limit: int = 0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TrackerConfig:
|
||||
trackers: list[str] = field(default_factory=list)
|
||||
project_name: str = "fastvideo"
|
||||
run_name: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ModelTrainingConfig:
|
||||
weighting_scheme: str = "uniform"
|
||||
logit_mean: float = 0.0
|
||||
logit_std: float = 1.0
|
||||
mode_scale: float = 1.0
|
||||
precondition_outputs: bool = False
|
||||
moba_config: dict = field(default_factory=dict)
|
||||
enable_gradient_checkpointing_type: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TrainingConfig:
|
||||
distributed: DistributedConfig = field(default_factory=DistributedConfig)
|
||||
data: DataConfig = field(default_factory=DataConfig)
|
||||
optimizer: OptimizerConfig = field(default_factory=OptimizerConfig)
|
||||
loop: TrainingLoopConfig = field(default_factory=TrainingLoopConfig)
|
||||
checkpoint: CheckpointConfig = field(default_factory=CheckpointConfig)
|
||||
tracker: TrackerConfig = field(default_factory=TrackerConfig)
|
||||
vsa_sparsity: float = 0.0
|
||||
model: ModelTrainingConfig = field(default_factory=ModelTrainingConfig)
|
||||
pipeline_config: PipelineConfig | None = None
|
||||
model_path: str = ""
|
||||
dit_precision: str = "fp32"
|
||||
@@ -89,6 +89,13 @@ class BaseTracker:
|
||||
if artifacts:
|
||||
self.log(artifacts, step)
|
||||
|
||||
def log_file(
|
||||
self,
|
||||
file_path: str,
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
"""Attach a file to the tracker run (e.g. config YAML)."""
|
||||
|
||||
def finish(self) -> None: # pragma: no cover - interface
|
||||
"""Finalize the tracker session."""
|
||||
|
||||
@@ -162,6 +169,16 @@ class WandbTracker(BaseTracker):
|
||||
|
||||
self.log(normalized_artifacts, step)
|
||||
|
||||
def log_file(
|
||||
self,
|
||||
file_path: str,
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
self._wandb.save(
|
||||
file_path,
|
||||
base_path=os.path.dirname(file_path),
|
||||
)
|
||||
|
||||
def finish(self) -> None:
|
||||
self._run.finish()
|
||||
|
||||
@@ -212,6 +229,14 @@ class SequentialTracker(BaseTracker):
|
||||
tracker.log_artifacts(artifacts, step)
|
||||
self._timed_metrics = {}
|
||||
|
||||
def log_file(
|
||||
self,
|
||||
file_path: str,
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
for tracker in self._trackers:
|
||||
tracker.log_file(file_path, name=name)
|
||||
|
||||
def finish(self) -> None:
|
||||
for tracker in self._trackers:
|
||||
tracker.finish()
|
||||
|
||||
@@ -65,7 +65,8 @@ def gather_state_dict_on_cpu_rank0(
|
||||
def compute_density_for_timestep_sampling(
|
||||
weighting_scheme: str,
|
||||
batch_size: int,
|
||||
generator,
|
||||
generator: torch.Generator,
|
||||
device: torch.device | str = "cpu",
|
||||
logit_mean: float | None = None,
|
||||
logit_std: float | None = None,
|
||||
mode_scale: float | None = None,
|
||||
@@ -83,15 +84,15 @@ def compute_density_for_timestep_sampling(
|
||||
mean=logit_mean,
|
||||
std=logit_std,
|
||||
size=(batch_size, ),
|
||||
device="cpu",
|
||||
device=device,
|
||||
generator=generator,
|
||||
)
|
||||
u = torch.nn.functional.sigmoid(u)
|
||||
elif weighting_scheme == "mode":
|
||||
u = torch.rand(size=(batch_size, ), device="cpu", generator=generator)
|
||||
u = torch.rand(size=(batch_size, ), device=device, generator=generator)
|
||||
u = 1 - u - mode_scale * (torch.cos(math.pi * u / 2)**2 - 1 + u)
|
||||
else:
|
||||
u = torch.rand(size=(batch_size, ), device="cpu", generator=generator)
|
||||
u = torch.rand(size=(batch_size, ), device=device, generator=generator)
|
||||
return u
|
||||
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ nav:
|
||||
- STA Mask Search (Archived): inference/examples/sta_mask_search.md
|
||||
- Training:
|
||||
- Overview: training/overview.md
|
||||
- Training Infrastructure: training/train_infra.md
|
||||
- Data Preprocessing: training/data_preprocess.md
|
||||
- Fine-tuning: training/finetune.md
|
||||
- Examples:
|
||||
@@ -159,6 +160,7 @@ nav:
|
||||
- Debugging: utilities/debugging.md
|
||||
- Design:
|
||||
- Overview: design/overview.md
|
||||
- Training Architecture: design/training_architecture.md
|
||||
- Developer Guide:
|
||||
- Overview: contributing/overview.md
|
||||
- Developer Environment:
|
||||
|
||||
Reference in New Issue
Block a user