Skip to content

Commit 3a130c5

Browse files
committed
Lazily materialize camera-length video inputs
1 parent 41ec7e8 commit 3a130c5

4 files changed

Lines changed: 334 additions & 36 deletions

File tree

src/solarwm/backends/wan22/runtime/inference.py

Lines changed: 178 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,31 @@ class _MaterializedCandidate:
7474
noise_seed: int
7575
start_adjusted: bool
7676
pass_rollouts: Mapping[str, int]
77-
prepared: _PreparedCase
77+
source_pixel_frames: int
78+
camera_fingerprint: str
79+
prepared: _PreparedCase | None
80+
81+
82+
@dataclass(frozen=True)
83+
class _DeferredCameraCase:
84+
row: IndexRow
85+
sample_plan: SamplePlan
86+
noise_seed: int
87+
pass_rollouts: Mapping[str, int]
88+
source_pixel_frames: int
89+
prompt: str
90+
camera_fingerprint: str
91+
92+
93+
@dataclass
94+
class _DeferredCameraInputs:
95+
rows: tuple[IndexRow, ...]
96+
resolver: Any
97+
runtime_guards: CameraGuards
98+
manifest_guards: CameraGuards
99+
cases: dict[int, _DeferredCameraCase]
100+
shards: TarShardReader | None = None
101+
reader: RawSampleReader | None = None
78102

79103

80104
class _FamilyAdapter:
@@ -600,6 +624,7 @@ def __init__(self, config: Mapping[str, Any], plan: GenerationPlan) -> None:
600624
self.vae.to(self.device)
601625
self._loaded_role: str | None = None
602626
self._prepared: dict[int, _PreparedCase] = {}
627+
self._deferred_camera_inputs: _DeferredCameraInputs | None = None
603628

604629
def sync(self) -> None:
605630
import torch.distributed as dist
@@ -610,7 +635,91 @@ def sync(self) -> None:
610635
def close(self) -> None:
611636
from .distributed import cleanup_torchrun
612637

613-
cleanup_torchrun()
638+
try:
639+
self._close_deferred_camera_inputs()
640+
finally:
641+
cleanup_torchrun()
642+
643+
def _close_deferred_camera_inputs(self) -> None:
644+
deferred = getattr(self, "_deferred_camera_inputs", None)
645+
if deferred is None:
646+
return
647+
self._deferred_camera_inputs = None
648+
try:
649+
if deferred.shards is not None:
650+
deferred.shards.close()
651+
finally:
652+
for slot in deferred.cases:
653+
self._prepared.pop(slot, None)
654+
deferred.cases.clear()
655+
deferred.reader = None
656+
deferred.shards = None
657+
658+
def _materialize_deferred_camera_case(self, case: InferenceCase) -> None:
659+
deferred = getattr(self, "_deferred_camera_inputs", None)
660+
if deferred is None:
661+
raise BackendContractError("Wan adapter has no deferred camera inputs")
662+
descriptor = deferred.cases.get(case.slot)
663+
if descriptor is None:
664+
raise BackendContractError(
665+
f"Wan adapter has no deferred camera case {case.sample_id!r}"
666+
)
667+
if self._prepared:
668+
raise BackendContractError("Wan adapter retained another prepared camera case")
669+
metadata = case.metadata
670+
if (
671+
descriptor.row.sample_id != case.sample_id
672+
or descriptor.sample_plan.start_frame != case.start_frame
673+
or descriptor.noise_seed != case.noise_seed
674+
or descriptor.prompt != case.prompt
675+
or descriptor.camera_fingerprint != case.camera_fingerprint
676+
or descriptor.row.key != str(metadata.get("key", ""))
677+
or descriptor.row.shard != str(metadata.get("source_shard", ""))
678+
or descriptor.row.ordinal != int(metadata.get("source_row_ordinal", -1))
679+
or descriptor.source_pixel_frames != int(metadata.get("source_pixel_frames", -1))
680+
or descriptor.sample_plan.source_frame_indices[-1]
681+
!= int(metadata.get("source_frame_last", -1))
682+
or dict(descriptor.pass_rollouts)
683+
!= dict(metadata.get("rollout_latent_frames_by_pass", {}))
684+
):
685+
raise DataContractError(
686+
f"deferred camera case {case.sample_id!r} identity drifted"
687+
)
688+
if deferred.reader is None:
689+
deferred.shards = TarShardReader(
690+
deferred.resolver,
691+
max_open=int(self.config["data"].get("tar_cache_size", 4)),
692+
)
693+
deferred.reader = RawSampleReader(deferred.rows, deferred.shards)
694+
raw = deferred.reader.materialize(descriptor.sample_plan)
695+
camera_fingerprint = hashlib.blake2s(raw.members["camera_member"]).hexdigest()
696+
if raw.caption != case.prompt or camera_fingerprint != case.camera_fingerprint:
697+
raise DataContractError(
698+
f"deferred camera case {case.sample_id!r} payload drifted"
699+
)
700+
data = self.config["data"]
701+
camera = build_camera_tokens(
702+
raw.members["camera_member"],
703+
descriptor.sample_plan.source_frame_indices,
704+
raw.manifest,
705+
source_fps=_source_fps(descriptor.row),
706+
output_fps=float(data.get("fps", 16.0)),
707+
frame_sequence_length=int(self.config["model"]["frame_sequence_length"]),
708+
guards=deferred.runtime_guards,
709+
manifest_guards=deferred.manifest_guards,
710+
configured_array_key=str(data["camera_array_key"]),
711+
)
712+
pixels = decode_video(
713+
raw.members["video_member"],
714+
descriptor.sample_plan.source_frame_indices,
715+
height=int(data["height"]),
716+
width=int(data["width"]),
717+
)
718+
self._prepared[case.slot] = _PreparedCase(
719+
pixels=pixels,
720+
camera=camera,
721+
source_pixel_frames=descriptor.source_pixel_frames,
722+
)
614723

615724
def weight_id(self, role: str) -> str:
616725
if role not in {"live", "ema"}:
@@ -669,6 +778,11 @@ def _load_role(self, role: str) -> None:
669778
def build_cases(self, plan: GenerationPlan) -> tuple[InferenceCase, ...]:
670779
data = self.config["data"]
671780
validation = self.config["validation"]
781+
camera_length = (
782+
str(self.config.get("inference", {}).get("length", "fixed")).strip().lower() == "camera"
783+
)
784+
if camera_length:
785+
self._close_deferred_camera_inputs()
672786
frozen_path_value = getattr(self, "validation_plan_path", None)
673787
frozen_path = Path(str(frozen_path_value)) if frozen_path_value is not None else None
674788
frozen_key = validation_plan_key("wan22", self.config)
@@ -724,9 +838,14 @@ def validation_guard(name: str) -> float | None:
724838
max_rel_translation=float(data["max_rel_translation"]),
725839
max_camera_abs=float(data["max_camera_abs"]),
726840
)
727-
camera_length = (
728-
str(self.config.get("inference", {}).get("length", "fixed")).strip().lower() == "camera"
729-
)
841+
if camera_length:
842+
self._deferred_camera_inputs = _DeferredCameraInputs(
843+
rows=tuple(source_rows),
844+
resolver=resolver,
845+
runtime_guards=runtime_guards,
846+
manifest_guards=manifest_guards,
847+
cases={},
848+
)
730849
variable_rollout = camera_length or any(
731850
item.variable_rollout_by_source for item in plan.passes
732851
)
@@ -742,6 +861,11 @@ def validation_guard(name: str) -> float | None:
742861
# Candidate rows retain full-index ordinals, so random access must
743862
# address the complete source index rather than the seeded order.
744863
reader = RawSampleReader(source_rows, shards)
864+
camera_reader = RawSampleReader(
865+
source_rows,
866+
shards,
867+
member_fields=("camera_member",),
868+
)
745869

746870
def materialize_candidate(row: IndexRow, slot: int) -> _MaterializedCandidate:
747871
pass_rollouts = {
@@ -765,13 +889,15 @@ def materialize_candidate(row: IndexRow, slot: int) -> _MaterializedCandidate:
765889
variable_rollout_by_source=variable_rollout,
766890
start_at_first_frame=camera_length,
767891
)
768-
raw = reader.materialize(sample_plan)
769-
pixels = decode_video(
770-
raw.members["video_member"],
771-
sample_plan.source_frame_indices,
772-
height=int(data["height"]),
773-
width=int(data["width"]),
774-
)
892+
raw = (camera_reader if camera_length else reader).materialize(sample_plan)
893+
pixels = None
894+
if not camera_length:
895+
pixels = decode_video(
896+
raw.members["video_member"],
897+
sample_plan.source_frame_indices,
898+
height=int(data["height"]),
899+
width=int(data["width"]),
900+
)
775901
camera = build_camera_tokens(
776902
raw.members["camera_member"],
777903
sample_plan.source_frame_indices,
@@ -789,13 +915,44 @@ def materialize_candidate(row: IndexRow, slot: int) -> _MaterializedCandidate:
789915
noise_seed=noise_seed,
790916
start_adjusted=start_adjusted,
791917
pass_rollouts=pass_rollouts,
792-
prepared=_PreparedCase(
793-
pixels=pixels,
794-
camera=camera,
795-
source_pixel_frames=source_pixel_frames,
918+
source_pixel_frames=source_pixel_frames,
919+
camera_fingerprint=hashlib.blake2s(
920+
raw.members["camera_member"]
921+
).hexdigest(),
922+
prepared=(
923+
None
924+
if camera_length
925+
else _PreparedCase(
926+
pixels=pixels,
927+
camera=camera,
928+
source_pixel_frames=source_pixel_frames,
929+
)
796930
),
797931
)
798932

933+
def retain_candidate(
934+
row: IndexRow,
935+
slot: int,
936+
materialized: _MaterializedCandidate,
937+
) -> None:
938+
if camera_length:
939+
deferred = self._deferred_camera_inputs
940+
if deferred is None or materialized.prepared is not None:
941+
raise BackendContractError("Wan deferred camera state is invalid")
942+
deferred.cases[slot] = _DeferredCameraCase(
943+
row=row,
944+
sample_plan=materialized.sample_plan,
945+
noise_seed=materialized.noise_seed,
946+
pass_rollouts=dict(materialized.pass_rollouts),
947+
source_pixel_frames=materialized.source_pixel_frames,
948+
prompt=str(materialized.raw.caption),
949+
camera_fingerprint=materialized.camera_fingerprint,
950+
)
951+
return
952+
if materialized.prepared is None:
953+
raise BackendContractError("Wan fixed validation case lacks pixels")
954+
self._prepared[slot] = materialized.prepared
955+
799956
if frozen_cases is not None:
800957
restored: list[InferenceCase] = []
801958
for case in frozen_cases:
@@ -821,21 +978,18 @@ def materialize_candidate(row: IndexRow, slot: int) -> _MaterializedCandidate:
821978
raise DataContractError(
822979
f"frozen validation sample {case.sample_id!r} changed camera validity"
823980
) from exc
824-
camera_fingerprint = hashlib.blake2s(
825-
materialized.raw.members["camera_member"]
826-
).hexdigest()
827981
if (
828982
materialized.sample_plan.start_frame != case.start_frame
829983
or materialized.noise_seed != case.noise_seed
830984
or materialized.raw.caption != case.prompt
831-
or camera_fingerprint != case.camera_fingerprint
985+
or materialized.camera_fingerprint != case.camera_fingerprint
832986
or dict(materialized.pass_rollouts)
833987
!= dict(case.metadata.get("rollout_latent_frames_by_pass", {}))
834988
):
835989
raise DataContractError(
836990
f"frozen validation sample {case.sample_id!r} materialization drifted"
837991
)
838-
self._prepared[case.slot] = materialized.prepared
992+
retain_candidate(row, case.slot, materialized)
839993
restored.append(case)
840994
return tuple(restored)
841995

@@ -930,17 +1084,15 @@ def materialize_candidate(row: IndexRow, slot: int) -> _MaterializedCandidate:
9301084
prompt=raw.caption,
9311085
start_frame=sample_plan.start_frame,
9321086
noise_seed=materialized.noise_seed,
933-
camera_fingerprint=hashlib.blake2s(
934-
raw.members["camera_member"]
935-
).hexdigest(),
1087+
camera_fingerprint=materialized.camera_fingerprint,
9361088
metadata={
9371089
"key": row.key,
9381090
"source_shard": row.shard,
9391091
"source_row_ordinal": row.ordinal,
9401092
"dataset": _row_dataset(row),
9411093
"scene": row.values.get("scene"),
9421094
"source_num_frames": _num_frames(row),
943-
"source_pixel_frames": materialized.prepared.source_pixel_frames,
1095+
"source_pixel_frames": materialized.source_pixel_frames,
9441096
"train_latent_frames": int(data["latent_frames"]),
9451097
"rollout_latent_frames_by_pass": materialized.pass_rollouts,
9461098
**({"rollout_length_source": "camera"} if camera_length else {}),
@@ -955,7 +1107,7 @@ def materialize_candidate(row: IndexRow, slot: int) -> _MaterializedCandidate:
9551107
},
9561108
)
9571109
)
958-
self._prepared[slot] = materialized.prepared
1110+
retain_candidate(row, slot, materialized)
9591111
local_cases = tuple(cases)
9601112
if frozen_path is not None:
9611113
all_cases = _gather_partitioned_cases(local_cases)

src/solarwm/backends/wan22/runtime/stage2.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2298,7 +2298,8 @@ def __init__(self, values: Mapping[str, Any], generation_plan: Any) -> None:
22982298
self.text_encoder.to(self.device)
22992299
self.vae.to(self.device)
23002300
self._loaded_role: str | None = None
2301-
self._prepared: dict[str, Any] = {}
2301+
self._prepared: dict[int, Any] = {}
2302+
self._deferred_camera_inputs = None
23022303

23032304
@staticmethod
23042305
def _root(module: Any) -> Any:
@@ -2343,7 +2344,22 @@ def generate(self, case: Any, *, weights_id: str) -> Any:
23432344
"Stage2 standalone generation weights identity drift"
23442345
)
23452346
self._load_role(role)
2346-
return _stage2_generated_sample(self, case, weights_id=weights_id)
2347+
camera_length = (
2348+
str(self.config.get("inference", {}).get("length", "fixed"))
2349+
.strip()
2350+
.lower()
2351+
== "camera"
2352+
)
2353+
deferred = self._deferred_camera_inputs
2354+
if camera_length and deferred is None:
2355+
raise BackendContractError("Stage2 camera-length inputs were not prepared")
2356+
if not camera_length:
2357+
return _stage2_generated_sample(self, case, weights_id=weights_id)
2358+
self._materialize_deferred_camera_case(case)
2359+
try:
2360+
return _stage2_generated_sample(self, case, weights_id=weights_id)
2361+
finally:
2362+
self._prepared.pop(case.slot, None)
23472363

23482364
return _Adapter(config, plan)
23492365

0 commit comments

Comments
 (0)