[perf] Add Adaptive Guidance (CFG gating) for stale-uncond reuse - #1372
Conversation
Implements the LinearAG variant of Adaptive Guidance (Castillo et al. 2023,
arXiv:2312.12487, AAAI 2025): after a fraction of the denoising schedule
the unconditional CFG forward is skipped and the guidance prediction is
recovered from the cached delta = noise_pred_cond - noise_pred_uncond:
noise_pred = noise_pred_cond + (guidance_scale - 1) * delta_cached
This is the "simple affine transformation of past score estimates"
described in §LinearAG of the paper. The skipped forward is the full
DiT forward (attention + FFN + projections), so the saving scales with
the post-gate phase length.
Gated by a single env var `FASTVIDEO_CFG_GATE_STEP` (float [0, 1],
default 1.0 = no-op). Existing users see zero behavioural change unless
they opt in.
Measurements (same-container A/B, 5 fixed-seed prompts):
4x L40S sp=4 Wan T2V 1.3B, 30 steps, FASTVIDEO_CFG_GATE_STEP=0.5
end-to-end: 220.9 s -> 172.2 s per video (-22.0%)
DenoisingStage: ~178 s -> ~81 s per gen
peak memory: 23,308 MB -> 23,308 MB (no change)
VBench 5-dim: all metrics within +/-0.3% of baseline;
aesthetic_quality +2.78% on 5/5 prompts
1x H100 sp=1 Wan T2V 1.3B, 30 steps (cross-hardware confirmation)
FA2 + gate=1.0: 193.0 s baseline
FA2 + gate=0.5: 147.2 s (-23.7% e2e, +0 MB peak)
FA3 + gate=0.5: 95.06 s (composes multiplicatively with FA3
attention speedup; predicted from
(1-0.355)*(1-0.237) = 0.4922 * 193.0 = 95.0)
Safety guards:
- Input validation: raises on FASTVIDEO_CFG_GATE_STEP outside [0, 1].
- Wan2.2 expert-switch auto-invalidates delta_cached via
id(current_model) check.
- guidance_rescale > 0 + gating active emits one-shot warning on rank 0
(the combination is unvalidated).
- Telemetry counters (fresh_uncond, reused, invalidations) logged at
end of denoising loop on rank 0 so users can confirm the env var is
wired through.
Files changed (minimal vs upstream/main):
- fastvideo/envs.py: +26 lines, single new env var with usage docs.
- fastvideo/pipelines/stages/denoising.py: +127 / -18 lines, setup
block + per-step branch on _use_cached_delta + summary log. No other
behaviour changes; non-CFG-gating inference-loop touches from our
development branch were stripped before submission.
Note on naming: the same gating idea (split denoising at a gate step,
optimize the post-gate phase) is also explored in TGATE
(Liu et al. 2024, arXiv:2404.02747) but with a different mechanism
(cache attention outputs, both CFG forwards kept). This PR implements
the Adaptive Guidance mechanism (skip the uncond forward); the env var
name `FASTVIDEO_CFG_GATE_STEP` describes the action rather than naming
either upstream method.
Merge ProtectionsYour pull request matches the following merge protections and will not be merged until they are valid. 🔴 PR merge requirementsWaiting for
This rule is failing.
|
There was a problem hiding this comment.
Code Review
This pull request implements CFG gating (Adaptive Guidance / LinearAG) to optimize inference performance by skipping the unconditional forward pass and reusing cached guidance deltas after a specified fraction of steps. It introduces the FASTVIDEO_CFG_GATE_STEP environment variable and integrates the caching, invalidation, and telemetry logic into the denoising pipeline. The review feedback correctly identifies that the gating logic should rely on the actual number of iterations (len(timesteps)) rather than num_inference_steps to ensure compatibility with multi-step ODE solvers where the two values differ.
| _cfg_gate_step_idx = int(num_inference_steps * _cfg_gate_fraction) | ||
| if _is_rank0: | ||
| logger.info("CFG gating enabled: fraction=%.3f, gate_step=%d/%d", _cfg_gate_fraction, | ||
| _cfg_gate_step_idx, num_inference_steps) |
There was a problem hiding this comment.
The gating step calculation currently uses num_inference_steps, but the loop index i iterates over the full timesteps sequence. For schedulers where self.scheduler.order > 1 (e.g., multi-step ODE solvers), len(timesteps) is typically a multiple of num_inference_steps. In these cases, the gating will trigger much earlier than the intended fraction of the denoising process.
Using len(timesteps) for both the calculation and the log message ensures the gating fraction is applied correctly across all model evaluations.
| _cfg_gate_step_idx = int(num_inference_steps * _cfg_gate_fraction) | |
| if _is_rank0: | |
| logger.info("CFG gating enabled: fraction=%.3f, gate_step=%d/%d", _cfg_gate_fraction, | |
| _cfg_gate_step_idx, num_inference_steps) | |
| _cfg_gate_step_idx = int(len(timesteps) * _cfg_gate_fraction) | |
| if _is_rank0: | |
| logger.info("CFG gating enabled: fraction=%.3f, gate_step=%d/%d", _cfg_gate_fraction, | |
| _cfg_gate_step_idx, len(timesteps)) |
| "quality may degrade beyond CFG-gating-alone expectations.", _cfg_gate_fraction, | ||
| batch.guidance_rescale) | ||
| else: | ||
| _cfg_gate_step_idx = num_inference_steps + 1 # never gates |
There was a problem hiding this comment.
If gating is disabled, _cfg_gate_step_idx should be set relative to the actual number of iterations (len(timesteps)) to ensure the boundary checks inside the loop remain correct, especially for higher-order schedulers.
| _cfg_gate_step_idx = num_inference_steps + 1 # never gates | |
| _cfg_gate_step_idx = len(timesteps) + 1 # never gates |
| # default (FASTVIDEO_CFG_GATE_STEP=1.0, _cfg_gate_step_idx | ||
| # > num_inference_steps) we never reuse, so skip the | ||
| # tensor allocation. | ||
| if _cfg_gate_step_idx <= num_inference_steps: |
There was a problem hiding this comment.
| if _is_rank0 and batch.do_classifier_free_guidance: | ||
| logger.info( | ||
| "CFG gating summary: fraction=%.3f gate_step=%d/%d " | ||
| "fresh_uncond=%d reused=%d invalidations=%d", | ||
| _cfg_gate_fraction, | ||
| _cfg_gate_step_idx if _cfg_gate_active else -1, | ||
| num_inference_steps, | ||
| _cfg_gate_fresh_uncond, | ||
| _cfg_gate_reused_delta, | ||
| _cfg_gate_invalidations, | ||
| ) |
There was a problem hiding this comment.
The telemetry summary should use len(timesteps) as the denominator to stay consistent with the iteration-based counters (_cfg_gate_fresh_uncond and _cfg_gate_reused_delta), which increment every loop iteration regardless of the scheduler order.
| if _is_rank0 and batch.do_classifier_free_guidance: | |
| logger.info( | |
| "CFG gating summary: fraction=%.3f gate_step=%d/%d " | |
| "fresh_uncond=%d reused=%d invalidations=%d", | |
| _cfg_gate_fraction, | |
| _cfg_gate_step_idx if _cfg_gate_active else -1, | |
| num_inference_steps, | |
| _cfg_gate_fresh_uncond, | |
| _cfg_gate_reused_delta, | |
| _cfg_gate_invalidations, | |
| ) | |
| if _is_rank0 and batch.do_classifier_free_guidance: | |
| logger.info( | |
| "CFG gating summary: fraction=%.3f gate_step=%d/%d " | |
| "fresh_uncond=%d reused=%d invalidations=%d", | |
| _cfg_gate_fraction, | |
| _cfg_gate_step_idx if _cfg_gate_active else -1, | |
| len(timesteps), | |
| _cfg_gate_fresh_uncond, | |
| _cfg_gate_reused_delta, | |
| _cfg_gate_invalidations, | |
| ) |
Addresses inline review on PR hao-ai-lab#1372: for schedulers with order > 1 (DPM-Solver++ 2M, Heun, etc.) `len(timesteps) = num_inference_steps * scheduler.order + num_warmup_steps`. The denoising loop iterates over `timesteps` directly (`for i, t in enumerate(timesteps)`), so the gate index needs to scale with `len(timesteps)` to apply the intended fraction of the loop. Previous behaviour (correct for order=1 schedulers like Wan T2V's FlowMatchEulerDiscrete; incorrect for order=2): - gate_step = int(num_inference_steps * fraction) → with order=2, num_inference_steps=30, fraction=0.5: len(timesteps)=60, gate_step=15, gate fires at iteration 15/60 (25 % through the loop) instead of the intended 50 %. Now correct for all scheduler orders: - gate_step = int(len(timesteps) * fraction) → fires at the intended fraction of the loop regardless of order. Four sites updated to use len(timesteps): - gate_step_idx computation in the active branch - "never gates" sentinel in the inactive branch - cache-refresh condition inside the CFG loop - telemetry summary denominator Measurement impact: all numbers reported in the PR description are unaffected — they were produced on FlowMatchEulerDiscrete (order=1, no warmup), where len(timesteps) == num_inference_steps == 30.
|
Hi @rich7420 — this is a code review from one of @SolitaryThinker's AI reviewer agents (Gob). I run these to help triage PRs but @SolitaryThinker hasn't personally verified every finding. If anything below doesn't match what you know about the code, please ping @SolitaryThinker — they'll take a closer look. TL;DRThe implementation matches the claimed Adaptive Guidance / LinearAG mechanism: it caches Verdict: approve-with-followup
FindingsS2 — No regression/unit tests cover the new env var or gate branchThe changed file list only includes Suggested lightweight coverage:
This does not need a full Wan generation; a fake model/scheduler call-count test would be enough. S2 — Env var docs still say
|
|
/merge |
Run cond + uncond as a single batch=2 DiT forward per denoise step instead of two sequential batch=1 forwards. Composable with Adaptive Guidance (hao-ai-lab#1372). Mechanism is bit-equivalent to sequential CFG on H100 FA3 eager (SSIM=1.000000, 5 prompts, Wan 14B 720x1280x49f/30steps). See PR body for full validation matrix across L40S/H100 and eager/compile. Changes: - fastvideo_args.py: new use_batched_cfg: bool = True field + --use-batched-cfg CLI. Auto-fallback to sequential when V2V/I2V/ TI2V/action/camera conditioning is present (those carry batch=1 conditioning tensors out of scope for this PR). - entrypoints/video_generator.py: add use_batched_cfg to _FROM_PRETRAINED_CONVENIENCE_KWARGS so users don't see the DeprecationWarning from the legacy kwarg path. - pipelines/stages/denoising.py: gated batched branch in main DenoisingStage.forward. Cats [neg, pos] along batch dim with shape-match defensive fallback, single forward, chunk(2), existing CFG-combine + guidance_rescale math reused. Other DenoisingStage subclasses (Cosmos25, Dmd, ...) unchanged. Sequential CFG path preserved bit-for-bit for non-batched callers. Other DiTs (HunyuanVideo, LongCat, ...) unaffected.
|
@SolitaryThinker thanks for the test and review! |
Run cond + uncond as a single batch=2 DiT forward per denoise step instead of two sequential batch=1 forwards. Composable with Adaptive Guidance (hao-ai-lab#1372). Mechanism is bit-equivalent to sequential CFG on H100 FA3 eager (SSIM=1.000000, 5 prompts, Wan 14B 720x1280x49f/30steps). See PR body for full validation matrix across L40S/H100 and eager/compile. Changes: - fastvideo_args.py: new use_batched_cfg: bool = True field + --use-batched-cfg CLI. Auto-fallback to sequential when V2V/I2V/ TI2V/action/camera conditioning is present (those carry batch=1 conditioning tensors out of scope for this PR). - entrypoints/video_generator.py: add use_batched_cfg to _FROM_PRETRAINED_CONVENIENCE_KWARGS so users don't see the DeprecationWarning from the legacy kwarg path. - pipelines/stages/denoising.py: gated batched branch in main DenoisingStage.forward. Cats [neg, pos] along batch dim with shape-match defensive fallback, single forward, chunk(2), existing CFG-combine + guidance_rescale math reused. Other DenoisingStage subclasses (Cosmos25, Dmd, ...) unchanged. Sequential CFG path preserved bit-for-bit for non-batched callers. Other DiTs (HunyuanVideo, LongCat, ...) unaffected.
Run cond + uncond as a single batch=2 DiT forward per denoise step instead of two sequential batch=1 forwards. Composable with Adaptive Guidance (hao-ai-lab#1372). Mechanism is bit-equivalent to sequential CFG on H100 FA3 eager (SSIM=1.000000, 5 prompts, Wan 14B 720x1280x49f/30steps). See PR body for full validation matrix across L40S/H100 and eager/compile. Changes: - fastvideo_args.py: new use_batched_cfg: bool = True field + --use-batched-cfg CLI. Auto-fallback to sequential when V2V/I2V/ TI2V/action/camera conditioning is present (those carry batch=1 conditioning tensors out of scope for this PR). - entrypoints/video_generator.py: add use_batched_cfg to _FROM_PRETRAINED_CONVENIENCE_KWARGS so users don't see the DeprecationWarning from the legacy kwarg path. - pipelines/stages/denoising.py: gated batched branch in main DenoisingStage.forward. Cats [neg, pos] along batch dim with shape-match defensive fallback, single forward, chunk(2), existing CFG-combine + guidance_rescale math reused. Other DenoisingStage subclasses (Cosmos25, Dmd, ...) unchanged. Sequential CFG path preserved bit-for-bit for non-batched callers. Other DiTs (HunyuanVideo, LongCat, ...) unaffected.
Run cond + uncond as a single batch=2 DiT forward per denoise step instead of two sequential batch=1 forwards. Composable with Adaptive Guidance (hao-ai-lab#1372). Mechanism is bit-equivalent to sequential CFG on H100 FA3 eager (SSIM=1.000000, 5 prompts, Wan 14B 720x1280x49f/30steps). See PR body for full validation matrix across L40S/H100 and eager/compile. Changes: - fastvideo_args.py: new use_batched_cfg: bool = True field + --use-batched-cfg CLI. Auto-fallback to sequential when V2V/I2V/ TI2V/action/camera conditioning is present (those carry batch=1 conditioning tensors out of scope for this PR). - entrypoints/video_generator.py: add use_batched_cfg to _FROM_PRETRAINED_CONVENIENCE_KWARGS so users don't see the DeprecationWarning from the legacy kwarg path. - pipelines/stages/denoising.py: gated batched branch in main DenoisingStage.forward. Cats [neg, pos] along batch dim with shape-match defensive fallback, single forward, chunk(2), existing CFG-combine + guidance_rescale math reused. Other DenoisingStage subclasses (Cosmos25, Dmd, ...) unchanged. Sequential CFG path preserved bit-for-bit for non-batched callers. Other DiTs (HunyuanVideo, LongCat, ...) unaffected.
Run cond + uncond as a single batch=2 DiT forward per denoise step instead of two sequential batch=1 forwards. Default OFF so this PR has zero behaviour change for any user who doesn't opt in via `use_batched_cfg=True`. Composes with hao-ai-lab#1372 Adaptive Guidance — mutually exclusive at the gate level (AG selectively skips uncond, batched-CFG forces both; running them together defeats AG's win). Bit-equivalent to sequential CFG on H100 FA3 eager (SSIM=1.000000, Wan 14B 720x1280x49f/30steps, 5 prompts). On other configs (compile, FA2, smaller models) bf16 numerics drift slightly (~0.04 SSIM mean, visually imperceptible per frame-by-frame inspection) due to Inductor kernel selection and batched flash-attn numerics. Perf delta is run-to-run variable. Changes: - fastvideo_args.py: new use_batched_cfg: bool = False field + --use-batched-cfg CLI. Auto-fallback to sequential when V2V/I2V/ TI2V/action/camera conditioning is present (those carry batch=1 conditioning tensors out of scope for this PR) OR when AG is active. - entrypoints/video_generator.py: add use_batched_cfg to _FROM_PRETRAINED_CONVENIENCE_KWARGS. - pipelines/stages/denoising.py: gated batched branch in main DenoisingStage.forward. Cats [neg, pos] along batch dim with shape-match defensive fallback, single forward, chunk(2), existing CFG-combine + guidance_rescale math reused. Other DenoisingStage subclasses (Cosmos25, Dmd, ...) unchanged. - api/schema.py + api/compat.py: EngineConfig.use_batched_cfg field + legacy<->typed mappings (mirrors disable_autocast). - docs/design/inference_schema_parity_inventory.yaml: inventory entry under fastvideo_args.moved. - tests/api/test_parser.py: YAML-roundtrip expected dict updated. Sequential CFG path preserved bit-for-bit for non-batched callers. Other DiTs (HunyuanVideo, LongCat, ...) unaffected.
Run cond + uncond as a single batch=2 DiT forward per denoise step instead of two sequential batch=1 forwards. Default OFF so this PR has zero behaviour change for any user who doesn't opt in via `use_batched_cfg=True`. Composes with hao-ai-lab#1372 Adaptive Guidance — mutually exclusive at the gate level (AG selectively skips uncond, batched-CFG forces both; running them together defeats AG's win). Bit-equivalent to sequential CFG on H100 FA3 eager (SSIM=1.000000, Wan 14B 720x1280x49f/30steps, 5 prompts). On other configs (compile, FA2, smaller models) bf16 numerics drift slightly (~0.04 SSIM mean, visually imperceptible per frame-by-frame inspection) due to Inductor kernel selection and batched flash-attn numerics. Perf delta is run-to-run variable. Changes: - fastvideo_args.py: new use_batched_cfg: bool = False field + --use-batched-cfg CLI. Auto-fallback to sequential when V2V/I2V/ TI2V/action/camera conditioning is present (those carry batch=1 conditioning tensors out of scope for this PR) OR when AG is active. - entrypoints/video_generator.py: add use_batched_cfg to _FROM_PRETRAINED_CONVENIENCE_KWARGS. - pipelines/stages/denoising.py: gated batched branch in main DenoisingStage.forward. Cats [neg, pos] along batch dim with shape-match defensive fallback, single forward, chunk(2), existing CFG-combine + guidance_rescale math reused. Other DenoisingStage subclasses (Cosmos25, Dmd, ...) unchanged. - api/schema.py + api/compat.py: EngineConfig.use_batched_cfg field + legacy<->typed mappings (mirrors disable_autocast). - docs/design/inference_schema_parity_inventory.yaml: inventory entry under fastvideo_args.moved. - tests/api/test_parser.py: YAML-roundtrip expected dict updated. Sequential CFG path preserved bit-for-bit for non-batched callers. Other DiTs (HunyuanVideo, LongCat, ...) unaffected.
Run cond + uncond as a single batch=2 DiT forward per denoise step instead of two sequential batch=1 forwards. Default OFF so this PR has zero behaviour change for any user who doesn't opt in via `use_batched_cfg=True`. Composes with hao-ai-lab#1372 Adaptive Guidance — mutually exclusive at the gate level (AG selectively skips uncond, batched-CFG forces both; running them together defeats AG's win). Bit-equivalent to sequential CFG on H100 FA3 eager (SSIM=1.000000, Wan 14B 720x1280x49f/30steps, 5 prompts). On other configs (compile, FA2, smaller models) bf16 numerics drift slightly (~0.04 SSIM mean, visually imperceptible per frame-by-frame inspection) due to Inductor kernel selection and batched flash-attn numerics. Perf delta is run-to-run variable. Changes: - fastvideo_args.py: new use_batched_cfg: bool = False field + --use-batched-cfg CLI. Auto-fallback to sequential when V2V/I2V/ TI2V/action/camera conditioning is present (those carry batch=1 conditioning tensors out of scope for this PR) OR when AG is active. - entrypoints/video_generator.py: add use_batched_cfg to _FROM_PRETRAINED_CONVENIENCE_KWARGS. - pipelines/stages/denoising.py: gated batched branch in main DenoisingStage.forward. Cats [neg, pos] along batch dim with shape-match defensive fallback, single forward, chunk(2), existing CFG-combine + guidance_rescale math reused. Other DenoisingStage subclasses (Cosmos25, Dmd, ...) unchanged. - api/schema.py + api/compat.py: EngineConfig.use_batched_cfg field + legacy<->typed mappings (mirrors disable_autocast). - docs/design/inference_schema_parity_inventory.yaml: inventory entry under fastvideo_args.moved. - tests/api/test_parser.py: YAML-roundtrip expected dict updated. Sequential CFG path preserved bit-for-bit for non-batched callers. Other DiTs (HunyuanVideo, LongCat, ...) unaffected.
Purpose
Adds an inference-only CFG acceleration that skips the unconditional forward pass after a
fraction of the denoising schedule has completed and reconstructs the guidance prediction
from a cached
delta = noise_pred_cond − noise_pred_uncond:This is the LinearAG variant of Adaptive Guidance (Castillo et al. 2023, arXiv:2312.12487,
AAAI 2025) — described in the paper as "simple affine transformations of past score
estimates" replacing entire neural function evaluations in the post-convergence phase.
The same "split denoising at a gate step and optimize the post-gate phase" framing is
also explored in TGATE (Liu et al. 2024), but TGATE
caches attention outputs and keeps both CFG forwards — a different mechanism. This PR
implements the Adaptive Guidance mechanism (skip the uncond forward); the env var name
FASTVIDEO_CFG_GATE_STEPdescribes the action rather than naming either upstream method.Gated by a single env var
FASTVIDEO_CFG_GATE_STEP(float[0, 1], default1.0=no-op). Existing users see zero behavioural change unless they opt in.
Measured −22.0 % end-to-end on 4× L40S and −23.7 % on 1× H100 for Wan T2V 1.3B at
720×1280 / 77 frames / 30 inference steps, with VBench quality preserved within noise on
4 of 5 dimensions. On a separate (out-of-scope-for-this-PR) FA3 attention path on H100,
the win composes multiplicatively with the attention speedup for −50.8 % end-to-end.
Changes
fastvideo/envs.py(+26 lines): introduceFASTVIDEO_CFG_GATE_STEP: floatdeclaration in the
TYPE_CHECKINGblock plus the dispatching lambda inenvironment_variables, with full inline docstring covering semantics, edgecases (0.0 / 0.5 / 1.0), and known caveats.
fastvideo/pipelines/stages/denoising.py(+127 / −18 lines, behaviour-additive):out-of-range), pre-compute
gate_step = int(num_steps * fraction), andinitialise
delta_cached+delta_cached_model_id+ telemetry counters.logger.warningwhenguidance_rescale > 0combines withCFG gating active (combination is unvalidated — flagged so callers know).
populated and
i ≥ gate_step, computenoise_pred = cond + (s − 1) * delta_cachedand skip the uncond forward;otherwise fall back to the original two-pass CFG and refresh the cache.
id(current_model)changeinvalidates the cache (the cached delta is tied to the model that produced
it; reusing it across the boundary is silently wrong).
fresh_uncond,reused,invalidationscounters so users can confirm the env var is wired.When
FASTVIDEO_CFG_GATE_STEP=1.0(default), the gate index is set abovenum_inference_steps, the cache is never read, and every step takes the originalcode path — bit-exact backwards compatible.
Test Plan
The implementation was validated on the existing performance benchmark, plus a 5-prompt
quality A/B harness. Commands to reproduce the perf path locally:
Quality-pass A/B (5 fixed-seed prompts, same container, baseline vs gating active) was
run on both 4× L40S sp=4 (FA2 path) and 1× H100 sp=1 (FA2 and FA3 paths) via a local
harness; raw numbers in the next section.
Test Results
Performance — same-container A/B, 5 fixed-seed prompts, Wan T2V 1.3B at 720×1280 / 77 frames / 30 steps
FASTVIDEO_CFG_GATE_STEPPeak memory is flat across all six variants (within 1 MB); CFG gating adds no activation
allocation. On H100 the FA3 attention speedup composes cleanly with CFG gating: predicted
combined
(1 − 0.355) × (1 − 0.237) = 0.492 ⇒ 95.0 s, measured 95.06 s (within 0.1 %).Quality — VBench 5-metric, 4× L40S, 5 prompts (FA2, gate=1.0 vs 0.5)
All deltas are within VBench's 5-sample noise floor (~±0.5 %) except
aesthetic_quality,which moves +2.78 % relative on 5/5 prompts — beyond the noise band in the improvement
direction (consistent with the Adaptive Guidance argument that late-step CFG over-guides
fine detail and post-convergence affine reuse softens that).
Quality — VBench 5-metric, 1× H100 + FA3, 5 prompts (gate=1.0 vs 0.7 vs 0.5)
Sweeps the gate fraction to map the speed–quality curve on Hopper.
aesthetic_qualitymonotonically improves with more aggressive gating across bothhardware classes — the same effect reproduces on L40S/FA2 and on H100/FA3, suggesting
the mechanism is independent of attention numerics.
temporal_flickeringstays flat atboth gate fractions, so CFG gating does not introduce frame-level artefacts.
imaging_qualityshifts in the opposite direction between the two hardware classes —small (~1 %) but worth noting.
Quality — per-prompt SSIM + LPIPS, 1× H100 + FA3, vs
FASTVIDEO_CFG_GATE_STEP=1.0baselineSSIMandLPIPS-alexmeasured frame-by-frame on the same 5 prompts, then averaged:For context: LPIPS-alex < 0.05 ≈ nearly identical; 0.05–0.10 ≈ small perceptual
difference; > 0.10 ≈ clearly perceptible. Gate=0.7 sits comfortably in the
"nearly identical" band. Gate=0.5 enters the "small perceptual difference" band with
the most aggressive content (car at 0.096 mean, 0.12 max) brushing the edge of
perceptibility.
Side-by-side comparison videos
Each clip shows gate=1.0 (baseline) | gate=0.7 | gate=0.5 horizontally stacked at the
same seed.
noodle (food market motion)
noodle_compare.mp4
mountain (static landscape)
mountain_compare.mp4
car (high motion)
car_compare.mp4
portrait (face detail)
portrait_compare.mp4
metal (reflective surface)
metal_compare.mp4
Per-frame SSIM / LPIPS curve (portrait prompt, illustrative)
The curves are flat in time (no monotonic drift across the 77-frame clip), confirming
CFG gating does not accumulate error frame-to-frame.
Functional log excerpt — CFG gating telemetry
fresh_uncond + reused = num_inference_stepsandinvalidations = 0for single-transformer pipelines; on Wan 2.2 (boundary switch between high/low-noise experts)
the invalidation counter increments at the boundary as expected.
Checklist
pre-commit run --all-filesand fixed all issuestested configurations)
For model/pipeline changes, also check: