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.
66 lines
3.0 KiB
Python
66 lines
3.0 KiB
Python
"""Nested workflows — recursive cross-model composition (design_v3 §13; §9.13).
|
|
|
|
A workflow id is a servable, so a `WorkflowStage` can invoke a *workflow* exactly as it invokes a
|
|
model. These tests build a workflow whose first stage IS the T2I→I2V workflow (then extends its video),
|
|
register it on an engine that already serves the inner workflow, and assert: it runs end-to-end;
|
|
`requires` lists the inner workflow; validation recurses; and a self-referencing workflow is rejected
|
|
by the cycle guard (no infinite recursion).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from v2.models import build_image_video_engine, build_t2i_i2v_extend_workflow
|
|
from v2.program import Workflow, WorkflowStage
|
|
from v2.request import TaskType, make_request
|
|
from v2.runtime import Engine
|
|
|
|
|
|
def test_nested_workflow_runs_end_to_end():
|
|
eng = build_image_video_engine() # serves flux-t2i, wan-i2v, AND image_video.t2i_i2v
|
|
outer = build_t2i_i2v_extend_workflow()
|
|
eng.register_workflow(outer)
|
|
out = outer.run(eng, prompt="a red fox in snow", seed=2)
|
|
frames = np.asarray(out.artifacts["video"].frames)
|
|
assert frames.ndim == 4 and frames.shape[1] > 1 # the extended (I2V) video
|
|
|
|
|
|
def test_nested_workflow_requires_the_inner_workflow():
|
|
outer = build_t2i_i2v_extend_workflow()
|
|
assert outer.requires == ["image_video.t2i_i2v", "wan-i2v"] # a workflow id among its deps
|
|
# registering on an engine that does NOT serve the inner workflow fails fast
|
|
with pytest.raises(ValueError, match="requires unregistered"):
|
|
Engine().register_workflow(outer)
|
|
|
|
|
|
def test_nested_workflow_addressable_by_id():
|
|
eng = build_image_video_engine()
|
|
eng.register_workflow(build_t2i_i2v_extend_workflow())
|
|
assert eng.serves("image_video.t2i_i2v_extend")
|
|
out = eng.run(make_request(TaskType.T2V, "image_video.t2i_i2v_extend", "a lighthouse"))
|
|
assert out.artifacts["video"].frames is not None # engine.run routes the OUTER workflow,
|
|
# which routes the INNER workflow as its stage 1
|
|
|
|
|
|
def _selfref():
|
|
return Workflow("image_video.loop", [WorkflowStage(
|
|
"image_video.loop", lambda s: make_request(TaskType.T2V, "image_video.loop", s["prompt"]))])
|
|
|
|
|
|
def test_self_reference_caught_at_registration():
|
|
"""A workflow that requires itself can't even register — `requires` includes its own (unserved) id.
|
|
This is the primary cycle prevention (declared deps, validated)."""
|
|
eng = build_image_video_engine()
|
|
with pytest.raises(ValueError, match="requires unregistered"):
|
|
eng.register_workflow(_selfref())
|
|
|
|
|
|
def test_runtime_cycle_guard_is_defense_in_depth():
|
|
"""Even if a cycle slipped past validation (here forced by inserting directly), the run-time guard
|
|
raises rather than recursing forever."""
|
|
eng = build_image_video_engine()
|
|
eng._workflows["image_video.loop"] = _selfref() # bypass validate() to simulate a cycle
|
|
with pytest.raises(ValueError, match="cycle"):
|
|
eng.run(make_request(TaskType.T2V, "image_video.loop", "x"))
|