Download 1.3B/code/trainer/distribution_matching.py from Elastic-forcing/Elastic-forcing-code: direct link, hf CLI and curl.
- Browser
- Download file 148 kB
-
https://huggingface.co/Elastic-forcing/Elastic-forcing-code/resolve/main/1.3B/code/trainer/distribution_matching.py
- Command line
-
hf download hf://Elastic-forcing/Elastic-forcing-code/1.3B/code/trainer/distribution_matching.py
-
curl -L -o distribution_matching.py https://huggingface.co/Elastic-forcing/Elastic-forcing-code/resolve/main/1.3B/code/trainer/distribution_matching.py
148 kB
| """Training orchestration for distribution matching (paper Section 3.3). | |
| Start at _run_distribution_matching_step: collect the distribution batch, | |
| form the feature-space objective, replay the differentiation batch, and update | |
| only the generator. The long initialization/resume code preserves the released | |
| asset and checkpoint contracts; it does not define a separate score model. | |
| """ | |
| import copy | |
| import gc | |
| import hashlib | |
| import logging | |
| import math | |
| from collections.abc import Mapping | |
| from utils.dataset import ShardingLMDBDataset, cycle | |
| from utils.dataset import ( | |
| OpenVidPromptVideoDataset, | |
| ODERegressionLMDBDataset, | |
| TextDataset, | |
| ) | |
| from utils.distributed import ( | |
| EMA_FSDP, | |
| fsdp_wrap, | |
| fsdp_state_dict, | |
| fsdp_optim_state_dict, | |
| fsdp_load_optim_state_dict, | |
| launch_distributed_job, | |
| ) | |
| from utils.misc import ( | |
| init_wandb_from_config, | |
| set_seed, | |
| merge_dict_list, | |
| ) | |
| from trainer.fresh_video_gradcache import FreshVideoGradCache | |
| from trainer.feature_gradient_replay import ( | |
| FeatureGradientReplay, | |
| ) | |
| import torch.distributed as dist | |
| from omegaconf import OmegaConf | |
| from model import DistributionMatchingModel | |
| import numpy as np | |
| import random | |
| import imageio.v3 as iio | |
| import torch | |
| from utils.optional_wandb import wandb | |
| import time | |
| import os | |
| import shutil | |
| def _canonicalize_experiment_identity_paths(value): | |
| """Make shared asset identities independent of a compute-node mount alias.""" | |
| local_root = "/data/shared/video-generation-assets" | |
| logical_root = "/assets/video-generation-assets" | |
| if isinstance(value, str): | |
| if value == local_root or value.startswith(local_root + "/"): | |
| return logical_root + value[len(local_root):] | |
| return value | |
| if isinstance(value, Mapping): | |
| return { | |
| key: _canonicalize_experiment_identity_paths(item) | |
| for key, item in value.items() | |
| } | |
| if isinstance(value, tuple): | |
| return tuple( | |
| _canonicalize_experiment_identity_paths(item) for item in value) | |
| if isinstance(value, list): | |
| return [ | |
| _canonicalize_experiment_identity_paths(item) for item in value] | |
| return value | |
| def _dense_video_metric_contract(feature_type): | |
| """Return the backend-owned prefix and universally required replay keys.""" | |
| prefixes = { | |
| "vjepa2_dense_latent": "vjepa2_dense", | |
| "dinov3_dense_latent": "dinov3_dense", | |
| "dual_dinov3_vjepa2_dense_latent": "dinov3_vjepa2_dense", | |
| "triple_dinov3_vjepa2_videomae_v2_dense_latent": "triple_dense", | |
| "triple_dinov3_vjepa2_videomae_v2_t5_joint_dense_latent": ( | |
| "triple_dense"), | |
| } | |
| try: | |
| prefix = prefixes[str(feature_type)] | |
| except KeyError as exc: | |
| raise ValueError( | |
| "fresh-video GradCache requires a supported dense feature type; " | |
| f"got {feature_type!r}" | |
| ) from exc | |
| return prefix, frozenset({ | |
| "combined_latent_grad_norm", | |
| f"{prefix}_feature_grad_norm", | |
| f"{prefix}_video_grad_norm", | |
| f"{prefix}_replay_max_abs", | |
| f"{prefix}_latent_replay_max_abs", | |
| }) | |
| def _validated_resume_data_counter_override(value, *, resume_checkpoint): | |
| """Validate an explicit lightweight-resume prompt-stream position. | |
| The counter is deliberately never derived from ``step`` or the current | |
| world size. Changing the rank topology changes how many per-rank batches | |
| correspond to one optimizer update, so callers must provide the desired | |
| absolute per-rank batch counter explicitly. | |
| """ | |
| if value is None: | |
| return None | |
| if not resume_checkpoint: | |
| raise ValueError( | |
| "resume_data_counter_override is only valid with " | |
| "resume_checkpoint") | |
| if type(value) is not int: | |
| raise TypeError( | |
| "resume_data_counter_override must be a non-negative integer") | |
| if value < 0: | |
| raise ValueError( | |
| "resume_data_counter_override must be a non-negative integer") | |
| return value | |
| def _validated_fresh_video_generator_grad_sample_scale( | |
| value, | |
| *, | |
| partial_gradient_enabled, | |
| ): | |
| """Validate the explicit suffix-replay gradient compensation.""" | |
| try: | |
| scale = float(value) | |
| except (TypeError, ValueError) as exc: | |
| raise ValueError( | |
| "fresh_video_generator_grad_sample_scale must be finite and " | |
| "strictly positive" | |
| ) from exc | |
| if not math.isfinite(scale) or scale <= 0.0: | |
| raise ValueError( | |
| "fresh_video_generator_grad_sample_scale must be finite and " | |
| "strictly positive") | |
| if scale != 1.0 and not bool(partial_gradient_enabled): | |
| raise ValueError( | |
| "fresh_video_generator_grad_sample_scale may differ from 1 only " | |
| "when a strict generator-gradient subsample is enabled") | |
| return scale | |
| def _generator_lr_for_completed_step(config, completed_step): | |
| """Return the LR used by the optimizer update producing ``completed_step``. | |
| A cosine window keeps ``config.lr`` through its inclusive start step, then | |
| decays to the configured minimum at the inclusive end step. Naming the | |
| public/checkpoint step explicitly avoids a resume off-by-one: checkpoint | |
| 75 next performs the update that publishes checkpoint/log step 76. | |
| """ | |
| if type(completed_step) is not int or completed_step < 0: | |
| raise ValueError("completed_step must be a non-negative integer") | |
| schedule = str(getattr(config, "generator_lr_schedule", "")).strip() | |
| if schedule != "cosine_window": | |
| raise ValueError( | |
| "generator_lr_schedule must be 'cosine_window'; got " | |
| f"{schedule!r}") | |
| base_lr = float(config.lr) | |
| min_lr = float(config.generator_lr_min) | |
| start_step = int(config.generator_lr_cosine_start_step) | |
| end_step = int(config.generator_lr_cosine_end_step) | |
| if ( | |
| not math.isfinite(base_lr) | |
| or not math.isfinite(min_lr) | |
| or base_lr <= 0.0 | |
| or min_lr <= 0.0 | |
| or min_lr > base_lr | |
| ): | |
| raise ValueError( | |
| "cosine-window generator learning rates must satisfy " | |
| "0 < generator_lr_min <= lr") | |
| if start_step < 0 or end_step <= start_step: | |
| raise ValueError( | |
| "generator LR cosine window requires " | |
| "0 <= start_step < end_step") | |
| if completed_step <= start_step: | |
| return base_lr | |
| if completed_step >= end_step: | |
| return min_lr | |
| progress = (completed_step - start_step) / (end_step - start_step) | |
| return min_lr + 0.5 * (base_lr - min_lr) * ( | |
| 1.0 + math.cos(math.pi * progress)) | |
| def _scale_replay_video_cotangents(cotangents, scale): | |
| """Scale replay gradients without changing the normalized MMD value. | |
| GradCache has already applied the configured MMD loss weight and the FSDP | |
| world-size compensation at this point. The explicit partial-gradient | |
| compensation is therefore applied exactly once, immediately before | |
| prepare/replay consumes these cotangents. | |
| """ | |
| values = tuple(cotangents) | |
| if float(scale) == 1.0: | |
| return values | |
| def scaled(cotangent): | |
| if isinstance(cotangent, Mapping): | |
| return { | |
| key: (value * float(scale)).contiguous() | |
| for key, value in cotangent.items() | |
| } | |
| return (cotangent * float(scale)).contiguous() | |
| return tuple(scaled(cotangent) for cotangent in values) | |
| def _atomic_torch_save(value, path): | |
| """Publish a checkpoint file only after ``torch.save`` fully succeeds.""" | |
| temporary_path = f"{path}.tmp" | |
| torch.save(value, temporary_path) | |
| os.replace(temporary_path, path) | |
| def _fresh_video_generator_grad_microbatches( | |
| *, | |
| local_samples, | |
| grad_local_samples, | |
| batch_size, | |
| ): | |
| """Return how many trailing microbatches build generator graphs. | |
| ``grad_local_samples=0`` is the compatibility path and replays every | |
| microbatch. A positive value keeps the complete forward feature batch for | |
| the MMD scalar but replays only the trailing whole microbatches. No | |
| inverse-probability rescaling is applied: earlier features are constants, | |
| exactly matching a partial derivative of the full MMD objective. | |
| """ | |
| values = { | |
| "local_samples": local_samples, | |
| "grad_local_samples": grad_local_samples, | |
| "batch_size": batch_size, | |
| } | |
| if any(type(value) is not int for value in values.values()): | |
| raise TypeError( | |
| "fresh-video generator-gradient geometry must use integers") | |
| if local_samples <= 0 or batch_size <= 0: | |
| raise ValueError("local_samples and batch_size must be positive") | |
| if local_samples % batch_size != 0: | |
| raise ValueError("local_samples must be divisible by batch_size") | |
| if grad_local_samples < 0 or grad_local_samples > local_samples: | |
| raise ValueError( | |
| "grad_local_samples must be zero or lie in [1, local_samples]") | |
| selected_samples = ( | |
| local_samples if grad_local_samples == 0 else grad_local_samples) | |
| if selected_samples % batch_size != 0: | |
| raise ValueError( | |
| "grad_local_samples must select complete physical microbatches") | |
| return selected_samples // batch_size | |
| def _transformer_block_types(wrapper, label): | |
| model = getattr(wrapper, "model", None) | |
| blocks = getattr(model, "blocks", None) | |
| if blocks is None or len(blocks) == 0: | |
| raise ValueError( | |
| f"{label} transformer FSDP strategy requires model.blocks") | |
| return {type(block) for block in blocks} | |
| def _strict_sha256(value, label): | |
| """Return a normalized SHA-256 string or reject ambiguous identity.""" | |
| normalized = str(value or "").strip().lower() | |
| if ( | |
| len(normalized) != 64 | |
| or any(character not in "0123456789abcdef" for character in normalized) | |
| ): | |
| raise ValueError(f"{label} must be a lowercase 64-character SHA-256") | |
| return normalized | |
| def _file_sha256(path): | |
| digest = hashlib.sha256() | |
| with open(path, "rb") as handle: | |
| for chunk in iter(lambda: handle.read(1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| class DistributionMatchingTrainer: | |
| def __init__(self, config): | |
| self.config = config | |
| self.step = 0 | |
| self.data_counter = 0 | |
| # Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.) | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| launch_distributed_job() | |
| global_rank = dist.get_rank() | |
| self.world_size = dist.get_world_size() | |
| self.uses_fresh_video_gradcache = ( | |
| str(getattr(config, "distribution_loss", "")) | |
| == "wan_kv_mmd" | |
| and str(getattr(config, "wan_mmd_estimator", "ema_rff")) | |
| == "nystrom_exact_repulsion" | |
| ) | |
| self.fresh_video_local_samples = int( | |
| getattr(config, "fresh_video_local_samples", config.batch_size)) | |
| if self.uses_fresh_video_gradcache: | |
| if self.fresh_video_local_samples <= 0: | |
| raise ValueError( | |
| "fresh_video_local_samples must be positive") | |
| if self.fresh_video_local_samples % int(config.batch_size) != 0: | |
| raise ValueError( | |
| "fresh_video_local_samples must be divisible by " | |
| f"batch_size; got {self.fresh_video_local_samples} and " | |
| f"{int(config.batch_size)}") | |
| self.fresh_video_microbatches = ( | |
| self.fresh_video_local_samples // int(config.batch_size)) | |
| else: | |
| self.fresh_video_microbatches = 1 | |
| expected_total_batch = ( | |
| int(config.batch_size) | |
| * self.world_size | |
| * self.fresh_video_microbatches | |
| ) | |
| configured_total_batch = int( | |
| getattr(config, "total_batch_size", expected_total_batch)) | |
| if configured_total_batch != expected_total_batch: | |
| raise ValueError( | |
| "total_batch_size must equal per-rank batch_size * world_size " | |
| "* fresh microbatches; " | |
| f"got {configured_total_batch} != {int(config.batch_size)} * " | |
| f"{self.world_size} * {self.fresh_video_microbatches} " | |
| f"({expected_total_batch})" | |
| ) | |
| self.fresh_video_total_samples = configured_total_batch | |
| configured_grad_local_samples = int(getattr( | |
| config, | |
| "fresh_video_generator_grad_local_samples", | |
| self.fresh_video_local_samples, | |
| )) | |
| self.fresh_video_generator_grad_microbatches = ( | |
| _fresh_video_generator_grad_microbatches( | |
| local_samples=self.fresh_video_local_samples, | |
| grad_local_samples=configured_grad_local_samples, | |
| batch_size=int(config.batch_size), | |
| ) | |
| if self.uses_fresh_video_gradcache | |
| else 1 | |
| ) | |
| self.fresh_video_generator_grad_local_samples = ( | |
| configured_grad_local_samples | |
| if self.uses_fresh_video_gradcache | |
| else int(config.batch_size) | |
| ) | |
| self.fresh_video_generator_grad_subsample_enabled = bool( | |
| self.uses_fresh_video_gradcache | |
| and self.fresh_video_generator_grad_local_samples | |
| < self.fresh_video_local_samples | |
| ) | |
| self.fresh_video_full_student_partial_gradient = bool(getattr( | |
| config, "fresh_video_full_student_partial_gradient", False)) | |
| if ( | |
| self.fresh_video_full_student_partial_gradient | |
| and not self.fresh_video_generator_grad_subsample_enabled | |
| ): | |
| raise ValueError( | |
| "fresh_video_full_student_partial_gradient requires a strict " | |
| "gradient suffix smaller than fresh_video_local_samples") | |
| self.fresh_video_generator_grad_global_samples = ( | |
| self.fresh_video_generator_grad_local_samples * self.world_size | |
| ) | |
| self.fresh_video_generator_grad_sample_scale = ( | |
| _validated_fresh_video_generator_grad_sample_scale( | |
| getattr( | |
| config, | |
| "fresh_video_generator_grad_sample_scale", | |
| 1.0, | |
| ), | |
| partial_gradient_enabled=( | |
| self.fresh_video_generator_grad_subsample_enabled), | |
| ) | |
| ) | |
| configured_teacher_samples = getattr( | |
| config, "expected_current_teacher_samples", None) | |
| if ( | |
| configured_teacher_samples is not None | |
| and int(configured_teacher_samples) | |
| != self.fresh_video_generator_grad_global_samples | |
| ): | |
| raise ValueError( | |
| "expected_current_teacher_samples must equal the global " | |
| "gradient suffix, got " | |
| f"{int(configured_teacher_samples)} != " | |
| f"{self.fresh_video_generator_grad_global_samples}") | |
| self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32 | |
| self.device = torch.cuda.current_device() | |
| self.is_main_process = global_rank == 0 | |
| self.causal = config.causal | |
| self.disable_wandb = config.disable_wandb | |
| # use a random seed for the training | |
| if config.seed == 0: | |
| random_seed = torch.randint(0, 10000000, (1,), device=self.device) | |
| dist.broadcast(random_seed, src=0) | |
| config.seed = random_seed.item() | |
| set_seed(config.seed + global_rank) | |
| if self.is_main_process and not self.disable_wandb: | |
| init_wandb_from_config(config) | |
| self.output_path = config.logdir | |
| # Step 2: Initialize the model and optimizer | |
| self.model = DistributionMatchingModel(config, device=self.device) | |
| self.uses_critic = bool(getattr(self.model, "uses_critic", True)) | |
| if ( | |
| bool(getattr( | |
| self.model, "uses_fresh_video_gradcache", False)) | |
| != self.uses_fresh_video_gradcache | |
| ): | |
| raise RuntimeError( | |
| "trainer/model fresh-video GradCache configuration mismatch") | |
| fresh_cache_class = ( | |
| FeatureGradientReplay | |
| if bool(getattr(self.model, "dual_video_mmd_enabled", False)) | |
| else FreshVideoGradCache | |
| ) | |
| self.fresh_video_gradcache = ( | |
| fresh_cache_class( | |
| self.model.latent_mmd, | |
| target_local_samples=self.fresh_video_local_samples, | |
| gradient_local_samples=( | |
| self.fresh_video_generator_grad_local_samples), | |
| require_sample_id_alignment=( | |
| str(getattr(config, "current_teacher_source", "")) | |
| .strip().lower() in { | |
| "matched_manifest_video", | |
| "matched_dual_feature_bank", | |
| "matched_triple_feature_bank", | |
| "matched_dinov3_vjepa2_feature_bank", | |
| }), | |
| full_student_partial_gradient=( | |
| self.fresh_video_full_student_partial_gradient), | |
| loss_scale=float(getattr( | |
| config, "wan_kv_loss_weight", 1.0)), | |
| ) | |
| if self.uses_fresh_video_gradcache | |
| else None | |
| ) | |
| # Save pretrained model state_dicts to CPU | |
| self.fake_score_state_dict_cpu = ( | |
| self.model.fake_score.state_dict() | |
| if getattr(self.model, "fake_score", None) is not None | |
| else None | |
| ) | |
| generator_wrap_strategy = config.generator_fsdp_wrap_strategy | |
| self.model.generator = fsdp_wrap( | |
| self.model.generator, | |
| sharding_strategy=config.sharding_strategy, | |
| mixed_precision=config.mixed_precision, | |
| wrap_strategy=generator_wrap_strategy, | |
| transformer_module=( | |
| _transformer_block_types(self.model.generator, "generator") | |
| if generator_wrap_strategy == "transformer" | |
| else None | |
| ), | |
| ) | |
| if getattr(self.model, "real_score", None) is not None: | |
| real_wrap_strategy = config.real_score_fsdp_wrap_strategy | |
| self.model.real_score = fsdp_wrap( | |
| self.model.real_score, | |
| sharding_strategy=config.sharding_strategy, | |
| mixed_precision=config.mixed_precision, | |
| wrap_strategy=real_wrap_strategy, | |
| transformer_module=( | |
| _transformer_block_types(self.model.real_score, "real_score") | |
| if real_wrap_strategy == "transformer" | |
| else None | |
| ), | |
| ) | |
| if getattr(self.model, "fake_score", None) is not None: | |
| self.model.fake_score = fsdp_wrap( | |
| self.model.fake_score, | |
| sharding_strategy=config.sharding_strategy, | |
| mixed_precision=config.mixed_precision, | |
| wrap_strategy=config.fake_score_fsdp_wrap_strategy | |
| ) | |
| self.model.text_encoder = fsdp_wrap( | |
| self.model.text_encoder, | |
| sharding_strategy=config.sharding_strategy, | |
| mixed_precision=config.mixed_precision, | |
| wrap_strategy=config.text_encoder_fsdp_wrap_strategy, | |
| cpu_offload=getattr(config, "text_encoder_cpu_offload", False) | |
| ) | |
| if ( | |
| getattr(self.model, "uses_vae_for_loss", False) | |
| or ( | |
| not config.no_visualize | |
| and getattr(self.model, "uses_vae_for_visualization", True) | |
| ) | |
| or bool(getattr(config, "load_raw_video", False)) | |
| or int(getattr(config, "eval_interval", 0)) > 0 | |
| ): | |
| self.model.vae = self.model.vae.to( | |
| device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32) | |
| self.generator_optimizer = torch.optim.AdamW( | |
| [param for param in self.model.generator.parameters() | |
| if param.requires_grad], | |
| lr=config.lr, | |
| betas=(config.beta1, config.beta2), | |
| weight_decay=config.weight_decay | |
| ) | |
| if self.uses_critic: | |
| self.critic_optimizer = torch.optim.AdamW( | |
| [param for param in self.model.fake_score.parameters() | |
| if param.requires_grad], | |
| lr=config.lr_critic if hasattr(config, "lr_critic") else config.lr, | |
| betas=(config.beta1_critic, config.beta2_critic), | |
| weight_decay=config.weight_decay | |
| ) | |
| else: | |
| self.critic_optimizer = None | |
| # Step 3: Initialize the dataloader | |
| current_teacher_enabled = bool(getattr( | |
| self.model, "current_teacher_attraction_enabled", False)) | |
| configured_teacher_source = getattr( | |
| config, "current_teacher_source", None) | |
| requested_teacher_source = str( | |
| configured_teacher_source | |
| if configured_teacher_source is not None | |
| else "auto" | |
| ).strip().lower() | |
| self.current_teacher_source = ( | |
| "lmdb_latent" if current_teacher_enabled else "none" | |
| ) if requested_teacher_source == "auto" else requested_teacher_source | |
| if self.current_teacher_source not in { | |
| "none", "lmdb_latent", "openvid_video", "matched_manifest_video", | |
| "matched_dual_feature_bank", "matched_triple_feature_bank", | |
| "matched_dinov3_vjepa2_feature_bank", | |
| }: | |
| raise ValueError( | |
| "current_teacher_source must be auto, none, lmdb_latent, " | |
| "openvid_video, matched_manifest_video, or " | |
| "matched_dual_feature_bank, matched_triple_feature_bank, or " | |
| "matched_dinov3_vjepa2_feature_bank; got " | |
| f"{self.current_teacher_source!r}") | |
| if current_teacher_enabled == (self.current_teacher_source == "none"): | |
| raise ValueError( | |
| "current_teacher_source must name exactly one source iff " | |
| "exact current-teacher attraction is enabled") | |
| self.current_teacher_dataset = None | |
| self.matched_counter_sampling_enabled = False | |
| self.experiment_identity = None | |
| if self.config.distribution_loss == "wan_kv_mmd": | |
| if self.current_teacher_source in { | |
| "matched_dual_feature_bank", | |
| "matched_triple_feature_bank", | |
| "matched_dinov3_vjepa2_feature_bank", | |
| }: | |
| source = self.current_teacher_source | |
| triple_bank = source == "matched_triple_feature_bank" | |
| dino_vjepa_bank = ( | |
| source == "matched_dinov3_vjepa2_feature_bank") | |
| if triple_bank: | |
| from utils.triple_video_feature_bank import ( | |
| TripleVideoFeatureBank as FeatureBank, | |
| ) | |
| manifest_field = "triple_teacher_bank_manifest" | |
| content_field = "triple_teacher_bank_content_hash" | |
| expected_branch_names = { | |
| "dinov3", "vjepa2", "videomae_v2"} | |
| expected_feature_type = ( | |
| "triple_dinov3_vjepa2_videomae_v2_dense_latent") | |
| elif dino_vjepa_bank: | |
| from utils.dinov3_vjepa2_feature_bank import ( | |
| DINOv3VJEPA2FeatureBank as FeatureBank, | |
| ) | |
| manifest_field = "dinov3_vjepa2_teacher_bank_manifest" | |
| content_field = ( | |
| "dinov3_vjepa2_teacher_bank_content_hash") | |
| expected_branch_names = {"dinov3", "vjepa2"} | |
| expected_feature_type = ( | |
| "dual_dinov3_vjepa2_dense_latent") | |
| else: | |
| from utils.dual_video_feature_bank import ( | |
| DualVideoFeatureBank as FeatureBank, | |
| ) | |
| manifest_field = "dual_teacher_bank_manifest" | |
| content_field = "dual_teacher_bank_content_hash" | |
| expected_branch_names = {"vjepa2", "videomae_v2"} | |
| expected_feature_type = ( | |
| "dual_vjepa2_videomae_v2_dense_latent") | |
| expected_feature_types = ( | |
| { | |
| "triple_dinov3_vjepa2_videomae_v2_dense_latent", | |
| "triple_dinov3_vjepa2_videomae_v2_t5_joint_dense_latent", | |
| } if triple_bank else {expected_feature_type} | |
| ) | |
| if not bool(getattr(self.model, "dual_video_mmd_enabled", False)): | |
| raise ValueError( | |
| "matched feature bank requires multi-representation mode") | |
| if str(getattr( | |
| self.model, "feature_type", "")) not in expected_feature_types: | |
| raise ValueError( | |
| "teacher bank branch set does not match model mode") | |
| if not self.uses_fresh_video_gradcache: | |
| raise ValueError("matched feature bank requires exact GradCache") | |
| if int(config.batch_size) != 1: | |
| raise ValueError( | |
| "matched feature-bank training requires batch_size=1") | |
| manifest_sha_field = f"{manifest_field}_sha256" | |
| kwargs = { | |
| "expected_manifest_sha256": _strict_sha256( | |
| getattr(config, manifest_sha_field), manifest_sha_field), | |
| "expected_content_hash": _strict_sha256( | |
| getattr(config, content_field), content_field), | |
| "expected_source_manifest_sha256": _strict_sha256( | |
| config.matched_manifest_sha256, | |
| "matched_manifest_sha256"), | |
| "expected_sample_count": int(getattr( | |
| config, "matched_manifest_samples", 5000)), | |
| "expected_vjepa_model_sha256": _strict_sha256( | |
| config.vjepa2_model_sha256, | |
| "vjepa2_model_sha256"), | |
| "validate_all_shards": True, | |
| } | |
| if triple_bank or dino_vjepa_bank: | |
| kwargs.update({ | |
| "expected_source_csv_sha256": _strict_sha256( | |
| config.matched_source_csv_sha256, | |
| "matched_source_csv_sha256"), | |
| "expected_sample_identity_sha256": _strict_sha256( | |
| config.matched_sample_identity_sha256, | |
| "matched_sample_identity_sha256"), | |
| }) | |
| if not dino_vjepa_bank: | |
| kwargs.update({ | |
| "expected_videomae_revision": str( | |
| config.videomae_v2_model_revision), | |
| "expected_videomae_download_manifest_content_hash": ( | |
| _strict_sha256( | |
| config.videomae_v2_download_manifest_content_hash, | |
| "videomae_v2_download_manifest_content_hash")), | |
| }) | |
| if triple_bank or dino_vjepa_bank: | |
| kwargs.update({ | |
| "expected_dinov3_checkpoint_sha256": _strict_sha256( | |
| config.dinov3_checkpoint_sha256, | |
| "dinov3_checkpoint_sha256"), | |
| "expected_dinov3_source_commit": str( | |
| config.dinov3_source_commit), | |
| "expected_shard_size": 32, | |
| }) | |
| dataset = FeatureBank( | |
| str(getattr(config, manifest_field)), **kwargs) | |
| model_bank_hash = str(getattr( | |
| self.model, | |
| "triple_teacher_bank_content_hash" | |
| if triple_bank else "dual_teacher_bank_content_hash")) | |
| if dataset.content_hash != model_bank_hash: | |
| raise ValueError( | |
| "training bank identity differs from all references") | |
| self.current_teacher_dataset = dataset | |
| self.matched_counter_sampling_enabled = True | |
| branches = copy.deepcopy(self.model.dual_branch_registry) | |
| if set(branches) != expected_branch_names: | |
| raise ValueError( | |
| "model branch registry does not match teacher bank") | |
| self.experiment_identity = { | |
| "current_teacher_source": self.current_teacher_source, | |
| f"{manifest_field}_sha256": dataset.manifest_sha256, | |
| content_field: dataset.content_hash, | |
| "source_manifest_sha256": _strict_sha256( | |
| config.matched_manifest_sha256, | |
| "matched_manifest_sha256"), | |
| "sample_count": len(dataset), | |
| "branches": branches, | |
| "forward_global_samples": self.fresh_video_total_samples, | |
| "gradient_global_samples": ( | |
| self.fresh_video_generator_grad_global_samples), | |
| "teacher_global_samples": int( | |
| config.expected_current_teacher_samples), | |
| "full_student_partial_gradient": ( | |
| self.fresh_video_full_student_partial_gradient), | |
| "kss_denominator": self.fresh_video_total_samples ** 2, | |
| "fixed_attraction_denominator": ( | |
| self.fresh_video_total_samples), | |
| "teacher_cross_denominator": ( | |
| self.fresh_video_total_samples | |
| * int(config.expected_current_teacher_samples)), | |
| "teacher_self_denominator": ( | |
| int(config.expected_current_teacher_samples) ** 2), | |
| "replay_cotangent_compensation": ( | |
| self.fresh_video_generator_grad_sample_scale), | |
| "branch_normalization": ( | |
| "raw_div_stop_gradient_raw_plus_1e-4"), | |
| "branch_aggregation": ( | |
| "sum_15_to_1_to_2_then_divide_10" | |
| if triple_bank | |
| else "sum_10_to_1_without_divide_2"), | |
| } | |
| elif self.current_teacher_source == "matched_manifest_video": | |
| from utils.matched_video_dataset import ( | |
| MatchedManifestVideoDataset, | |
| ) | |
| from utils.matched_video_feature_cache import ( | |
| MATCHED_PYAV_LAST64_DECODER_CONTRACT_SHA256, | |
| MATCHED_VJEPA2_L15_LAST64_FEATURE_CONTRACT_SHA256, | |
| ) | |
| if not self.uses_fresh_video_gradcache: | |
| raise ValueError( | |
| "current_teacher_source=matched_manifest_video requires " | |
| "the fresh-video exact Nyström path") | |
| if int(config.batch_size) != 1: | |
| raise ValueError( | |
| "lazy matched-video teacher decoding requires " | |
| "batch_size=1") | |
| manifest_path = os.path.realpath(os.path.expanduser(str( | |
| config.matched_manifest_path))) | |
| expected_manifest_sha256 = _strict_sha256( | |
| config.matched_manifest_sha256, | |
| "matched_manifest_sha256", | |
| ) | |
| actual_manifest_sha256 = _file_sha256(manifest_path) | |
| if actual_manifest_sha256 != expected_manifest_sha256: | |
| raise ValueError( | |
| "frozen matched-video manifest SHA-256 mismatch: " | |
| f"{actual_manifest_sha256} != " | |
| f"{expected_manifest_sha256}") | |
| source_csv_sha256 = _strict_sha256( | |
| config.matched_source_csv_sha256, | |
| "matched_source_csv_sha256", | |
| ) | |
| expected_sample_identity_sha256 = _strict_sha256( | |
| config.matched_sample_identity_sha256, | |
| "matched_sample_identity_sha256", | |
| ) | |
| model_sha256 = _strict_sha256( | |
| config.vjepa2_model_sha256, "vjepa2_model_sha256") | |
| feature_contract_sha256 = _strict_sha256( | |
| config.matched_teacher_feature_contract_sha256, | |
| "matched_teacher_feature_contract_sha256", | |
| ) | |
| if ( | |
| feature_contract_sha256 | |
| != MATCHED_VJEPA2_L15_LAST64_FEATURE_CONTRACT_SHA256 | |
| ): | |
| raise ValueError( | |
| "configured matched-video feature contract does not " | |
| "match runtime") | |
| decoder_contract_sha256 = _strict_sha256( | |
| config.matched_teacher_decoder_contract_sha256, | |
| "matched_teacher_decoder_contract_sha256", | |
| ) | |
| if ( | |
| decoder_contract_sha256 | |
| != MATCHED_PYAV_LAST64_DECODER_CONTRACT_SHA256 | |
| ): | |
| raise ValueError( | |
| "configured matched-video decoder contract does not " | |
| "match runtime") | |
| sampling_algorithm = str( | |
| config.matched_sampling_algorithm) | |
| if sampling_algorithm != "sha256_seed_step_position_mod_n_v1": | |
| raise ValueError( | |
| "unsupported matched counter sampling algorithm: " | |
| f"{sampling_algorithm!r}") | |
| if not bool(config.matched_sampling_with_replacement): | |
| raise ValueError( | |
| "matched sampler must explicitly enable replacement") | |
| expected_samples = int(getattr( | |
| config, "matched_manifest_samples", 5000)) | |
| dataset = MatchedManifestVideoDataset( | |
| manifest_path=manifest_path, | |
| video_root=str(config.matched_video_root), | |
| cache_root=str(config.matched_teacher_cache_root), | |
| expected_model_sha256=model_sha256, | |
| expected_feature_contract_sha256=( | |
| feature_contract_sha256), | |
| expected_source_csv_sha256=source_csv_sha256, | |
| expected_count=expected_samples, | |
| ) | |
| actual_sample_identity_sha256 = str( | |
| dataset.manifest_metadata.get( | |
| "sample_identity_sha256", "")) | |
| if ( | |
| actual_sample_identity_sha256 | |
| != expected_sample_identity_sha256 | |
| ): | |
| raise ValueError( | |
| "matched-video sample identity SHA-256 mismatch: " | |
| f"{actual_sample_identity_sha256} != " | |
| f"{expected_sample_identity_sha256}") | |
| self.current_teacher_dataset = dataset | |
| self.matched_counter_sampling_enabled = True | |
| self.experiment_identity = { | |
| "current_teacher_source": "matched_manifest_video", | |
| "matched_manifest_sha256": expected_manifest_sha256, | |
| "matched_source_csv_sha256": source_csv_sha256, | |
| "matched_sample_identity_sha256": ( | |
| expected_sample_identity_sha256), | |
| "matched_manifest_samples": expected_samples, | |
| "vjepa2_reference_model_id": str( | |
| config.vjepa2_reference_model_id), | |
| "vjepa2_model_sha256": model_sha256, | |
| "matched_teacher_feature_contract_sha256": ( | |
| feature_contract_sha256), | |
| "matched_teacher_decoder_contract_sha256": ( | |
| decoder_contract_sha256), | |
| "student_frame_contract": "last64_frames_17_80", | |
| "live_teacher_frame_contract": "last64_frames_17_80", | |
| "fixed_reference_frame_contract": "central64_existing_pack", | |
| "counter_sampling_algorithm": ( | |
| "sha256_seed_step_position_mod_n_v1"), | |
| "counter_sampling_seed": int(config.seed), | |
| "forward_global_samples": ( | |
| self.fresh_video_total_samples), | |
| "gradient_global_samples": ( | |
| self.fresh_video_generator_grad_global_samples), | |
| "explicit_gradient_compensation": ( | |
| self.fresh_video_generator_grad_sample_scale), | |
| "partial_mmd_gradient_compensation": ( | |
| self.fresh_video_generator_grad_sample_scale), | |
| "teacher_global_samples": int( | |
| config.expected_current_teacher_samples), | |
| "partial_mmd_objective": ( | |
| "full_student_biased_mmd_suffix_partial_backward_v1" | |
| if self.fresh_video_full_student_partial_gradient | |
| else "rectangular_gradient_student_v1" | |
| ), | |
| "full_student_partial_gradient": ( | |
| self.fresh_video_full_student_partial_gradient), | |
| "partial_mmd_kss_denominator": ( | |
| self.fresh_video_total_samples ** 2 | |
| if self.fresh_video_full_student_partial_gradient | |
| else self.fresh_video_total_samples | |
| * self.fresh_video_generator_grad_global_samples | |
| ), | |
| "partial_mmd_fixed_attraction_denominator": ( | |
| self.fresh_video_total_samples | |
| if self.fresh_video_full_student_partial_gradient | |
| else self.fresh_video_generator_grad_global_samples | |
| ), | |
| "partial_mmd_live_cross_denominator": ( | |
| self.fresh_video_total_samples | |
| * int(config.expected_current_teacher_samples) | |
| if self.fresh_video_full_student_partial_gradient | |
| else self.fresh_video_generator_grad_global_samples | |
| * int(config.expected_current_teacher_samples) | |
| ), | |
| "partial_mmd_teacher_self_denominator": ( | |
| int(config.expected_current_teacher_samples) ** 2), | |
| "partial_replay_upstream_commit": ( | |
| "ef43f2d616ff31ba34904ae9743b0de4dda95afd"), | |
| "fixed_reference_file_sha256": _strict_sha256( | |
| config.vjepa2_nystrom_reference_sha256, | |
| "vjepa2_nystrom_reference_sha256"), | |
| "fixed_reference_hash": str(getattr( | |
| self.model, "vjepa2_nystrom_reference_hash", "")), | |
| "live_teacher_baseline_commit": str( | |
| config.live_teacher_baseline_commit), | |
| "partial_gradient_reference_commit": str( | |
| config.partial_gradient_reference_commit), | |
| "feature_protocol_identity": str( | |
| config.feature_protocol_identity), | |
| } | |
| elif self.current_teacher_source == "openvid_video": | |
| from utils.openvid_feature_cache import ( | |
| OPENVID_PYAV_DECODER_CONTRACT_SHA256, | |
| OPENVID_VJEPA2_L15_FEATURE_CONTRACT_SHA256, | |
| ) | |
| if not self.uses_fresh_video_gradcache: | |
| raise ValueError( | |
| "current_teacher_source=openvid_video requires the " | |
| "fresh-video exact Nyström path") | |
| if int(config.batch_size) != 1: | |
| raise ValueError( | |
| "OpenVid lazy teacher decoding currently requires " | |
| "batch_size=1") | |
| manifest_path = os.path.realpath(os.path.expanduser(str( | |
| config.openvid_manifest_path))) | |
| expected_manifest_sha256 = _strict_sha256( | |
| config.openvid_manifest_sha256, | |
| "openvid_manifest_sha256", | |
| ) | |
| actual_manifest_sha256 = _file_sha256(manifest_path) | |
| if actual_manifest_sha256 != expected_manifest_sha256: | |
| raise ValueError( | |
| "frozen OpenVid manifest SHA-256 mismatch: " | |
| f"{actual_manifest_sha256} != " | |
| f"{expected_manifest_sha256}") | |
| model_sha256 = _strict_sha256( | |
| config.vjepa2_model_sha256, "vjepa2_model_sha256") | |
| feature_contract_sha256 = _strict_sha256( | |
| config.openvid_teacher_feature_contract_sha256, | |
| "openvid_teacher_feature_contract_sha256", | |
| ) | |
| if ( | |
| feature_contract_sha256 | |
| != OPENVID_VJEPA2_L15_FEATURE_CONTRACT_SHA256 | |
| ): | |
| raise ValueError( | |
| "configured OpenVid feature contract does not match " | |
| "runtime") | |
| decoder_contract_sha256 = _strict_sha256( | |
| config.openvid_teacher_decoder_contract_sha256, | |
| "openvid_teacher_decoder_contract_sha256", | |
| ) | |
| if ( | |
| decoder_contract_sha256 | |
| != OPENVID_PYAV_DECODER_CONTRACT_SHA256 | |
| ): | |
| raise ValueError( | |
| "configured OpenVid decoder contract does not match " | |
| "runtime") | |
| dataset = OpenVidPromptVideoDataset( | |
| manifest_path=manifest_path, | |
| video_root=str(config.openvid_video_root), | |
| cache_root=str(config.openvid_teacher_cache_root), | |
| expected_model_sha256=model_sha256, | |
| expected_feature_contract_sha256=( | |
| feature_contract_sha256), | |
| ) | |
| expected_samples = int(getattr( | |
| config, "openvid_manifest_samples", 12800)) | |
| if len(dataset) != expected_samples: | |
| raise ValueError( | |
| "frozen OpenVid dataset size mismatch: " | |
| f"{len(dataset)} != {expected_samples}") | |
| self.current_teacher_dataset = dataset | |
| self.experiment_identity = { | |
| "current_teacher_source": "openvid_video", | |
| "openvid_manifest_sha256": expected_manifest_sha256, | |
| "vjepa2_reference_model_id": str( | |
| config.vjepa2_reference_model_id), | |
| "vjepa2_model_sha256": model_sha256, | |
| "openvid_teacher_feature_contract_sha256": ( | |
| feature_contract_sha256), | |
| "openvid_teacher_decoder_contract_sha256": ( | |
| decoder_contract_sha256), | |
| } | |
| else: | |
| load_matched_terminal_teacher = bool( | |
| self.uses_fresh_video_gradcache | |
| and self.current_teacher_source == "lmdb_latent") | |
| dataset = ODERegressionLMDBDataset( | |
| config.teacher_latent_data_path, | |
| max_pair=int(getattr( | |
| config, "teacher_latent_count", 1e8)), | |
| load_latents=( | |
| not self.uses_fresh_video_gradcache | |
| or load_matched_terminal_teacher | |
| ), | |
| load_terminal_latent_only=( | |
| load_matched_terminal_teacher), | |
| ) | |
| elif self.config.i2v: | |
| dataset = ShardingLMDBDataset(config.data_path, max_pair=int(1e8)) | |
| else: | |
| dataset = TextDataset(config.data_path) | |
| self.dataset = dataset | |
| if self.matched_counter_sampling_enabled: | |
| self.dataloader_len = 1 | |
| self.dataloader = None | |
| else: | |
| sampler = torch.utils.data.distributed.DistributedSampler( | |
| dataset, shuffle=True, drop_last=True) | |
| dataloader = torch.utils.data.DataLoader( | |
| dataset, | |
| batch_size=config.batch_size, | |
| sampler=sampler, | |
| num_workers=int(getattr(config, "num_workers", 4)), | |
| drop_last=True, | |
| collate_fn=( | |
| dataset.collate_fn | |
| if self.current_teacher_dataset is dataset else None), | |
| ) | |
| self.dataloader_len = max(1, len(dataloader)) | |
| self.dataloader = cycle(dataloader) | |
| if dist.get_rank() == 0: | |
| print("DATASET SIZE %d" % len(dataset)) | |
| ############################################################################################################## | |
| # 6. Record trainable parameters. Generator EMA is initialized only | |
| # after loading the generator checkpoint, below. | |
| rename_param = ( | |
| lambda name: name.replace("_fsdp_wrapped_module.", "") | |
| .replace("_checkpoint_wrapped_module.", "") | |
| .replace("_orig_mod.", "") | |
| ) | |
| self.name_to_trainable_params = {} | |
| for n, p in self.model.generator.named_parameters(): | |
| if not p.requires_grad: | |
| continue | |
| renamed_n = rename_param(n) | |
| self.name_to_trainable_params[renamed_n] = p | |
| ema_weight = float(getattr(config, "ema_weight", 0.0) or 0.0) | |
| self.generator_ema = None | |
| resume_ema_state = None | |
| ############################################################################################################## | |
| # 7. Load model/distribution state before constructing an EMA shadow. | |
| resume_checkpoint = getattr(config, "resume_checkpoint", None) | |
| self.resume_data_counter_override = ( | |
| _validated_resume_data_counter_override( | |
| getattr(config, "resume_data_counter_override", None), | |
| resume_checkpoint=resume_checkpoint, | |
| ) | |
| ) | |
| if resume_checkpoint: | |
| # Restore generator weights, optional generator EMA, distribution | |
| # history and step. Optimizer/RNG/dataloader state is restored later | |
| # when save_full_state is enabled and optim.pt is present. | |
| print(f"Resuming training state from {resume_checkpoint}") | |
| ckpt = torch.load(resume_checkpoint, map_location="cpu", weights_only=False) | |
| if self.experiment_identity is not None: | |
| saved_identity = ( | |
| ckpt.get("experiment_identity") | |
| if isinstance(ckpt, dict) | |
| else None | |
| ) | |
| saved_identity = _canonicalize_experiment_identity_paths( | |
| saved_identity) | |
| runtime_identity = _canonicalize_experiment_identity_paths( | |
| self.experiment_identity) | |
| if saved_identity != runtime_identity: | |
| raise ValueError( | |
| "resume checkpoint experiment identity does not match " | |
| "the frozen teacher/V-JEPA2 experiment identity: " | |
| f"{saved_identity!r} != " | |
| f"{runtime_identity!r}") | |
| gen_sd = ckpt["generator"] if (isinstance(ckpt, dict) and "generator" in ckpt) else ckpt | |
| self.model.generator.load_state_dict(gen_sd, strict=False) | |
| if isinstance(ckpt, dict): | |
| resume_ema_state = ckpt.get("generator_ema") | |
| if ( | |
| isinstance(ckpt, dict) | |
| and "fd" in ckpt | |
| and hasattr(self.model, "load_fd_state_dict") | |
| ): | |
| self.model.load_fd_state_dict(ckpt["fd"]) | |
| print(" restored distribution state") | |
| elif getattr( | |
| self.model, "requires_distribution_state_on_resume", False | |
| ): | |
| raise ValueError( | |
| "resume checkpoint is missing the required " | |
| "distribution history under key 'fd'") | |
| if isinstance(ckpt, dict) and "step" in ckpt: | |
| self.step = int(ckpt["step"]) | |
| else: | |
| import re | |
| m = re.search(r"checkpoint_model_(\d+)", str(resume_checkpoint)) | |
| self.step = int(m.group(1)) if m else 0 | |
| if isinstance(ckpt, dict) and "data_counter" in ckpt: | |
| self.data_counter = int(ckpt["data_counter"]) | |
| print(f" resumed at step {self.step}") | |
| elif getattr(config, "generator_ckpt", False): | |
| print(f"Loading pretrained generator from {config.generator_ckpt}") | |
| state_dict = torch.load(config.generator_ckpt, map_location="cpu") | |
| fd_state_dict = state_dict.get("fd") if isinstance(state_dict, dict) else None | |
| if isinstance(state_dict, dict): | |
| if "generator" in state_dict: | |
| state_dict = state_dict["generator"] | |
| elif "model" in state_dict: | |
| state_dict = state_dict["model"] | |
| self.model.generator.load_state_dict( | |
| state_dict, strict=False | |
| ) | |
| if fd_state_dict is not None and hasattr(self.model, "load_fd_state_dict"): | |
| is_compatible_initialization_state = ( | |
| self.model.can_load_initialization_fd_state(fd_state_dict) | |
| if hasattr( | |
| self.model, "can_load_initialization_fd_state") | |
| else not getattr( | |
| self.model, | |
| "requires_distribution_state_on_resume", | |
| False, | |
| ) | |
| ) | |
| if is_compatible_initialization_state: | |
| # Known dense-MMD states are always passed through the | |
| # model's strict metadata/cross-feature compatibility | |
| # checks. Unknown distribution-state payloads in an initialization-only | |
| # checkpoint remain intentionally ignorable. | |
| self.model.load_fd_state_dict(fd_state_dict) | |
| elif self.is_main_process: | |
| print( | |
| " initialization checkpoint has unrelated or " | |
| "incompatible FD state; using generator weights only") | |
| ############################################################################################################## | |
| ema_start_step = int(getattr(config, "ema_start_step", 0)) | |
| if ema_weight > 0.0 and self.step >= ema_start_step: | |
| print(f"Setting up EMA with weight {ema_weight}") | |
| # This now snapshots the loaded generator, never the stale | |
| # pre-checkpoint parameters. | |
| self.generator_ema = EMA_FSDP( | |
| self.model.generator, decay=ema_weight) | |
| if resume_ema_state is not None: | |
| self.generator_ema.load_state_dict(resume_ema_state) | |
| print(" restored EMA shadow") | |
| elif resume_checkpoint and self.step > ema_start_step: | |
| raise ValueError( | |
| "resume checkpoint is past ema_start_step but has no " | |
| "generator_ema state") | |
| self.max_grad_norm_generator = getattr(config, "max_grad_norm_generator", 10.0) | |
| self.max_grad_norm_critic = getattr(config, "max_grad_norm_critic", 10.0) | |
| self.previous_time = None | |
| # ---- precise-resume + periodic eval bookkeeping ---- | |
| # ``save_full_state`` additionally writes the optimizer state, per-rank RNG | |
| # states and the dataloader counter so a resume continues bit-for-bit (up | |
| # to multi-worker dataloader ordering). Off by default so other trainers | |
| # (DMD etc.) keep their lightweight checkpoints. | |
| self.save_full_state = bool(getattr(config, "save_full_state", False)) | |
| self.save_full_state_final_only = bool( | |
| getattr(config, "save_full_state_final_only", False)) | |
| self.checkpoint_keep_last = int( | |
| getattr(config, "checkpoint_keep_last", 0)) | |
| if self.checkpoint_keep_last < 0: | |
| raise ValueError("checkpoint_keep_last must be non-negative") | |
| self.eval_interval = int(getattr(config, "eval_interval", 0)) | |
| self.eval_seed = int(getattr(config, "eval_seed", 12345)) | |
| self.eval_num_prompts = int(getattr(config, "eval_num_prompts", 4)) | |
| self.eval_prompts = self._load_eval_prompts() if self.eval_interval > 0 else [] | |
| self._last_saved_step = None | |
| # If we resumed and the optimizer/RNG/data state was restored, this is set. | |
| self._restore_full_training_state(resume_checkpoint) | |
| self._active_generator_lr = self._set_generator_learning_rate( | |
| self.step + 1) | |
| if self.is_main_process and str(getattr( | |
| self.config, "generator_lr_schedule", "")).strip(): | |
| print( | |
| "[generator-lr] restored_optimizer_override " | |
| f"next_completed_step={self.step + 1} " | |
| f"lr={self._active_generator_lr:.12g}", | |
| flush=True, | |
| ) | |
| def _all_ranks_true(self, local_value): | |
| """Create an all-rank decision before a rank can branch or raise.""" | |
| flag = torch.tensor( | |
| int(bool(local_value)), device=self.device, dtype=torch.int32) | |
| if dist.is_initialized() and dist.get_world_size() > 1: | |
| dist.all_reduce(flag, op=dist.ReduceOp.MIN) | |
| return bool(flag.item()) | |
| def _set_generator_learning_rate(self, completed_step): | |
| """Apply an opt-in schedule after optimizer restore and before update.""" | |
| schedule = str(getattr( | |
| self.config, "generator_lr_schedule", "")).strip() | |
| if not schedule: | |
| # Preserve historical resume behavior for every configuration that | |
| # has not explicitly opted into a generator LR schedule. | |
| return float(self.generator_optimizer.param_groups[0]["lr"]) | |
| lr = _generator_lr_for_completed_step( | |
| self.config, completed_step) | |
| for param_group in self.generator_optimizer.param_groups: | |
| param_group["lr"] = lr | |
| return lr | |
| def save(self): | |
| if ( | |
| self.fresh_video_gradcache is not None | |
| and ( | |
| self.fresh_video_gradcache.local_samples | |
| or self.fresh_video_gradcache.finalized | |
| ) | |
| ): | |
| raise RuntimeError( | |
| "refusing to checkpoint a partial fresh-video optimizer " | |
| "update") | |
| if ( | |
| hasattr(self.model, "distribution_state_pending") | |
| and self.model.distribution_state_pending() | |
| ): | |
| raise RuntimeError( | |
| "refusing to checkpoint pending distribution state") | |
| # max_steps can coincide with log_iters; avoid gathering and rewriting | |
| # the same full FSDP checkpoint twice. | |
| if self._last_saved_step == self.step: | |
| return | |
| max_steps = int(getattr(self.config, "max_steps", 0)) | |
| write_full_state = ( | |
| self.save_full_state | |
| and ( | |
| not self.save_full_state_final_only | |
| or (max_steps > 0 and self.step >= max_steps) | |
| ) | |
| ) | |
| required_free_gib = float( | |
| getattr( | |
| self.config, | |
| ( | |
| "checkpoint_full_min_free_gib" | |
| if write_full_state | |
| else "checkpoint_model_min_free_gib" | |
| ), | |
| 0.0, | |
| ) | |
| ) | |
| disk_ok = True | |
| disk_error = None | |
| if self.is_main_process and required_free_gib > 0: | |
| try: | |
| free_gib = ( | |
| shutil.disk_usage(self.output_path).free / (1024 ** 3)) | |
| if free_gib < required_free_gib: | |
| disk_ok = False | |
| disk_error = ( | |
| f"checkpoint at step {self.step} needs at least " | |
| f"{required_free_gib:.1f} GiB free, only " | |
| f"{free_gib:.1f} GiB is available" | |
| ) | |
| except Exception as exc: | |
| disk_ok = False | |
| disk_error = f"checkpoint disk-space check failed: {exc}" | |
| disk_flag = torch.tensor( | |
| int(disk_ok), device=self.device, dtype=torch.int32) | |
| if dist.is_initialized(): | |
| dist.broadcast(disk_flag, src=0) | |
| if not bool(disk_flag.item()): | |
| raise RuntimeError( | |
| disk_error or "rank 0 checkpoint disk-space check failed") | |
| print("Start gathering distributed model states...") | |
| generator_state_dict = fsdp_state_dict( | |
| self.model.generator) | |
| critic_state_dict = ( | |
| fsdp_state_dict(self.model.fake_score) | |
| if self.uses_critic | |
| else None | |
| ) | |
| if self.generator_ema is not None: | |
| state_dict = { | |
| "generator": generator_state_dict, | |
| "generator_ema": self.generator_ema.state_dict(), | |
| } | |
| else: | |
| state_dict = { | |
| "generator": generator_state_dict, | |
| } | |
| if self.uses_critic: | |
| state_dict["critic"] = critic_state_dict | |
| if hasattr(self.model, "fd_state_dict"): | |
| state_dict["fd"] = self.model.fd_state_dict() | |
| # record the step so a resume continues numbering / EMA timing. | |
| state_dict["step"] = self.step | |
| state_dict["data_counter"] = self.data_counter | |
| if self.experiment_identity is not None: | |
| state_dict["experiment_identity"] = ( | |
| _canonicalize_experiment_identity_paths( | |
| self.experiment_identity)) | |
| # ---- full training state for precise resume (optimizer + RNG + data) ---- | |
| gen_optim_full = None | |
| critic_optim_full = None | |
| if write_full_state: | |
| # collective: every rank participates; full state gathered on rank 0. | |
| gen_optim_full = fsdp_optim_state_dict(self.model.generator, self.generator_optimizer) | |
| if self.uses_critic: | |
| critic_optim_full = fsdp_optim_state_dict(self.model.fake_score, self.critic_optimizer) | |
| ckpt_dir = os.path.join(self.output_path, f"checkpoint_model_{self.step:06d}") | |
| rank0_write_error = None | |
| if self.is_main_process: | |
| try: | |
| os.makedirs(ckpt_dir, exist_ok=True) | |
| model_path = os.path.join(ckpt_dir, "model.pt") | |
| _atomic_torch_save(state_dict, model_path) | |
| print("Model saved to", model_path) | |
| if write_full_state: | |
| optim_path = os.path.join(ckpt_dir, "optim.pt") | |
| _atomic_torch_save( | |
| { | |
| "generator_optimizer": gen_optim_full, | |
| "critic_optimizer": critic_optim_full, | |
| "step": self.step, | |
| "data_counter": self.data_counter, | |
| "world_size": self.world_size, | |
| }, | |
| optim_path, | |
| ) | |
| print( | |
| "Optimizer/training state saved to", | |
| optim_path, | |
| ) | |
| except Exception as exc: | |
| rank0_write_error = exc | |
| write_ok = torch.tensor( | |
| int(rank0_write_error is None), | |
| device=self.device, | |
| dtype=torch.int32, | |
| ) | |
| if dist.is_initialized(): | |
| dist.broadcast(write_ok, src=0) | |
| if not bool(write_ok.item()): | |
| if rank0_write_error is not None: | |
| raise RuntimeError( | |
| f"checkpoint write failed at step {self.step}" | |
| ) from rank0_write_error | |
| raise RuntimeError( | |
| f"rank 0 checkpoint write failed at step {self.step}") | |
| # Per-rank RNG state (each rank writes its own) for reproducible streams. | |
| rng_write_ok = True | |
| rng_write_error = None | |
| if write_full_state: | |
| try: | |
| os.makedirs(ckpt_dir, exist_ok=True) | |
| rng_state = { | |
| "torch": torch.get_rng_state(), | |
| "cuda": torch.cuda.get_rng_state(self.device), | |
| "numpy": np.random.get_state(), | |
| "python": random.getstate(), | |
| "data_counter": self.data_counter, | |
| } | |
| _atomic_torch_save( | |
| rng_state, | |
| os.path.join( | |
| ckpt_dir, f"rng_rank{dist.get_rank():02d}.pt"), | |
| ) | |
| except Exception as exc: | |
| rng_write_ok = False | |
| rng_write_error = exc | |
| rng_ok = torch.tensor( | |
| int(rng_write_ok), device=self.device, dtype=torch.int32) | |
| if dist.is_initialized(): | |
| dist.all_reduce(rng_ok, op=dist.ReduceOp.MIN) | |
| if not bool(rng_ok.item()): | |
| if rng_write_error is not None: | |
| raise RuntimeError( | |
| f"rank {dist.get_rank()} RNG checkpoint write failed at " | |
| f"step {self.step}" | |
| ) from rng_write_error | |
| raise RuntimeError( | |
| f"an RNG checkpoint write failed at step {self.step}") | |
| if dist.is_initialized(): | |
| dist.barrier() | |
| retention_error = None | |
| if self.is_main_process and self.checkpoint_keep_last > 0: | |
| try: | |
| checkpoints = [] | |
| for name in os.listdir(self.output_path): | |
| prefix = "checkpoint_model_" | |
| if not name.startswith(prefix): | |
| continue | |
| try: | |
| checkpoint_step = int(name[len(prefix):]) | |
| except ValueError: | |
| continue | |
| path = os.path.join(self.output_path, name) | |
| if not os.path.isdir(path): | |
| continue | |
| if not os.path.isfile(os.path.join(path, "model.pt")): | |
| shutil.rmtree(path) | |
| print("Removed incomplete checkpoint", path) | |
| continue | |
| checkpoints.append((checkpoint_step, path)) | |
| checkpoints.sort() | |
| for _, path in checkpoints[:-self.checkpoint_keep_last]: | |
| shutil.rmtree(path) | |
| print("Removed old checkpoint", path) | |
| except Exception as exc: | |
| retention_error = exc | |
| retention_ok = torch.tensor( | |
| int(retention_error is None), | |
| device=self.device, | |
| dtype=torch.int32, | |
| ) | |
| if dist.is_initialized(): | |
| dist.broadcast(retention_ok, src=0) | |
| if not bool(retention_ok.item()): | |
| if retention_error is not None: | |
| raise RuntimeError( | |
| "checkpoint retention failed after publishing a valid " | |
| f"checkpoint at step {self.step}" | |
| ) from retention_error | |
| raise RuntimeError( | |
| "rank 0 checkpoint retention failed after publishing a " | |
| f"valid checkpoint at step {self.step}") | |
| if dist.is_initialized(): | |
| dist.barrier() | |
| self._last_saved_step = self.step | |
| def _restore_full_training_state(self, resume_checkpoint): | |
| """Restore optimizer + per-rank RNG + dataloader position for a precise resume. | |
| Called at the end of ``__init__`` (after the optimizer and generator | |
| weights exist). Silently no-ops if the resume checkpoint has no | |
| ``optim.pt`` (e.g. an old lightweight checkpoint), so older runs still | |
| resume their weights/EMA/queue/step as before. | |
| """ | |
| if not resume_checkpoint: | |
| return | |
| if not self.save_full_state: | |
| # Lightweight checkpoints still record data_counter. They do not | |
| # promise bit-exact optimizer/RNG continuation, but they must not | |
| # restart the prompt stream at its first batch. | |
| self._apply_lightweight_resume_data_counter_override() | |
| self._fast_forward_dataloader(self.data_counter) | |
| if self.is_main_process: | |
| print( | |
| " lightweight resume: restored generator/distribution " | |
| "state and data position; optimizer and RNG start fresh", | |
| flush=True, | |
| ) | |
| return | |
| ckpt_dir = os.path.dirname(resume_checkpoint) | |
| optim_path = os.path.join(ckpt_dir, "optim.pt") | |
| if os.path.exists(optim_path): | |
| if getattr(self, "resume_data_counter_override", None) is not None: | |
| raise ValueError( | |
| "resume_data_counter_override is only valid for a " | |
| "lightweight resume; precise optimizer state exists at " | |
| f"{optim_path}") | |
| # rank0 holds the full optimizer state; FSDP broadcasts/shards it. | |
| full = None | |
| optim_load_error = None | |
| if self.is_main_process: | |
| try: | |
| full = torch.load( | |
| optim_path, | |
| map_location="cpu", | |
| weights_only=False, | |
| ) | |
| except Exception as exc: | |
| optim_load_error = exc | |
| optim_load_ok = torch.tensor( | |
| int(optim_load_error is None), | |
| device=self.device, | |
| dtype=torch.int32, | |
| ) | |
| if dist.is_initialized(): | |
| dist.broadcast(optim_load_ok, src=0) | |
| if not bool(optim_load_ok.item()): | |
| if optim_load_error is not None: | |
| raise RuntimeError( | |
| f"failed to load optimizer checkpoint {optim_path}" | |
| ) from optim_load_error | |
| raise RuntimeError( | |
| f"rank 0 failed to load optimizer checkpoint {optim_path}") | |
| saved_world_size = torch.tensor( | |
| [ | |
| int(full.get("world_size", self.world_size)) | |
| if full is not None | |
| else 0 | |
| ], | |
| device=self.device, | |
| dtype=torch.long, | |
| ) | |
| dist.broadcast(saved_world_size, src=0) | |
| if ( | |
| int(saved_world_size.item()) != self.world_size | |
| and not bool(getattr( | |
| self.config, "allow_resume_world_size_change", False)) | |
| ): | |
| raise ValueError( | |
| "precise resume world-size mismatch: checkpoint used " | |
| f"{int(saved_world_size.item())}, current run uses " | |
| f"{self.world_size}; set allow_resume_world_size_change " | |
| "only if non-bit-exact data/RNG continuation is acceptable") | |
| gen_osd = full["generator_optimizer"] if full is not None else None | |
| fsdp_load_optim_state_dict(self.model.generator, self.generator_optimizer, gen_osd) | |
| if self.uses_critic: | |
| critic_osd = full["critic_optimizer"] if full is not None else None | |
| if critic_osd is not None: | |
| fsdp_load_optim_state_dict(self.model.fake_score, self.critic_optimizer, critic_osd) | |
| # broadcast the data counter from rank0 | |
| dc = torch.tensor( | |
| [int(full["data_counter"]) if full is not None else 0], | |
| device=self.device, dtype=torch.long, | |
| ) | |
| if dist.is_initialized(): | |
| dist.broadcast(dc, src=0) | |
| self.data_counter = int(dc.item()) | |
| if self.is_main_process: | |
| print(f" restored optimizer state; data_counter={self.data_counter}") | |
| else: | |
| self._apply_lightweight_resume_data_counter_override() | |
| if self.is_main_process: | |
| print( | |
| " lightweight resume: restored generator/MMD history " | |
| "and data position; optimizer and RNG start fresh", | |
| flush=True, | |
| ) | |
| # Read RNG now, but restore it only after DataLoader fast-forwarding: | |
| # creating/advancing workers consumes the main process RNG. | |
| rng_path = os.path.join(ckpt_dir, f"rng_rank{dist.get_rank():02d}.pt") | |
| rng = None | |
| if os.path.exists(rng_path): | |
| rng = torch.load(rng_path, map_location="cpu", weights_only=False) | |
| # fast-forward the prompt stream to its position | |
| self._fast_forward_dataloader(self.data_counter) | |
| if rng is not None: | |
| torch.set_rng_state(rng["torch"]) | |
| torch.cuda.set_rng_state(rng["cuda"], self.device) | |
| np.random.set_state(rng["numpy"]) | |
| random.setstate(rng["python"]) | |
| if self.is_main_process: | |
| print(" restored per-rank RNG states") | |
| def _apply_lightweight_resume_data_counter_override(self): | |
| override = getattr(self, "resume_data_counter_override", None) | |
| if override is None: | |
| return | |
| checkpoint_counter = self.data_counter | |
| self.data_counter = override | |
| if self.is_main_process: | |
| print( | |
| " lightweight resume data cursor: explicitly overriding " | |
| f"checkpoint data_counter {checkpoint_counter} -> {override} " | |
| "from resume_data_counter_override", | |
| flush=True, | |
| ) | |
| def _matched_global_indices_for_step(self): | |
| """Counter-based with-replacement draw shared identically by all ranks.""" | |
| if not getattr(self, "matched_counter_sampling_enabled", False): | |
| raise RuntimeError("matched counter sampler is not enabled") | |
| population = len(self.dataset) | |
| if population <= 0: | |
| raise RuntimeError("matched counter sampler has an empty population") | |
| seed = int(self.config.seed) | |
| step = int(self.step) | |
| indices = [] | |
| for position in range(self.fresh_video_total_samples): | |
| payload = ( | |
| f"sf-matched-counter-v1\0{seed}\0{step}\0{position}" | |
| .encode("ascii") | |
| ) | |
| value = int.from_bytes( | |
| hashlib.sha256(payload).digest()[:8], "little") | |
| indices.append(value % population) | |
| return tuple(indices) | |
| def _matched_batch_for_microbatch(self, microbatch_index): | |
| if not getattr(self, "matched_counter_sampling_enabled", False): | |
| raise RuntimeError("matched counter sampler is not enabled") | |
| batch_size = int(self.config.batch_size) | |
| local_offset = ( | |
| dist.get_rank() * self.fresh_video_local_samples | |
| + int(microbatch_index) * batch_size | |
| ) | |
| indices = self._matched_global_indices_for_step() | |
| selected = indices[local_offset:local_offset + batch_size] | |
| if len(selected) != batch_size: | |
| raise RuntimeError("matched counter sampler produced a short slice") | |
| items = [self.dataset[index] for index in selected] | |
| self.data_counter += 1 | |
| return self.dataset.collate_fn(items) | |
| def _fast_forward_dataloader(self, n_consumed): | |
| if getattr(self, "matched_counter_sampling_enabled", False): | |
| return | |
| if n_consumed <= 0: | |
| return | |
| if hasattr(self.dataloader, "seek"): | |
| skip = self.dataloader.seek(n_consumed) | |
| else: | |
| skip = n_consumed % self.dataloader_len | |
| for _ in range(skip): | |
| next(self.dataloader) | |
| if self.is_main_process and skip: | |
| print( | |
| f" restored dataloader epoch/offset " | |
| f"({n_consumed // self.dataloader_len}, {skip})") | |
| def _next_batch(self): | |
| if getattr(self, "matched_counter_sampling_enabled", False): | |
| raise RuntimeError( | |
| "matched-manifest training must use the counter sampler") | |
| self.data_counter += 1 | |
| return next(self.dataloader) | |
| def _load_eval_prompts(self): | |
| path = getattr(self.config, "eval_prompt_path", "prompts/test_8.txt") | |
| try: | |
| with open(path, encoding="utf-8") as f: | |
| lines = [l.strip() for l in f if l.strip()] | |
| except FileNotFoundError: | |
| if self.is_main_process: | |
| print(f"[eval] prompt file {path} not found; eval disabled") | |
| return [] | |
| return lines[: self.eval_num_prompts] | |
| def _fd_eval_inference(self): | |
| """Generate fixed-prompt videos with the live generator and log to wandb. | |
| Runs the model's own few-step (here single-step, no-CFG) generation under | |
| a fixed seed so successive evals are directly comparable. All ranks | |
| participate (FSDP forward is collective); only rank 0 writes/logs. | |
| """ | |
| if not self.eval_prompts: | |
| return | |
| # Frozen encoders stay in eval mode; generator gradients are enabled only on replay. | |
| self.model.eval() | |
| # fix the RNG for reproducible eval, then restore so training is unperturbed | |
| cpu_rng = torch.get_rng_state() | |
| cuda_rng = torch.cuda.get_rng_state(self.device) | |
| torch.manual_seed(self.eval_seed) | |
| prompts = self.eval_prompts | |
| # Evaluation is deliberately serialized at one video per rank. The | |
| # training forward is also microbatch 1; evaluating all fixed prompts | |
| # together would multiply the peak generator/VAE memory before the | |
| # first optimizer update. | |
| rank0_pixels = [] | |
| try: | |
| for prompt in prompts: | |
| conditional_dict = self.model.text_encoder( | |
| text_prompts=[prompt]) | |
| unconditional_dict = self.model.text_encoder( | |
| text_prompts=[self.config.negative_prompt]) | |
| shape = [1] + list( | |
| self.config.image_or_video_shape[1:]) | |
| latent = self.model._run_few_step_generator( | |
| image_or_video_shape=shape, | |
| conditional_dict=conditional_dict, | |
| unconditional_dict=unconditional_dict, | |
| ) | |
| pixels = self.model.vae.decode_to_pixel( | |
| latent.to(self.dtype)) | |
| pixels = ((pixels + 1.0) * 0.5).clamp(0.0, 1.0) | |
| if self.is_main_process: | |
| rank0_pixels.append(pixels[0].float().cpu()) | |
| del latent, pixels, conditional_dict, unconditional_dict | |
| finally: | |
| torch.set_rng_state(cpu_rng) | |
| torch.cuda.set_rng_state(cuda_rng, self.device) | |
| if self.is_main_process: | |
| eval_dir = os.path.join(self.output_path, "eval", f"step_{self.step:06d}") | |
| os.makedirs(eval_dir, exist_ok=True) | |
| log_videos = {} | |
| for i in range(len(prompts)): | |
| vid = ( | |
| rank0_pixels[i].permute(0, 2, 3, 1).numpy() * 255.0 | |
| ).astype("uint8") # [T,H,W,C] | |
| out_path = os.path.join(eval_dir, f"sample_{i}.mp4") | |
| wrote = False | |
| try: | |
| iio.imwrite( | |
| out_path, | |
| vid, | |
| fps=16, | |
| codec="libx264", | |
| plugin="FFMPEG", | |
| ) | |
| wrote = True | |
| except Exception as exc: # pragma: no cover - codec/runtime issues | |
| print(f"[eval] failed to write {out_path}: {exc}") | |
| # Log the encoded mp4 by PATH so wandb does not require moviepy. | |
| if not self.disable_wandb and wrote: | |
| try: | |
| log_videos[f"eval/sample_{i}"] = wandb.Video(out_path, format="mp4") | |
| except Exception as exc: # pragma: no cover | |
| print(f"[eval] wandb.Video failed for {out_path}: {exc}") | |
| if not self.disable_wandb and log_videos: | |
| wandb.log(log_videos, step=self.step) | |
| print(f"[eval] step {self.step}: wrote {len(prompts)} videos to {eval_dir}", flush=True) | |
| if dist.is_initialized(): | |
| dist.barrier() | |
| def _batch_string_list(value, *, key, batch_size): | |
| if isinstance(value, str): | |
| values = [value] | |
| elif isinstance(value, (list, tuple)): | |
| values = list(value) | |
| else: | |
| raise TypeError(f"{key} must collate to a list of strings") | |
| if ( | |
| len(values) != batch_size | |
| or any(not isinstance(item, str) or not item for item in values) | |
| ): | |
| raise ValueError( | |
| f"{key} must contain one non-empty string per prompt") | |
| return values | |
| def _attach_matched_teacher_payload(self, batch): | |
| """Load cache/decode teacher data only for a selected replay suffix.""" | |
| if self.current_teacher_source != "matched_manifest_video": | |
| return | |
| if self.current_teacher_dataset is None: | |
| raise RuntimeError("matched teacher dataset is unavailable") | |
| batch_size = len(batch.get("prompts", ())) | |
| sample_ids = self._batch_string_list( | |
| batch.get("sample_ids"), key="sample_ids", batch_size=batch_size) | |
| cache_keys = self._batch_string_list( | |
| batch.get("teacher_cache_keys"), key="teacher_cache_keys", | |
| batch_size=batch_size) | |
| local_error = None | |
| payloads = None | |
| try: | |
| payloads = [ | |
| self.current_teacher_dataset.load_teacher_payload(sample_id) | |
| for sample_id in sample_ids | |
| ] | |
| for sample_id, cache_key, payload in zip( | |
| sample_ids, cache_keys, payloads | |
| ): | |
| if ( | |
| payload.get("sample_id") != sample_id | |
| or payload.get("teacher_cache_key") != cache_key | |
| ): | |
| raise ValueError( | |
| "lazy teacher payload identity does not match the " | |
| "selected student") | |
| hit_values = [ | |
| bool(payload["current_teacher_cache_hit"]) | |
| for payload in payloads | |
| ] | |
| if not (all(hit_values) or not any(hit_values)): | |
| raise ValueError( | |
| "a metadata microbatch cannot mix cached features and " | |
| "decoded videos") | |
| batch["current_teacher_cache_hit"] = torch.tensor( | |
| hit_values, dtype=torch.bool) | |
| batch["current_teacher_decode_seconds"] = sum( | |
| float(payload["current_teacher_decode_seconds"]) | |
| for payload in payloads | |
| ) | |
| if all(hit_values): | |
| batch["current_teacher_features"] = torch.stack([ | |
| payload["current_teacher_features"] | |
| for payload in payloads | |
| ]) | |
| else: | |
| batch["current_teacher_video01"] = torch.stack([ | |
| payload["current_teacher_video01"] | |
| for payload in payloads | |
| ]) | |
| except BaseException as exc: | |
| local_error = exc | |
| if not self._all_ranks_true(local_error is None): | |
| message = ( | |
| f": {type(local_error).__name__}: {local_error}" | |
| if local_error is not None else " on another rank") | |
| raise RuntimeError( | |
| "matched live-teacher lazy I/O failed" + message | |
| ) from local_error | |
| def _prepare_current_teacher_inputs(self, batch, batch_size): | |
| """Validate and move exactly one configured teacher payload.""" | |
| if not bool(getattr( | |
| self.model, "current_teacher_attraction_enabled", False | |
| )): | |
| return {}, None, None | |
| local_error = None | |
| forward_inputs = None | |
| cache_records = None | |
| cache_log = None | |
| try: | |
| if self.current_teacher_source in { | |
| "matched_dual_feature_bank", | |
| "matched_triple_feature_bank", | |
| "matched_dinov3_vjepa2_feature_bank", | |
| }: | |
| if self.current_teacher_dataset is None: | |
| raise RuntimeError("matched teacher bank is unavailable") | |
| sample_ids = self._batch_string_list( | |
| batch.get("sample_ids"), key="sample_ids", | |
| batch_size=batch_size) | |
| features = self.current_teacher_dataset.load_rows(sample_ids) | |
| registry = getattr(self.model, "dual_branch_registry", None) | |
| if isinstance(registry, Mapping): | |
| expected_shapes = { | |
| name: tuple(branch.get( | |
| "raw_sample_shape", branch["sample_shape"])) | |
| for name, branch in registry.items() | |
| } | |
| else: | |
| expected_shapes = getattr( | |
| self.model.latent_mmd, "sample_shapes", {}) | |
| if set(features) != set(expected_shapes): | |
| raise ValueError("teacher bank/model feature branches differ") | |
| for name, tensor in features.items(): | |
| expected = (batch_size, *expected_shapes[name]) | |
| if tensor.dtype != torch.float16 or tuple(tensor.shape) != expected: | |
| raise ValueError( | |
| f"teacher bank {name} row shape/dtype mismatch: " | |
| f"{tuple(tensor.shape)}/{tensor.dtype} != " | |
| f"{expected}/float16") | |
| features[name] = tensor.to( | |
| device=self.device, dtype=torch.float32) | |
| forward_inputs = {"current_teacher_features": features} | |
| cache_records = () | |
| cache_log = { | |
| "hits": batch_size, | |
| "misses": 0, | |
| "decode_seconds": 0.0, | |
| "bank_rows_read": batch_size, | |
| } | |
| elif self.current_teacher_source == "lmdb_latent": | |
| latent = batch.get("ode_latent") | |
| if ( | |
| not isinstance(latent, torch.Tensor) | |
| or latent.ndim != 5 | |
| or int(latent.shape[0]) != batch_size | |
| or not latent.is_floating_point() | |
| ): | |
| raise ValueError( | |
| "exact LMDB current-teacher MMD requires one terminal " | |
| "latent per prompt with shape " | |
| "[B,frames,channels,height,width]") | |
| latent = latent.to(device=self.device, dtype=self.dtype) | |
| if not bool(torch.isfinite(latent).all().item()): | |
| raise FloatingPointError( | |
| "matched current teacher latent contains non-finite " | |
| "values") | |
| forward_inputs = {"current_teacher_latent": latent} | |
| elif self.current_teacher_source in { | |
| "openvid_video", "matched_manifest_video" | |
| }: | |
| features = batch.get("current_teacher_features") | |
| video01 = batch.get("current_teacher_video01") | |
| if (features is None) == (video01 is None): | |
| raise ValueError( | |
| "video teacher batch must provide exactly one of cached " | |
| "current_teacher_features or current_teacher_video01") | |
| cache_hit = batch.get("current_teacher_cache_hit") | |
| if ( | |
| not isinstance(cache_hit, torch.Tensor) | |
| or cache_hit.dtype != torch.bool | |
| or tuple(cache_hit.shape) != (batch_size,) | |
| ): | |
| raise ValueError( | |
| "current_teacher_cache_hit must be BoolTensor[B]") | |
| sample_ids = self._batch_string_list( | |
| batch.get("sample_ids"), key="sample_ids", | |
| batch_size=batch_size) | |
| cache_keys = self._batch_string_list( | |
| batch.get("teacher_cache_keys"), | |
| key="teacher_cache_keys", batch_size=batch_size) | |
| decode_value = batch.get("current_teacher_decode_seconds", 0.0) | |
| if isinstance(decode_value, torch.Tensor): | |
| decode_seconds = float( | |
| decode_value.detach().float().sum().item()) | |
| else: | |
| decode_seconds = float(decode_value) | |
| if not np.isfinite(decode_seconds) or decode_seconds < 0.0: | |
| raise ValueError( | |
| "current_teacher_decode_seconds must be finite and " | |
| "non-negative") | |
| if features is not None: | |
| expected_shape = ( | |
| batch_size, | |
| int(self.model.dense_num_tokens), | |
| int(self.model.dense_feature_dim), | |
| ) | |
| if ( | |
| not isinstance(features, torch.Tensor) | |
| or not features.is_floating_point() | |
| or tuple(features.shape) != expected_shape | |
| or not bool(cache_hit.all().item()) | |
| ): | |
| raise ValueError( | |
| "cached video teacher features must be floating " | |
| f"{expected_shape} with every cache-hit flag set") | |
| features = features.to( | |
| device=self.device, dtype=torch.float32) | |
| if not bool(torch.isfinite(features).all().item()): | |
| raise FloatingPointError( | |
| "cached video teacher features are non-finite") | |
| forward_inputs = { | |
| "current_teacher_features": features} | |
| cache_hits = batch_size | |
| cache_records = () | |
| else: | |
| expected_prefix = ( | |
| batch_size, | |
| int(self.model.dense_num_frames), | |
| 3, | |
| ) | |
| if ( | |
| not isinstance(video01, torch.Tensor) | |
| or not video01.is_floating_point() | |
| or video01.ndim != 5 | |
| or tuple(video01.shape[:3]) != expected_prefix | |
| or bool(cache_hit.any().item()) | |
| ): | |
| raise ValueError( | |
| "uncached video teacher video must be floating " | |
| "[B,64,3,H,W] with every cache-hit flag clear") | |
| if ( | |
| not bool(torch.isfinite(video01).all().item()) | |
| or float(video01.min().item()) < 0.0 | |
| or float(video01.max().item()) > 1.0 | |
| ): | |
| raise ValueError( | |
| "current_teacher_video01 must contain finite " | |
| "values in [0,1]") | |
| video01 = video01.to( | |
| device=self.device, dtype=torch.float32) | |
| forward_inputs = { | |
| "current_teacher_video01": video01} | |
| cache_hits = 0 | |
| cache_records = tuple(zip(cache_keys, sample_ids)) | |
| cache_log = { | |
| "hits": cache_hits, | |
| "misses": batch_size - cache_hits, | |
| "decode_seconds": decode_seconds, | |
| } | |
| else: | |
| raise RuntimeError( | |
| "enabled exact current teacher has no configured source") | |
| except BaseException as exc: | |
| local_error = exc | |
| if not self._all_ranks_true(local_error is None): | |
| message = ( | |
| f": {type(local_error).__name__}: {local_error}" | |
| if local_error is not None else " on another rank") | |
| raise RuntimeError( | |
| "current teacher batch validation failed" + message | |
| ) from local_error | |
| return forward_inputs, cache_records, cache_log | |
| def _write_current_teacher_cache(self, records, features): | |
| if not records: | |
| return | |
| if self.current_teacher_dataset is None: | |
| raise RuntimeError("live-teacher feature cache writer is unavailable") | |
| cpu_features = features.detach().float().cpu() | |
| if int(cpu_features.shape[0]) != len(records): | |
| raise ValueError( | |
| "teacher feature count does not match cache-miss records") | |
| for (cache_key, sample_id), feature in zip(records, cpu_features): | |
| self.current_teacher_dataset.write_feature_cache( | |
| cache_key, feature, sample_id=sample_id) | |
| def _encode_fresh_video_text_conditioning( | |
| self, | |
| text_prompts, | |
| batch_size, | |
| ): | |
| """Encode prompts and keep the joint-feature mask out-of-band.""" | |
| if len(text_prompts) != int(batch_size): | |
| raise ValueError( | |
| "text prompt count must match the fresh microbatch size") | |
| joint_enabled = bool(getattr( | |
| self.model, "triple_t5_joint_enabled", False)) | |
| conditional_dict = self.model.text_encoder( | |
| text_prompts=text_prompts, | |
| **( | |
| {"return_attention_mask": True} | |
| if joint_enabled else {} | |
| ), | |
| ) | |
| t5_attention_mask = conditional_dict.pop( | |
| "prompt_attention_mask", None) | |
| if joint_enabled: | |
| if not isinstance(t5_attention_mask, torch.Tensor): | |
| raise RuntimeError( | |
| "triple joint training requires the text encoder to " | |
| "return prompt_attention_mask") | |
| prompt_embeds = conditional_dict.get("prompt_embeds") | |
| if ( | |
| not isinstance(prompt_embeds, torch.Tensor) | |
| or t5_attention_mask.shape != prompt_embeds.shape[:2] | |
| ): | |
| raise ValueError( | |
| "prompt_attention_mask must match prompt_embeds [B,S]") | |
| if t5_attention_mask.device != prompt_embeds.device: | |
| raise ValueError( | |
| "prompt_attention_mask and prompt_embeds must share " | |
| "a device") | |
| elif t5_attention_mask is not None: | |
| raise RuntimeError( | |
| "pure-video text encoding unexpectedly returned an " | |
| "attention mask") | |
| cached_unconditional = getattr( | |
| self, "unconditional_dict", None) | |
| cached_batch = ( | |
| next(iter(cached_unconditional.values())).shape[0] | |
| if cached_unconditional else None | |
| ) | |
| if cached_unconditional is None or cached_batch != batch_size: | |
| unconditional_dict = self.model.text_encoder( | |
| text_prompts=[ | |
| self.config.negative_prompt] * batch_size) | |
| unconditional_dict = { | |
| key: value.detach() | |
| for key, value in unconditional_dict.items() | |
| } | |
| self.unconditional_dict = unconditional_dict | |
| else: | |
| unconditional_dict = cached_unconditional | |
| return conditional_dict, unconditional_dict, t5_attention_mask | |
| def _collect_rollout_microbatch( | |
| self, | |
| batch, | |
| *, | |
| gradient_selected=True, | |
| vjepa2_window_sample_keys=None, | |
| ): | |
| """Collect generated features and replay inputs without a generator graph. | |
| Reference minibatch rows are attached only to selected differentiation | |
| samples; all generated features still enter the distribution loss.""" | |
| text_prompts = batch["prompts"] | |
| batch_size = len(text_prompts) | |
| sample_ids = ( | |
| self._batch_string_list( | |
| batch.get("sample_ids"), key="sample_ids", | |
| batch_size=batch_size) | |
| if batch.get("sample_ids") is not None | |
| else None | |
| ) | |
| if ( | |
| gradient_selected | |
| and self.current_teacher_source == "matched_manifest_video" | |
| ): | |
| self._attach_matched_teacher_payload(batch) | |
| if gradient_selected: | |
| teacher_forward_inputs, cache_records, teacher_cache_log = ( | |
| self._prepare_current_teacher_inputs(batch, batch_size)) | |
| else: | |
| teacher_forward_inputs, cache_records, teacher_cache_log = ( | |
| {}, None, None) | |
| # Transfer ownership out of the DataLoader batch before model entry. | |
| # Each tensor is popped from teacher_forward_inputs as the call binds | |
| # it, so the model can release its final reference before decoding the | |
| # student and avoid a teacher+student pixel-memory peak. | |
| for key in ( | |
| "current_teacher_features", | |
| "current_teacher_video01", | |
| "ode_latent", | |
| ): | |
| batch.pop(key, None) | |
| image_or_video_shape = list(self.config.image_or_video_shape) | |
| image_or_video_shape[0] = batch_size | |
| first_pass_started = time.perf_counter() | |
| with torch.no_grad(): | |
| text_conditioning_error = None | |
| conditional_dict = None | |
| unconditional_dict = None | |
| t5_attention_mask = None | |
| try: | |
| ( | |
| conditional_dict, | |
| unconditional_dict, | |
| t5_attention_mask, | |
| ) = self._encode_fresh_video_text_conditioning( | |
| text_prompts, | |
| batch_size, | |
| ) | |
| except BaseException as exc: | |
| text_conditioning_error = exc | |
| if not self._all_ranks_true(text_conditioning_error is None): | |
| message = ( | |
| f": {type(text_conditioning_error).__name__}: " | |
| f"{text_conditioning_error}" | |
| if text_conditioning_error is not None | |
| else " on another rank" | |
| ) | |
| raise RuntimeError( | |
| "fresh text conditioning failed" + message | |
| ) from text_conditioning_error | |
| features, replay_descriptor = ( | |
| self.model.collect_rollout_features( | |
| image_or_video_shape=image_or_video_shape, | |
| conditional_dict=conditional_dict, | |
| unconditional_dict=unconditional_dict, | |
| t5_attention_mask=t5_attention_mask, | |
| initial_latent=None, | |
| vjepa2_window_sample_keys=( | |
| vjepa2_window_sample_keys), | |
| require_current_teacher=bool(gradient_selected), | |
| current_teacher_features=teacher_forward_inputs.pop( | |
| "current_teacher_features", None), | |
| current_teacher_video01=teacher_forward_inputs.pop( | |
| "current_teacher_video01", None), | |
| current_teacher_latent=teacher_forward_inputs.pop( | |
| "current_teacher_latent", None), | |
| ) | |
| ) | |
| first_pass_seconds = time.perf_counter() - first_pass_started | |
| current_teacher_features = replay_descriptor.pop( | |
| "current_teacher_features", None) | |
| teacher_extract_seconds = replay_descriptor.pop( | |
| "current_teacher_extract_seconds", None) | |
| if ( | |
| gradient_selected | |
| and bool(getattr( | |
| self.model, "current_teacher_attraction_enabled", False)) | |
| and current_teacher_features is None | |
| ): | |
| raise RuntimeError( | |
| "fresh first pass omitted matched current teacher features") | |
| cache_write_error = None | |
| try: | |
| if cache_records: | |
| self._write_current_teacher_cache( | |
| cache_records, current_teacher_features) | |
| except BaseException as exc: | |
| cache_write_error = exc | |
| if not self._all_ranks_true(cache_write_error is None): | |
| message = ( | |
| f": {type(cache_write_error).__name__}: {cache_write_error}" | |
| if cache_write_error is not None else " on another rank") | |
| raise RuntimeError( | |
| "live-teacher feature cache write failed" + message | |
| ) from cache_write_error | |
| # The call-site pops every mutually exclusive input while binding it; | |
| # retaining anything here would defeat the teacher payload handoff. | |
| if teacher_forward_inputs: | |
| raise RuntimeError( | |
| "unconsumed current teacher forward input remained") | |
| if teacher_cache_log is not None: | |
| teacher_cache_log["first_pass_seconds"] = first_pass_seconds | |
| if teacher_extract_seconds is not None: | |
| teacher_cache_log["extract_seconds"] = float( | |
| teacher_extract_seconds) | |
| entry = { | |
| "features": features, | |
| "replay_descriptor": replay_descriptor, | |
| "image_or_video_shape": image_or_video_shape, | |
| "conditional_dict": conditional_dict, | |
| "unconditional_dict": unconditional_dict, | |
| "gradient_selected": bool(gradient_selected), | |
| "sample_ids": sample_ids, | |
| } | |
| if current_teacher_features is not None: | |
| entry["current_teacher_features"] = current_teacher_features | |
| if teacher_cache_log is not None: | |
| entry["current_teacher_cache_log"] = teacher_cache_log | |
| return entry | |
| def _fresh_video_window_log(self, entries, reference): | |
| if getattr( | |
| self.model, | |
| "vjepa2_dense_frame_sampling", | |
| "center_contiguous", | |
| ) != "random_contiguous_per_video": | |
| return {} | |
| num_starts = ( | |
| int(self.model.vjepa2_dense_source_num_frames) | |
| - int(self.model.vjepa2_dense_num_frames) | |
| + 1 | |
| ) | |
| local_error = None | |
| local_starts = None | |
| try: | |
| start_parts = [] | |
| for entry in entries: | |
| indices = entry["replay_descriptor"][ | |
| "vjepa2_frame_indices"] | |
| if ( | |
| not isinstance(indices, torch.Tensor) | |
| or indices.ndim != 2 | |
| or int(indices.shape[1]) | |
| != int(self.model.vjepa2_dense_num_frames) | |
| ): | |
| raise ValueError( | |
| "invalid V-JEPA2 frame indices in GradCache entry") | |
| start_parts.append(indices[:, 0].to( | |
| device=reference.device, dtype=torch.long)) | |
| local_starts = torch.cat(start_parts) | |
| if int(local_starts.numel()) != self.fresh_video_local_samples: | |
| raise ValueError( | |
| "V-JEPA2 window count does not match local samples") | |
| if ( | |
| int(local_starts.min().item()) < 0 | |
| or int(local_starts.max().item()) >= num_starts | |
| ): | |
| raise ValueError("V-JEPA2 window start is out of range") | |
| except BaseException as exc: | |
| local_error = exc | |
| if not self._all_ranks_true(local_error is None): | |
| message = ( | |
| f": {type(local_error).__name__}: {local_error}" | |
| if local_error is not None | |
| else " on another rank" | |
| ) | |
| raise RuntimeError( | |
| "V-JEPA2 window logging validation failed" + message | |
| ) from local_error | |
| histogram = torch.bincount( | |
| local_starts, minlength=num_starts)[:num_starts] | |
| if dist.is_initialized() and self.world_size > 1: | |
| dist.all_reduce(histogram, op=dist.ReduceOp.SUM) | |
| if int(histogram.sum().item()) != self.fresh_video_total_samples: | |
| raise RuntimeError( | |
| "global V-JEPA2 window histogram has an invalid sample count") | |
| used = histogram.nonzero(as_tuple=False).flatten() | |
| if used.numel() == 0: | |
| raise RuntimeError("global V-JEPA2 window histogram is empty") | |
| return { | |
| "vjepa2_dense_window_start_min": used[0], | |
| "vjepa2_dense_window_start_max": used[-1], | |
| "vjepa2_dense_window_unique_starts": used.new_tensor( | |
| int(used.numel())), | |
| "vjepa2_dense_source_num_frames": used.new_tensor( | |
| int(self.model.vjepa2_dense_source_num_frames)), | |
| } | |
| def _validate_partial_mmd_geometry(self, result): | |
| return None | |
| def _run_distribution_matching_step(self): | |
| """Perform one distribution-matching update (paper Sec. 3.3). | |
| Collect B generated representations, differentiate the feature objective | |
| for b selected rollouts, then replay encoders, VAE and generator. The | |
| released selection and loss conventions are documented in paper-to-code.md.""" | |
| # Frozen encoders stay in eval mode; generator gradients are enabled only on replay. | |
| self.model.eval() | |
| dense_log_prefix = getattr( | |
| self.model, "dense_log_prefix", "vjepa2_dense") | |
| def dense_key(suffix): | |
| return f"{dense_log_prefix}_{suffix}" | |
| if self.step % 20 == 0: | |
| torch.cuda.empty_cache() | |
| # This cache lasts one optimizer update; it is not a history of older rollouts. | |
| cache = self.fresh_video_gradcache | |
| if cache is None: | |
| raise RuntimeError("fresh video GradCache is not initialized") | |
| if cache.local_samples or cache.finalized: | |
| raise RuntimeError( | |
| "fresh video GradCache leaked state across optimizer updates") | |
| if ( | |
| hasattr(self.model, "distribution_state_pending") | |
| and self.model.distribution_state_pending() | |
| ): | |
| raise RuntimeError( | |
| "dynamic distribution state leaked across optimizer updates") | |
| # This consensus must happen before the first model forward because | |
| # enabled ranks enter a pixel-cache collective inside that forward. | |
| use_prepared_replay = bool( | |
| self.uses_fresh_video_gradcache | |
| and self.model.fresh_video_prepared_replay_enabled | |
| ) | |
| all_use_prepared = self._all_ranks_true(use_prepared_replay) | |
| none_use_prepared = self._all_ranks_true( | |
| not use_prepared_replay) | |
| if not (all_use_prepared or none_use_prepared): | |
| raise RuntimeError( | |
| "fresh-video prepared replay setting differs across ranks") | |
| use_prepared_replay = all_use_prepared | |
| entries = [] | |
| try: | |
| for microbatch_index in range(self.fresh_video_microbatches): | |
| window_sample_keys = None | |
| if getattr( | |
| self.model, | |
| "vjepa2_dense_frame_sampling", | |
| "center_contiguous", | |
| ) == "random_contiguous_per_video": | |
| local_start = ( | |
| microbatch_index * int(self.config.batch_size)) | |
| global_start = ( | |
| dist.get_rank() * self.fresh_video_local_samples | |
| + local_start | |
| ) | |
| update_start = ( | |
| int(self.step) * self.fresh_video_total_samples) | |
| window_sample_keys = tuple( | |
| update_start + global_start + offset | |
| for offset in range(int(self.config.batch_size)) | |
| ) | |
| gradient_selected = bool( | |
| microbatch_index >= ( | |
| self.fresh_video_microbatches | |
| - self.fresh_video_generator_grad_microbatches) | |
| ) | |
| batch = ( | |
| self._matched_batch_for_microbatch(microbatch_index) | |
| if getattr( | |
| self, "matched_counter_sampling_enabled", False) | |
| else self._next_batch() | |
| ) | |
| entry = self._collect_rollout_microbatch( | |
| batch, | |
| gradient_selected=gradient_selected, | |
| vjepa2_window_sample_keys=window_sample_keys, | |
| ) | |
| add_error = None | |
| try: | |
| student_ids = entry.get("sample_ids") | |
| if hasattr(cache, "partial_gradient_enabled"): | |
| cache.add( | |
| entry["features"], | |
| entry.get("current_teacher_features"), | |
| gradient_selected=gradient_selected, | |
| sample_ids=student_ids, | |
| current_teacher_sample_ids=( | |
| student_ids | |
| if entry.get( | |
| "current_teacher_features") is not None | |
| else None | |
| ), | |
| ) | |
| else: | |
| # Preserve the original one-argument cache protocol | |
| # used by legacy trainer fixtures and adapters. | |
| cache.add(entry["features"]) | |
| entry.pop("current_teacher_features", None) | |
| except BaseException as exc: | |
| add_error = exc | |
| if not self._all_ranks_true(add_error is None): | |
| message = ( | |
| f": {type(add_error).__name__}: {add_error}" | |
| if add_error is not None | |
| else " on another rank" | |
| ) | |
| raise RuntimeError( | |
| "fresh-video GradCache add failed" + message | |
| ) from add_error | |
| if not gradient_selected: | |
| descriptor = entry.get("replay_descriptor", {}) | |
| descriptor.pop("cached_student_video01", None) | |
| entry.pop("conditional_dict", None) | |
| entry.pop("unconditional_dict", None) | |
| entry.pop("image_or_video_shape", None) | |
| entries.append(entry) | |
| if ( | |
| use_prepared_replay | |
| and self.fresh_video_generator_grad_subsample_enabled | |
| and microbatch_index < ( | |
| self.fresh_video_microbatches | |
| - self.fresh_video_generator_grad_microbatches) | |
| ): | |
| # These feature values participate in the full MMD scalar, | |
| # but no generator graph will be built for them. Drop the | |
| # update-local decoded pixel cache immediately instead of | |
| # retaining fourteen unnecessary videos on host memory. | |
| entry["replay_descriptor"].pop( | |
| "cached_student_video01", None) | |
| # Differentiate the global feature-space loss before constructing generator graphs. | |
| result = cache.finalize() | |
| if ( | |
| self.fresh_video_generator_grad_subsample_enabled | |
| and hasattr(cache, "partial_gradient_enabled") | |
| ): | |
| self._validate_partial_mmd_geometry(result) | |
| window_log = self._fresh_video_window_log(entries, result.loss) | |
| video_cotangents = result.fsdp_cotangents | |
| expected_replay_start = ( | |
| len(entries) | |
| - self.fresh_video_generator_grad_microbatches | |
| ) | |
| expected_replay_indices = tuple( | |
| range(expected_replay_start, len(entries))) | |
| replay_indices = tuple(getattr( | |
| result, | |
| "replay_batch_indices", | |
| expected_replay_indices, | |
| )) | |
| if replay_indices != expected_replay_indices: | |
| raise RuntimeError( | |
| "GradCache replay indices are not the configured suffix: " | |
| f"{replay_indices} != {expected_replay_indices}") | |
| replay_entries = tuple(entries[index] for index in replay_indices) | |
| if ( | |
| not replay_entries | |
| or len(replay_entries) | |
| != self.fresh_video_generator_grad_microbatches | |
| or ( | |
| hasattr(cache, "partial_gradient_enabled") | |
| and any( | |
| not entry.get("gradient_selected", False) | |
| for entry in replay_entries | |
| ) | |
| ) | |
| ): | |
| raise RuntimeError( | |
| "generator-gradient replay microbatch selection is invalid") | |
| if len(video_cotangents) == len(replay_indices): | |
| replay_video_cotangents = tuple(video_cotangents) | |
| elif len(video_cotangents) == len(entries): | |
| replay_video_cotangents = tuple( | |
| video_cotangents[index] for index in replay_indices) | |
| else: | |
| raise RuntimeError( | |
| "fresh GradCache cotangent/replay count mismatch") | |
| # Apply B/b selection compensation once; FSDP averaging has a | |
| # separate factor. | |
| replay_video_cotangents = _scale_replay_video_cotangents( | |
| replay_video_cotangents, | |
| self.fresh_video_generator_grad_sample_scale, | |
| ) | |
| # Replay frozen encoder/VAE VJPs before generator activations. | |
| prepared_replays = [None] * len(replay_entries) | |
| if use_prepared_replay: | |
| prepared_replays = [ | |
| self.model.prepare_latent_gradients( | |
| replay_descriptor=entry["replay_descriptor"], | |
| expected_features=entry["features"], | |
| feature_cotangent=cotangent, | |
| replay_atol=float(getattr( | |
| self.config, "fresh_video_replay_atol", 1e-5)), | |
| replay_rtol=float(getattr( | |
| self.config, "fresh_video_replay_rtol", 1e-4)), | |
| ) | |
| for entry, cotangent in zip( | |
| replay_entries, replay_video_cotangents) | |
| ] | |
| replay_logs = [] | |
| for entry, cotangent, prepared_replay in zip( | |
| replay_entries, | |
| replay_video_cotangents, | |
| prepared_replays, | |
| ): | |
| surrogate, replay_log = self.model.replay_generator_gradients( | |
| replay_descriptor=entry["replay_descriptor"], | |
| expected_features=entry["features"], | |
| feature_cotangent=cotangent, | |
| image_or_video_shape=entry["image_or_video_shape"], | |
| conditional_dict=entry["conditional_dict"], | |
| unconditional_dict=entry["unconditional_dict"], | |
| initial_latent=None, | |
| prepared_replay=prepared_replay, | |
| replay_atol=float(getattr( | |
| self.config, "fresh_video_replay_atol", 1e-5)), | |
| replay_rtol=float(getattr( | |
| self.config, "fresh_video_replay_rtol", 1e-4)), | |
| ) | |
| surrogate.backward() | |
| replay_logs.append(replay_log) | |
| generator_grad_norm = self.model.generator.clip_grad_norm_( | |
| self.max_grad_norm_generator) | |
| log_dict = { | |
| key: torch.stack([ | |
| item[key].detach().float() for item in replay_logs | |
| ]).mean() | |
| for key in replay_logs[0] | |
| } | |
| meta = result.metadata | |
| generator_loss = result.optimized_loss | |
| multilayer_metadata = meta.get("layers") | |
| displayed_mmd = ( | |
| meta["raw_mmd_sum"] | |
| if isinstance(multilayer_metadata, dict) | |
| else result.loss | |
| ) | |
| log_dict.update({ | |
| "generator_loss": generator_loss, | |
| "generator_grad_norm": generator_grad_norm, | |
| "dmdtrain_gradient_norm": log_dict[ | |
| "combined_latent_grad_norm"], | |
| dense_key("mmd"): displayed_mmd, | |
| dense_key("optimized_mmd"): result.optimized_loss, | |
| dense_key("exact_repulsion"): meta["repulsion"], | |
| dense_key("nystrom_attraction"): meta["attraction"], | |
| dense_key("nystrom_attraction_weight"): meta.get( | |
| "attraction_weight", result.loss.new_ones(())), | |
| dense_key("reference_constant"): meta[ | |
| "reference_constant"], | |
| dense_key("local_video_samples"): meta[ | |
| "gradcache_local_samples"], | |
| dense_key("global_video_samples"): meta[ | |
| "gradcache_global_samples"], | |
| dense_key("gradient_local_video_samples"): meta.get( | |
| "gradcache_gradient_local_samples", | |
| result.loss.new_tensor( | |
| self.fresh_video_generator_grad_local_samples, | |
| dtype=torch.long, | |
| ), | |
| ), | |
| dense_key("gradient_global_video_samples"): meta.get( | |
| "gradcache_gradient_global_samples", | |
| result.loss.new_tensor( | |
| self.fresh_video_generator_grad_global_samples, | |
| dtype=torch.long, | |
| ), | |
| ), | |
| dense_key("current_teacher_global_samples"): meta.get( | |
| "gradcache_current_teacher_global_samples", | |
| result.loss.new_zeros((), dtype=torch.long), | |
| ), | |
| dense_key("kss_denominator"): meta.get( | |
| "gradcache_kss_denominator", | |
| result.loss.new_tensor( | |
| self.fresh_video_total_samples ** 2, | |
| dtype=torch.long, | |
| ), | |
| ), | |
| dense_key("fresh_microbatches"): result.loss.new_tensor( | |
| len(entries), dtype=torch.long), | |
| dense_key("replay_microbatches"): result.loss.new_tensor( | |
| len(replay_entries), dtype=torch.long), | |
| dense_key("raw_tokens"): result.loss.new_tensor( | |
| getattr( | |
| self.model, | |
| "dense_raw_tokens", | |
| self.model.vjepa2_dense_raw_tokens, | |
| ), dtype=torch.long), | |
| dense_key("sampled_tokens"): result.loss.new_tensor( | |
| getattr( | |
| self.model, | |
| "dense_num_tokens", | |
| self.model.vjepa2_dense_num_tokens, | |
| ), dtype=torch.long), | |
| dense_key("feature_dim"): result.loss.new_tensor( | |
| getattr( | |
| self.model, | |
| "dense_feature_dim", | |
| self.model.vjepa2_dense_feature_dim, | |
| ), dtype=torch.long), | |
| dense_key("num_frames"): result.loss.new_tensor( | |
| getattr( | |
| self.model, | |
| "dense_num_frames", | |
| self.model.vjepa2_dense_num_frames, | |
| ), dtype=torch.long), | |
| dense_key("layer"): result.loss.new_tensor( | |
| getattr( | |
| self.model, | |
| "dense_layer", | |
| self.model.vjepa2_dense_layer, | |
| ), dtype=torch.long), | |
| dense_key("prefix_blocks"): result.loss.new_tensor( | |
| getattr( | |
| self.model, | |
| "dense_layer", | |
| self.model.vjepa2_dense_layer, | |
| ) + 1, dtype=torch.long), | |
| dense_key("nystrom_landmarks"): result.loss.new_tensor( | |
| self.model.latent_mmd.landmarks.shape[0], | |
| dtype=torch.long), | |
| dense_key("reference_samples"): result.loss.new_tensor( | |
| self.model.vjepa2_nystrom_reference_count, | |
| dtype=torch.long), | |
| "fresh_video_optimizer_step_skipped": ( | |
| result.loss.new_zeros((), dtype=torch.long)), | |
| "fresh_video_generator_grad_sample_scale": ( | |
| result.loss.new_tensor( | |
| self.fresh_video_generator_grad_sample_scale)), | |
| "fresh_video_generator_effective_replay_scale": ( | |
| result.loss.new_tensor( | |
| self.fresh_video_generator_grad_sample_scale | |
| * float(getattr(self, "world_size", 1)))), | |
| "fresh_video_generator_grad_subsample_enabled": ( | |
| result.loss.new_tensor( | |
| int(self.fresh_video_generator_grad_subsample_enabled), | |
| dtype=torch.long, | |
| ) | |
| ), | |
| "fresh_video_generator_grad_local_samples": ( | |
| result.loss.new_tensor( | |
| self.fresh_video_generator_grad_local_samples, | |
| dtype=torch.long, | |
| ) | |
| ), | |
| "fresh_video_generator_grad_global_samples": ( | |
| result.loss.new_tensor( | |
| self.fresh_video_generator_grad_global_samples, | |
| dtype=torch.long, | |
| ) | |
| ), | |
| "fresh_video_generator_grad_microbatches": ( | |
| result.loss.new_tensor( | |
| self.fresh_video_generator_grad_microbatches, | |
| dtype=torch.long, | |
| ) | |
| ), | |
| }) | |
| if isinstance(meta.get("branches"), Mapping): | |
| for branch_name, branch in meta["branches"].items(): | |
| prefix = f"{branch_name}_dense" | |
| log_dict.update({ | |
| f"{prefix}_raw_mmd": branch["raw_mmd"], | |
| f"{prefix}_normalized_mmd": branch["normalized_mmd"], | |
| f"{prefix}_normalization_denominator": ( | |
| branch["normalization_denominator"]), | |
| f"{prefix}_exact_repulsion": branch["repulsion"], | |
| f"{prefix}_fixed_teacher_attraction": ( | |
| branch["fixed_teacher_attraction"]), | |
| f"{prefix}_current_teacher_attraction": ( | |
| branch["current_teacher_attraction"]), | |
| f"{prefix}_reference_constant": ( | |
| branch["reference_constant"]), | |
| f"{prefix}_bandwidth": branch["sigma"], | |
| f"{prefix}_kss_denominator": ( | |
| branch["repulsion_denominator"]), | |
| f"{prefix}_fixed_attraction_denominator": ( | |
| branch["fixed_teacher_attraction_denominator"]), | |
| f"{prefix}_teacher_cross_denominator": ( | |
| branch["current_teacher_attraction_denominator"]), | |
| f"{prefix}_teacher_self_denominator": ( | |
| branch["current_teacher_self_kernel_denominator"]), | |
| }) | |
| log_dict["dual_dense_normalized_mmd_sum"] = meta[ | |
| "normalized_mmd_sum"] | |
| teacher_cache_logs = [ | |
| entry["current_teacher_cache_log"] | |
| for entry in entries | |
| if "current_teacher_cache_log" in entry | |
| ] | |
| if teacher_cache_logs: | |
| cache_stats = result.loss.new_tensor([ | |
| sum(item["hits"] for item in teacher_cache_logs), | |
| sum(item["misses"] for item in teacher_cache_logs), | |
| sum(item["decode_seconds"] for item in teacher_cache_logs), | |
| sum(item["first_pass_seconds"] for item in teacher_cache_logs), | |
| len(teacher_cache_logs), | |
| sum(item.get("extract_seconds", 0.0) | |
| for item in teacher_cache_logs), | |
| sum("extract_seconds" in item | |
| for item in teacher_cache_logs), | |
| ], dtype=torch.float64) | |
| if dist.is_initialized() and self.world_size > 1: | |
| dist.all_reduce(cache_stats, op=dist.ReduceOp.SUM) | |
| total_items = cache_stats[0] + cache_stats[1] | |
| teacher_metric_prefix = ( | |
| "triple_bank" | |
| if self.current_teacher_source == "matched_triple_feature_bank" | |
| else ("dinov3_vjepa2_bank" | |
| if self.current_teacher_source | |
| == "matched_dinov3_vjepa2_feature_bank" | |
| else ("dual_bank" | |
| if self.current_teacher_source == "matched_dual_feature_bank" | |
| else ( | |
| "matched" | |
| if self.current_teacher_source == "matched_manifest_video" | |
| else "openvid" | |
| ))) | |
| ) | |
| log_dict.update({ | |
| f"{teacher_metric_prefix}_teacher_cache_hits": cache_stats[0], | |
| f"{teacher_metric_prefix}_teacher_cache_misses": cache_stats[1], | |
| f"{teacher_metric_prefix}_teacher_cache_hit_rate": ( | |
| cache_stats[0] / total_items.clamp_min(1.0)), | |
| f"{teacher_metric_prefix}_teacher_decode_seconds": cache_stats[2], | |
| f"{teacher_metric_prefix}_teacher_first_pass_seconds_per_microbatch": ( | |
| cache_stats[3] / cache_stats[4].clamp_min(1.0)), | |
| }) | |
| if float(cache_stats[6].item()) > 0: | |
| log_dict[ | |
| f"{teacher_metric_prefix}_teacher_extract_seconds_per_microbatch" | |
| ] = cache_stats[5] / cache_stats[6] | |
| if "current_attraction" in meta: | |
| log_dict.update({ | |
| dense_key("nystrom_current_attraction"): ( | |
| meta["current_attraction"]), | |
| dense_key("nystrom_attraction_ema_updates"): ( | |
| meta["attraction_ema_updates"]), | |
| dense_key("nystrom_attraction_current_coefficient"): ( | |
| meta["attraction_current_coefficient"]), | |
| }) | |
| if "current_teacher_attraction" in meta: | |
| log_dict.update({ | |
| dense_key("fixed_teacher_attraction"): ( | |
| meta["fixed_teacher_attraction"]), | |
| dense_key("current_teacher_attraction"): ( | |
| meta["current_teacher_attraction"]), | |
| dense_key("mixed_current_attraction"): ( | |
| meta["mixed_current_attraction"]), | |
| dense_key("fixed_teacher_normalized_weight"): ( | |
| meta[ | |
| "fixed_teacher_attraction_normalized_weight"]), | |
| dense_key("current_teacher_normalized_weight"): ( | |
| meta[ | |
| "current_teacher_attraction_normalized_weight"]), | |
| dense_key("current_teacher_self_kernel"): ( | |
| meta["current_teacher_self_kernel"]), | |
| dense_key("current_teacher_exact_mmd"): ( | |
| meta["current_teacher_exact_mmd"]), | |
| dense_key("current_teacher_samples"): ( | |
| meta["current_teacher_samples"]), | |
| }) | |
| if "attraction_ema_blend_current_coefficient" in meta: | |
| log_dict.update({ | |
| dense_key("attraction_ema_blend_coefficient"): ( | |
| meta[ | |
| "attraction_ema_blend_current_coefficient"]), | |
| dense_key("attraction_gradient_coefficient"): ( | |
| meta[ | |
| "attraction_gradient_current_coefficient"]), | |
| dense_key("effective_ema_loss"): ( | |
| meta["effective_ema_loss"]), | |
| }) | |
| if "repulsion_fifo_enabled" in meta: | |
| log_dict.update({ | |
| dense_key("repulsion_fifo_capacity"): ( | |
| meta["repulsion_fifo_capacity"]), | |
| dense_key("repulsion_fifo_fill_before"): ( | |
| meta["repulsion_fifo_fill_before"]), | |
| dense_key("repulsion_fifo_fill_after_commit"): ( | |
| meta["repulsion_fifo_fill_after_commit"]), | |
| dense_key("repulsion_fifo_commits_before"): ( | |
| meta["repulsion_fifo_commits"]), | |
| dense_key("repulsion_current_offdiag_kernel_sum"): ( | |
| meta["repulsion_current_offdiag_kernel_sum"]), | |
| dense_key("repulsion_history_kernel_sum"): ( | |
| meta["repulsion_history_kernel_sum"]), | |
| dense_key("repulsion_denominator"): ( | |
| meta["repulsion_denominator"]), | |
| }) | |
| if isinstance(multilayer_metadata, dict): | |
| log_dict.update({ | |
| "vjepa2_dense_multilayer_normalized_sum": result.loss, | |
| "vjepa2_dense_multilayer_weighted_sum": ( | |
| result.optimized_loss), | |
| "vjepa2_dense_num_layers": meta["num_layers"], | |
| "vjepa2_dense_sampled_tokens_per_layer": ( | |
| result.loss.new_tensor( | |
| self.model.vjepa2_dense_num_tokens, | |
| dtype=torch.long, | |
| ) | |
| ), | |
| "vjepa2_dense_gradcache_transport_tokens": ( | |
| result.loss.new_tensor( | |
| self.model.vjepa2_dense_num_tokens | |
| * len(self.model.vjepa2_dense_layers), | |
| dtype=torch.long, | |
| ) | |
| ), | |
| "vjepa2_dense_self_normalized": ( | |
| meta["self_normalized"]), | |
| "vjepa2_dense_normalization_epsilon": ( | |
| meta["normalization_epsilon"]), | |
| "vjepa2_dense_layer_min": result.loss.new_tensor( | |
| min(self.model.vjepa2_dense_layers), | |
| dtype=torch.long, | |
| ), | |
| "vjepa2_dense_layer_max": result.loss.new_tensor( | |
| max(self.model.vjepa2_dense_layers), | |
| dtype=torch.long, | |
| ), | |
| }) | |
| for layer in self.model.vjepa2_dense_layers: | |
| layer_meta = multilayer_metadata[str(layer)] | |
| prefix = f"vjepa2_dense_l{layer}" | |
| log_dict.update({ | |
| f"{prefix}_mmd": layer_meta["raw_mmd"], | |
| f"{prefix}_normalized_mmd": ( | |
| layer_meta["normalized_mmd"]), | |
| f"{prefix}_weighted_mmd": ( | |
| layer_meta["weighted_mmd"]), | |
| f"{prefix}_repulsion": ( | |
| layer_meta["repulsion"]), | |
| f"{prefix}_nystrom_attraction": ( | |
| layer_meta["attraction"]), | |
| f"{prefix}_reference_constant": ( | |
| layer_meta["reference_constant"]), | |
| f"{prefix}_bandwidth": layer_meta["sigma"], | |
| }) | |
| if "current_attraction" in layer_meta: | |
| log_dict.update({ | |
| f"{prefix}_nystrom_current_attraction": ( | |
| layer_meta["current_attraction"]), | |
| f"{prefix}_attraction_ema_updates": ( | |
| layer_meta["attraction_ema_updates"]), | |
| f"{prefix}_attraction_current_coefficient": ( | |
| layer_meta[ | |
| "attraction_current_coefficient"]), | |
| }) | |
| elif not isinstance(meta.get("branches"), Mapping): | |
| log_dict[dense_key("bandwidth")] = meta["sigma"] | |
| log_dict.update(window_log) | |
| return log_dict | |
| except FloatingPointError as exc: | |
| cache.discard() | |
| if hasattr(self.model, "discard_distribution_state"): | |
| self.model.discard_distribution_state() | |
| if self.is_main_process: | |
| print( | |
| "[warn] discarding complete fresh-video update after " | |
| f"non-finite value: {exc}", | |
| flush=True, | |
| ) | |
| nonfinite = torch.tensor( | |
| float("nan"), device=self.device, dtype=torch.float32) | |
| return { | |
| "generator_loss": nonfinite, | |
| "generator_grad_norm": nonfinite, | |
| "dmdtrain_gradient_norm": nonfinite, | |
| dense_key("update_discarded"): nonfinite.new_ones(()), | |
| } | |
| except BaseException: | |
| cache.discard() | |
| if hasattr(self.model, "discard_distribution_state"): | |
| self.model.discard_distribution_state() | |
| raise | |
| finally: | |
| # Prepared-replay pixels are update-local. Never let a decoded | |
| # batch leak into the next update after success or failure. | |
| for entry in entries: | |
| descriptor = entry.get("replay_descriptor", {}) | |
| if isinstance(descriptor, dict): | |
| descriptor.pop("cached_student_video01", None) | |
| def fwdbwd_one_step(self, batch, train_generator): | |
| # Frozen encoders stay in eval mode; generator gradients are enabled only on replay. | |
| self.model.eval() # prevent any randomness (e.g. dropout) | |
| if self.step % 20 == 0: | |
| torch.cuda.empty_cache() | |
| # Step 1: Get the next batch of text prompts | |
| text_prompts = batch["prompts"] | |
| if self.config.distribution_loss == "wan_kv_mmd": | |
| teacher_trajectory = batch["ode_latent"] | |
| local_shape_ok = teacher_trajectory.ndim == 6 | |
| if not self._all_ranks_true(local_shape_ok): | |
| raise ValueError( | |
| "wan_kv_mmd teacher LMDB yielded an invalid shape on at " | |
| "least one rank; expected " | |
| "[B, steps, frames, channels, height, width]") | |
| clean_latent = teacher_trajectory[:, -1].to( | |
| device=self.device, dtype=self.dtype) | |
| local_finite = bool(torch.isfinite(clean_latent).all().item()) | |
| if not self._all_ranks_true(local_finite): | |
| raise FloatingPointError( | |
| "teacher latent contains non-finite values on at least one rank") | |
| image_latent = None | |
| elif self.config.i2v: | |
| clean_latent = None | |
| image_latent = batch["ode_latent"][:, -1][:, 0:1, ].to( | |
| device=self.device, dtype=self.dtype) | |
| else: | |
| clean_latent = None | |
| image_latent = None | |
| batch_size = len(text_prompts) | |
| image_or_video_shape = list(self.config.image_or_video_shape) | |
| image_or_video_shape[0] = batch_size | |
| # Step 2: Extract the conditional infos | |
| with torch.no_grad(): | |
| conditional_dict = self.model.text_encoder( | |
| text_prompts=text_prompts) | |
| if not getattr(self, "unconditional_dict", None): | |
| unconditional_dict = self.model.text_encoder( | |
| text_prompts=[self.config.negative_prompt] * batch_size) | |
| unconditional_dict = {k: v.detach() | |
| for k, v in unconditional_dict.items()} | |
| self.unconditional_dict = unconditional_dict # cache the unconditional_dict | |
| else: | |
| unconditional_dict = self.unconditional_dict | |
| # Step 3: Store gradients for the generator (if training the generator) | |
| if train_generator: | |
| generator_loss, generator_log_dict = self.model.generator_loss( | |
| image_or_video_shape=image_or_video_shape, | |
| conditional_dict=conditional_dict, | |
| unconditional_dict=unconditional_dict, | |
| clean_latent=clean_latent, | |
| initial_latent=image_latent if self.config.i2v else None | |
| ) | |
| generator_loss.backward() | |
| generator_grad_norm = self.model.generator.clip_grad_norm_( | |
| self.max_grad_norm_generator) | |
| generator_log_dict.update({"generator_loss": generator_loss, | |
| "generator_grad_norm": generator_grad_norm}) | |
| return generator_log_dict | |
| else: | |
| generator_log_dict = {} | |
| # Step 4: Store gradients for the critic (if training the critic) | |
| critic_loss, critic_log_dict = self.model.critic_loss( | |
| image_or_video_shape=image_or_video_shape, | |
| conditional_dict=conditional_dict, | |
| unconditional_dict=unconditional_dict, | |
| clean_latent=clean_latent, | |
| initial_latent=image_latent if self.config.i2v else None | |
| ) | |
| critic_loss.backward() | |
| critic_grad_norm = self.model.fake_score.clip_grad_norm_( | |
| self.max_grad_norm_critic) | |
| critic_log_dict.update({"critic_loss": critic_loss, | |
| "critic_grad_norm": critic_grad_norm}) | |
| return critic_log_dict | |
| def generate_video(self, pipeline, prompts, image=None): | |
| batch_size = len(prompts) | |
| if image is not None: | |
| image = image.squeeze(0).unsqueeze(0).unsqueeze(2).to(device="cuda", dtype=torch.bfloat16) | |
| # Encode the input image as the first latent | |
| initial_latent = pipeline.vae.encode_to_latent(image).to(device="cuda", dtype=torch.bfloat16) | |
| initial_latent = initial_latent.repeat(batch_size, 1, 1, 1, 1) | |
| sampled_noise = torch.randn( | |
| [batch_size, self.model.num_training_frames - 1, 16, 60, 104], | |
| device="cuda", | |
| dtype=self.dtype | |
| ) | |
| else: | |
| initial_latent = None | |
| sampled_noise = torch.randn( | |
| [batch_size, self.model.num_training_frames, 16, 60, 104], | |
| device="cuda", | |
| dtype=self.dtype | |
| ) | |
| video, _ = pipeline.inference( | |
| noise=sampled_noise, | |
| text_prompts=prompts, | |
| return_latents=True, | |
| initial_latent=initial_latent | |
| ) | |
| current_video = video.permute(0, 1, 3, 4, 2).cpu().numpy() * 255.0 | |
| return current_video | |
| def train(self): | |
| start_step = self.step | |
| max_steps = int(getattr(self.config, "max_steps", 0)) | |
| if max_steps > 0 and self.step >= max_steps: | |
| if self.is_main_process: | |
| print( | |
| f"[done] checkpoint step {self.step} already reached " | |
| f"max_steps={max_steps}; nothing to train.", | |
| flush=True, | |
| ) | |
| return | |
| # baseline eval before any optimization (captures the init / resumed model) | |
| if self.eval_interval > 0: | |
| self._fd_eval_inference() | |
| while True: | |
| # ``self.step`` is the last committed/public step. Apply the LR | |
| # for the update about to produce the next completed step. | |
| self._active_generator_lr = self._set_generator_learning_rate( | |
| self.step + 1) | |
| TRAIN_GENERATOR = ( | |
| (not self.uses_critic) | |
| or self.step % self.config.dfake_gen_update_ratio == 0 | |
| ) | |
| update_committed = True | |
| # Train the generator | |
| if TRAIN_GENERATOR: | |
| self.generator_optimizer.zero_grad(set_to_none=True) | |
| if self.uses_fresh_video_gradcache: | |
| generator_log_dict = ( | |
| self._run_distribution_matching_step()) | |
| else: | |
| extras_list = [] | |
| batch = self._next_batch() | |
| extra = self.fwdbwd_one_step(batch, True) | |
| extras_list.append(extra) | |
| generator_log_dict = merge_dict_list(extras_list) | |
| # Skip the step if the generator gradient is non-finite (avoids | |
| # corrupting weights from a stray bf16 overflow); the gradients are | |
| # zeroed at the top of the next iteration. | |
| _ggn = generator_log_dict.get("generator_grad_norm", None) | |
| _local_ggn_ok = ( | |
| (_ggn is None) | |
| or bool(torch.isfinite( | |
| torch.as_tensor(_ggn).float()).all().item()) | |
| ) | |
| _ggn_ok = self._all_ranks_true(_local_ggn_ok) | |
| if _ggn_ok: | |
| prepare_error = None | |
| try: | |
| if hasattr( | |
| self.model, "prepare_distribution_state" | |
| ): | |
| self.model.prepare_distribution_state() | |
| except Exception as exc: | |
| prepare_error = exc | |
| prepare_ok = self._all_ranks_true( | |
| prepare_error is None) | |
| if not prepare_ok: | |
| if self.fresh_video_gradcache is not None: | |
| self.fresh_video_gradcache.discard() | |
| if hasattr(self.model, "discard_distribution_state"): | |
| self.model.discard_distribution_state() | |
| if prepare_error is not None: | |
| raise RuntimeError( | |
| "distribution transaction preparation failed " | |
| "before optimizer.step") from prepare_error | |
| raise RuntimeError( | |
| "distribution transaction preparation failed on " | |
| "another rank before optimizer.step") | |
| try: | |
| # Commit after every selected rollout contributed its gradient. | |
| self.generator_optimizer.step() | |
| if hasattr(self.model, "commit_distribution_state"): | |
| self.model.commit_distribution_state() | |
| if self.fresh_video_gradcache is not None: | |
| self.fresh_video_gradcache.commit() | |
| except BaseException: | |
| if self.fresh_video_gradcache is not None: | |
| self.fresh_video_gradcache.discard() | |
| if hasattr(self.model, "discard_distribution_state"): | |
| self.model.discard_distribution_state() | |
| raise | |
| update_committed = True | |
| if self.generator_ema is not None: | |
| self.generator_ema.update(self.model.generator) | |
| else: | |
| update_committed = False | |
| if self.fresh_video_gradcache is not None: | |
| self.fresh_video_gradcache.discard() | |
| if hasattr(self.model, "discard_distribution_state"): | |
| self.model.discard_distribution_state() | |
| if self.is_main_process: | |
| print(f"[warn] step {self.step}: non-finite generator_grad_norm={_ggn}; skipping optimizer step", flush=True) | |
| # Train the critic | |
| critic_log_dict = {} | |
| if self.uses_critic: | |
| self.critic_optimizer.zero_grad(set_to_none=True) | |
| extras_list = [] | |
| batch = self._next_batch() | |
| extra = self.fwdbwd_one_step(batch, False) | |
| extras_list.append(extra) | |
| critic_log_dict = merge_dict_list(extras_list) | |
| self.critic_optimizer.step() | |
| # Increment the step since we finished gradient update | |
| self.step += 1 | |
| # Create EMA params (if not already created) | |
| ema_start_step = int(getattr(self.config, "ema_start_step", 0)) | |
| ema_weight = float(getattr(self.config, "ema_weight", 0.0) or 0.0) | |
| if (self.step >= ema_start_step) and \ | |
| (self.generator_ema is None) and (ema_weight > 0): | |
| self.generator_ema = EMA_FSDP( | |
| self.model.generator, decay=ema_weight) | |
| # Save the model | |
| if (not self.config.no_save) and (self.step - start_step) > 0 and self.step % self.config.log_iters == 0: | |
| torch.cuda.empty_cache() | |
| self.save() | |
| torch.cuda.empty_cache() | |
| # Periodic eval inference (all ranks participate; rank 0 logs/saves). | |
| # Remember whether this step was evaluated so max_steps does not | |
| # write the same fixed-seed samples twice. | |
| did_periodic_eval = ( | |
| self.eval_interval > 0 | |
| and (self.step - start_step) > 0 | |
| and self.step % self.eval_interval == 0 | |
| ) | |
| if did_periodic_eval: | |
| torch.cuda.empty_cache() | |
| self._fd_eval_inference() | |
| torch.cuda.empty_cache() | |
| # Logging | |
| if self.is_main_process: | |
| wandb_loss_dict = {} | |
| if TRAIN_GENERATOR: | |
| wandb_loss_dict.update( | |
| { | |
| "generator_loss": generator_log_dict["generator_loss"].mean().item(), | |
| "generator_grad_norm": generator_log_dict["generator_grad_norm"].mean().item(), | |
| "dmdtrain_gradient_norm": generator_log_dict["dmdtrain_gradient_norm"].mean().item(), | |
| "generator_lr": float(self._active_generator_lr), | |
| } | |
| ) | |
| for key, value in generator_log_dict.items(): | |
| if key in wandb_loss_dict or key in { | |
| "generator_loss", | |
| "generator_grad_norm", | |
| "dmdtrain_gradient_norm", | |
| }: | |
| continue | |
| if isinstance(value, torch.Tensor) and value.numel() == 1: | |
| # Metadata includes integral counters (for example | |
| # Wan-KV history_updates). ``Tensor.mean`` rejects | |
| # integer dtypes; a one-element tensor needs no | |
| # reduction. | |
| wandb_loss_dict[key] = value.detach().item() | |
| if self.uses_critic: | |
| wandb_loss_dict.update( | |
| { | |
| "critic_loss": critic_log_dict["critic_loss"].mean().item(), | |
| "critic_grad_norm": critic_log_dict["critic_grad_norm"].mean().item() | |
| } | |
| ) | |
| if not self.disable_wandb: | |
| wandb.log(wandb_loss_dict, step=self.step) | |
| if (self.step % getattr(self.config, "stdout_log_iters", 1)) == 0: | |
| summary = " ".join( | |
| f"{k}={v:.4g}" for k, v in wandb_loss_dict.items() | |
| if isinstance(v, (int, float)) | |
| ) | |
| print(f"[step {self.step}] {summary}", flush=True) | |
| if self.step % self.config.gc_interval == 0: | |
| if dist.get_rank() == 0: | |
| logging.info("DistGarbageCollector: Running GC.") | |
| gc.collect() | |
| torch.cuda.empty_cache() | |
| if self.is_main_process: | |
| current_time = time.time() | |
| if self.previous_time is None: | |
| self.previous_time = current_time | |
| else: | |
| if not self.disable_wandb: | |
| wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step) | |
| self.previous_time = current_time | |
| # Stop only after logging the final step. This is used by smoke tests | |
| # and sequential launchers, so suppressing its metrics would hide | |
| # precisely the EMA transition they need to verify. | |
| max_steps = int(getattr(self.config, "max_steps", 0)) | |
| if max_steps > 0 and self.step >= max_steps: | |
| if not self.config.no_save: | |
| torch.cuda.empty_cache() | |
| self.save() | |
| torch.cuda.empty_cache() | |
| if self.eval_interval > 0 and not did_periodic_eval: | |
| self._fd_eval_inference() | |
| if self.is_main_process: | |
| print( | |
| f"[done] reached max_steps={max_steps}; exiting.", | |
| flush=True, | |
| ) | |
| break | |
| # Historical entry points remain aliases for external callers. | |
| _prepare_fresh_video_microbatch = _collect_rollout_microbatch | |
| _fresh_video_generator_update = _run_distribution_matching_step | |
| # Old import/class spelling remains valid for existing callers. | |
| Trainer = DistributionMatchingTrainer | |