Files
hao-ai-lab-FastVideo/v2/training/weight_sync.py
T
SolitaryThinker 9489c6c1dd [feat] Five more stress tests: LTX-2 A/V, weight-sync, served reward, cache-dit, nested workflows
The remaining design_v3 probes (all except 8b speculative decoding). All fit with no new runtime
primitive (148 tests pass).

#6 LTX-2 joint audio+video (§9.11) — LTX-2 declared an audio_vae required_for t2vs but never used
   it; now a single 2-stage denoise carries a synchronized audio latent (conditioned on video),
   applies per-modality CFG (guidance_per_modality), and decodes via video VAE + audio VAE → video +
   audio. Gated on requesting audio, so the T2V path is byte-identical (existing tests untouched).
   ToyAudioVAE; build_ltx2_av_program. 5 tests.

#4 Live weight-sync under in-flight serving (§9.14) — WeightSyncController makes the freeze → drain →
   transfer → bump version + invalidate → resume lifecycle explicit. Tests: a mid-flight swap corrupts
   (the hazard); draining first leaves the in-flight request bit-identical to baseline while a
   post-sync request reflects new weights; transformer-only sync, so the frozen text-encoder cache
   survives. The RL flywheel's hardest correctness. 3 tests.

#5 Reward-model-as-a-served-card (§9.15) — a reward model is a card (scorer + a score loop emitting
   REWARD_BATCH units); ServedRewardScorer drop-in-replaces the numpy scorer so any RL method becomes
   RLHF/RLAIF with no method change. ToyRewardModel; models/reward/. 4 tests.

#7 Content-adaptive control flow (§9.12) — CacheDiTDenoiseLoop (isolated WanDenoiseLoop subclass)
   reuses the cached velocity when predictions barely change (cache-dit skip) and early-exits on
   convergence — variable step count; interleave parity holds across ragged loops. models/adaptive/. 4 tests.

#8a Nested workflows (§9.13) — a workflow stage can invoke another workflow (engine.run routes ids);
   requires/validate recurse; cycles caught at registration + a run-time guard (engine._wf_running).
   build_t2i_i2v_extend_workflow. 5 tests.

designv4 §9.11–§9.15 + falsifier/layout/closing updates (148 tests, 25 files). Also removed a
pre-existing unused import in ltx2/loop.py.
2026-07-04 17:15:34 +00:00

96 lines
4.9 KiB
Python

"""WeightSyncPlan — versioned weight transfer with a declared role (design_v3 §10).
> RL ships a *role*, not "the weights": student / EMA / decay-blended old policy is declared
> (the landed NFT behavior policy is the *old* copy, not the student — the plan must carry that).
The lifecycle (design_v3 §10): freeze admission → drain/boundary-stop → transfer → bump
``weights_version`` → invalidate incompatible caches/graphs → publish → resume. Computed from
three inputs (mesh specs + per-model layout adapters + transport), validated pre-flight,
CPU-testable on fake pools. Here the "transfer" copies/blends the toy DiT params.
"""
from __future__ import annotations
import itertools
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class WeightRole(str, Enum):
STUDENT = "student" # the current learnable weights
EMA = "ema" # exponential moving average of student
OLD_POLICY = "old_policy" # decay-blended behavior policy (NFT samples from THIS, not student)
REFERENCE = "reference" # frozen KL anchor
TEACHER = "teacher" # frozen distillation teacher
CRITIC = "critic" # trainable fake-score / value head
_ver = itertools.count(1)
@dataclass
class WeightSyncPlan:
role: WeightRole
# three inputs (design_v3 §10): mesh specs (trainer↔engine), layout adapters, transport
mesh_specs: dict[str, Any] = field(default_factory=dict)
layout_adapters: dict[str, Any] = field(default_factory=dict)
transport: str = "in_proc" # in_proc | shm | nccl | rdma
decay: float = 0.0 # for EMA / OLD_POLICY blend
# which component(s) this plan syncs — versioned + cache-invalidated in isolation (design_v3 §7.1).
# Joint RL (UniRL) ships ONE plan per trainable expert: ``("transformer",)`` and ``("llm",)`` —
# so the LM weight sync does NOT flush the frozen text-encoder's feature cache, and vice-versa.
components: tuple[str, ...] = ("transformer",)
def apply(self, src_dit: Any, dst_dit: Any, dst_instance: Any = None) -> str:
"""Execute the lifecycle. Returns the new weights_version published on dst_instance."""
# 1) freeze admission + 2) drain/boundary-stop are scheduler ops (no-op in single-thread mini)
# 3) transfer per role
if self.role in (WeightRole.EMA, WeightRole.OLD_POLICY):
dst_dit.blend_from(src_dit, self.decay) # decay·dst + (1-decay)·src
else:
dst_dit.copy_from(src_dit) # hard copy (student push / reference init)
# 4) bump version + 5) invalidate ONLY this plan's component caches/graphs + 6) publish.
# Scoped to ``self.components`` so frozen text/vision encoders keep their feature caches,
# and the two experts of a joint-RL update version independently (design_v3 §7.1, §10).
version = f"w{next(_ver)}"
if dst_instance is not None:
dst_instance.set_weights_version(version, components=list(self.components))
return version
class WeightSyncController:
"""The §10 hot weight-sync lifecycle over a *live, serving* resident instance, made explicit and
testable — the RL flywheel's hardest correctness surface (rollout while serving, swap weights, stay
correct):
freeze() admission → (drain in-flight loops to a boundary) → sync() transfer+publish → resume.
The load-bearing invariant: an in-flight request finishes on the weights it **started** with — the
sync waits for it to drain, so its trajectory is never a half-and-half of two policies, and its
captured behavior is stamped with that version (for correct off-policy correction). ``freeze`` gates
admission so no NEW request starts the synced loop mid-swap; the caller drains the in-flight set,
then ``sync`` transfers per-component (bump version + invalidate that component's caches only) and
resumes. This is the engine-side counterpart to a training ``WeightSyncPlan`` — same lifecycle.
"""
def __init__(self, instance: Any):
self.instance = instance
self.frozen = False
self.synced = 0
def freeze(self) -> None:
self.frozen = True
def can_admit(self) -> bool:
return not self.frozen # admission gate: no new starts of the synced loop while frozen
def sync(self, src_dit: Any, *, component_id: str = "transformer",
role: WeightRole = WeightRole.STUDENT) -> str:
"""Transfer new weights into the resident component, bump its version, invalidate ONLY its
caches, and resume admission. Call AFTER in-flight loops on this instance have drained."""
version = WeightSyncPlan(role=role, components=(component_id,)).apply(
src_dit, self.instance.component(component_id), self.instance)
self.frozen = False
self.synced += 1
return version