Skip to content

omnidreams: Clean Forcing drift corrector (training recipe + gated deploy) - #398

Open
wenqingw-nv wants to merge 1 commit into
NVIDIA:mainfrom
wenqingw-nv:clean-forcing-omnidreams
Open

omnidreams: Clean Forcing drift corrector (training recipe + gated deploy)#398
wenqingw-nv wants to merge 1 commit into
NVIDIA:mainfrom
wenqingw-nv:clean-forcing-omnidreams

Conversation

@wenqingw-nv

@wenqingw-nv wenqingw-nv commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Long autoregressive Omnidreams rollouts drift: each chunk conditions on self-generated history and small errors compound into color/saturation runaway, texture mush, and content repetition. This PR adds a trained drift corrector (Clean Forcing), the recipe that produced it, and a gated deploy hook. drift_corrector=None (default) is byte-identical to current behavior.

Contents

  • omnidreams/_drift_corrector.py + two runner-config fields (drift_corrector, drift_corrector_gain) — a frozen-base LoRA (r16 on the self-attention q/k/v/o projections, ~7.3M params) applied at alpha*(t) × gain per denoise step, where alpha*(t) is the measured systematic fraction of the drift-induced error at each solver timestep (0.96 @ t=1000, 0.667 @ t=803).
  • integrations/omnidreams/drift_correction/ — the full recipe: mixed-lap tiled-HDMap loop pair construction (+ a gate-validated loop-free fork variant as an ablation), step-0 alpha*(t) systematicity gate (go/no-go + the deploy gain profile), v1 counterfactual-teacher training in velocity space, v2 DAgger + drift-contraction round with step-tagged and validation-peak snapshots, dial-grid eval, per-timestep reliability analysis. README documents the ~1 GPU-day reproduction.
  • tests/test_drift_corrector.py (ci_cpu, 6 tests) — the LoRA wrapper is a strict identity until a checkpoint is loaded and gated on; the wrap set matches the training-side module (same checkpoint order); the gate profile resolves by nearest-t lookup.

Docs

Measured (704×1280, 81-chunk ≈21.5 s rollouts, held-out scenes, GB300)

config MUSIQ ↑ late-window MUSIQ ↑ Δ-drift ↓ RAFT dynamics
base 49.1 46.7 +2.44 16.6
shipped alpha*(t) × 0.25 50.1 49.1 (+5%) −60% 13.8 (−17%)

Shipped config cuts the drift metric 60% with overall quality above base and visibly better tree/foliage detail and consistency across the rollout (human side-by-side review was held as the final acceptance bar throughout). The method/reproduction details: drift_correction/README.md (quick start)

Deploy notes

  • Per-step alpha*(t) gating cannot merge into one weight set → the LoRA runs unfused (~0.4% extra matmul); drift_corrector=None skips module surgery entirely.
  • Corrector LoRA checkpoint (~88 MB, md5 84beb8c014346833ce182310373b5d32) hosting: currently a fork release asset (https://github.com/wenqingw-nv/flashdreams-wq/releases/tag/clean-forcing-omnidreams-v3vp); release asset here vs HF — maintainer preference welcome.
  • Correctors encode a checkpoint-specific error map: re-instantiation on a new base checkpoint is ~1 GPU-day with the included recipe (the README quick-start is the refresh procedure).

Inference overhead (GB300, same rollout protocol as Measured; 3 runs, steady-state mean ± std)

config per-chunk (ms) end-to-end 81 chunks (s) effective fps peak VRAM
base 231.2 ± 5.0 18.88 ± 0.48 34.2 31.42 GB
shipped corrgate025 (pre-merged) 227.9 ± 1.4 18.49 ± 0.09 34.9 32.29 GB
  • Zero added per-chunk cost (within noise). The deploy hook pre-merges alpha*(t) × gain into one cached copy of the target projection weights per distinct gate value at load (2 sets, +1.8 GiB) and swaps the set per solver step, driven CPU-side from the denoising schedule — the corrected forward issues the same kernels as base: no LoRA matmuls, no GPU timestep readback. Overhead is therefore GPU-independent (base runtime on any card).
  • Output equivalence vs the runtime fp32 delta path: the bf16 weight merge preserves 96.5–97.5% of the correction delta norm; first-chunk latent diff ~0.8% relative. Instrument parity on 3 matched seeds (81-chunk rollouts): Δ-drift 2.49 ± 0.46 (pre-merged) vs 2.83 ± 0.38 (unfused), MUSIQ 58.6 ± 0.5 vs 58.4 ± 0.4 — no correction or quality loss beyond seed noise; side-by-side frame stacks are visually equivalent.
  • DRIFT_CORRECTOR_UNFUSED=1 selects the previous runtime fp32 delta path (338.5 ± 5.9 ms/chunk, +45%).
  • drift_corrector=None (default) runs the untouched base graph at zero overhead.

Video Comparison

Left: Omnidreams (base), Right: w/ Clean Forcing (better color/detail consistency across time; 4× speed):

base_vs_cleanForcing1.mp4
base_vs_cleanForcing2.mp4

@copy-pr-bot

copy-pr-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR ships the Clean Forcing drift corrector for Omnidreams: a frozen-base LoRA (r16 on self-attention q/k/v/o projections, ~7.3 M params) trained to close the velocity gap between drifted and clean-history predictions, deployed at alpha*(t) x gain per denoise step via a gated swap of pre-merged weight sets. drift_corrector=None is byte-identical to the current base path.

  • Deploy hook (omnidreams/_drift_corrector.py): pre-merged path caches one weight set per distinct alpha*(t) and swaps pointers each step with no LoRA matmul overhead; unfused fallback rescales A/B at runtime.
  • Training recipe (drift_correction/): pair construction, faithfulness gate, v1 counterfactual-teacher training, and v2 DAgger + drift-contraction round with dial-grid eval.
  • Tests (tests/test_drift_corrector.py): six ci_cpu tests covering identity at zero scale/zero-init B, wrap-set/checkpoint-order consistency, and pre-merge correctness including a non-mutation guard.

Confidence Score: 4/5

The production deploy path is well-tested and drift_corrector=None is transparent to existing users; the training scripts carry a hang risk isolated to the research workflow.

Both training scripts contain an unbounded retry loop in draw_losses that hangs indefinitely if every sampled cell is excluded — the late-horizon collapse is documented as a known stress case. The pre-merged pf callback also has an argument-handling asymmetry with the unfused path that could raise TypeError if the scheduler uses keyword arguments.

Files Needing Attention: train_v1.py and train_v2.py (unbounded draw_losses retry loop), and _drift_corrector.py (pre-merged pf callback parameter convention).

Important Files Changed

Filename Overview
integrations/omnidreams/omnidreams/_drift_corrector.py New production deploy hook; pre-merged path's inner pf callback uses fixed positional parameter names unlike the unfused path's *args, **kwargs, which may break if the scheduler uses keyword arguments.
integrations/omnidreams/omnidreams/runner.py Adds drift_corrector and drift_corrector_gain config fields; corrector setup runs before the save_embeddings_path early-return, permanently patching the transformer on that path (previously flagged).
integrations/omnidreams/tests/test_drift_corrector.py Six CPU-only tests covering identity, scaled-delta linearity, wrap-set matching, gate nearest-t lookup, and pre-merge correctness including a mutation guard.
integrations/omnidreams/drift_correction/train_v1.py v1 counterfactual-teacher LoRA trainer; draw_losses has an unbounded retry loop that hangs indefinitely if every cell is excluded as degenerate.
integrations/omnidreams/drift_correction/train_v2.py DAgger + drift-contraction trainer; shares the same unbounded draw_losses retry loop, compounded by val_r2 also calling draw_losses without an escape condition.
integrations/omnidreams/drift_correction/_train_attn.py Process-wide toggle for grad-friendly non-cache-writing self-attention; context managers correctly restore global state. _INJECT.pop(0) is O(n) per block (previously flagged).
integrations/omnidreams/drift_correction/_lora.py Training-side LoRA wrapper and parameter I/O; load_lora uses weights_only=False (previously flagged). Index-keyed save/load matches deploy module order.
integrations/omnidreams/drift_correction/_host.py Host adapter for rollout capture and counterfactual KV-history probes; load_clip uses weights_only=False (previously flagged). CONTEXT_NOISE_SEED seeding is consistent with training.
integrations/omnidreams/drift_correction/gain_predict.py Analysis utility for predicted deploy gain schedule; regex-parsed rho dict may silently miss timesteps, causing an uninformative KeyError.

Comments Outside Diff (1)

  1. integrations/omnidreams/drift_correction/train_v1.py, line 2299-2309 (link)

    P1 Infinite loop when all cells are excluded

    draw_losses retries until sample_losses returns a non-None value, but there is no escape hatch. If every (k, t) combination sampled from ids has rel_v > REL_V_EXCLUDE, the loop never terminates and the training process hangs indefinitely. The clip-0 late-horizon collapse scenario is already called out in the README as a known stress case, so this is a realistic failure mode. A max-retry guard — or at minimum a counter that prints a warning and raises after N consecutive exclusions — would make this debuggable rather than an invisible hang.

Reviews (2): Last reviewed commit: "omnidreams: add Clean Forcing drift corr..." | Re-trigger Greptile

Comment on lines +245 to 254
if cfg.drift_corrector is not None:
from omnidreams._drift_corrector import apply_drift_corrector

mode = apply_drift_corrector(
self, cfg.drift_corrector, cfg.drift_corrector_gain
)
logger.info(f"[{cfg.runner_name}] drift corrector: {mode}")
if cfg.save_embeddings_path is not None:
self._run_save_embeddings(cfg.save_embeddings_path)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Corrector deployed even on the save-embeddings path

apply_drift_corrector runs unconditionally before the save_embeddings_path early-return check. When a user sets both drift_corrector and save_embeddings_path, the full corrector deployment happens (loading ~88 MB, wrapping every target linear, monkey-patching predict_flow), then _run_save_embeddings returns immediately without ever calling predict_flow. The patch persists on the transformer instance, leaving it in a permanently modified state even after that run exits — which matters if the runner or pipeline object is later inspected or reused. Moving the corrector setup after the mode-dispatch block (or gating it on embeddings_path is None and save_embeddings_path is None) would avoid the wasted work and the permanently-patched transformer.

network = network._orig_mod
params = _apply_lora(network)

sd = torch.load(checkpoint, map_location="cpu", weights_only=False)["lora"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security weights_only=False allows arbitrary code execution from the checkpoint file

torch.load(..., weights_only=False) deserializes arbitrary Python objects via pickle, so any file pointed at by drift_corrector (a user-supplied Path in the runner config) can execute arbitrary code at load time. The checkpoint format used here is a plain {"lora": {int: Tensor}} dict — no custom classes or non-tensor objects — so weights_only=True would work after registering the built-in types with torch.serialization.add_safe_globals if needed. The same pattern appears in _host.py (load_clip) and _lora.py (load_lora), which are used in the training scripts; at minimum the production deploy path in _drift_corrector.py should use weights_only=True.

Comment on lines +145 to +148
def gated_pf(*args, **kwargs):
ts = kwargs.get("timestep", args[1] if len(args) > 1 else None)
t = float(ts.reshape(-1).max())
alpha = min(GATE_ALPHA.items(), key=lambda kv: abs(kv[0] - t))[1]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent None path in gated_pf gives an unhelpful AttributeError

If predict_flow is ever called without a timestep argument and with fewer than two positional args, ts resolves to None and ts.reshape(-1) raises AttributeError: 'NoneType' object has no attribute 'reshape' with no context about the actual problem. In all current call sites timestep is passed as a keyword argument, so the defensive fallback args[1] if len(args) > 1 else None works, but the None case should raise an explicit ValueError rather than deferring to an opaque AttributeError deep inside the corrector hook. A one-line guard (if ts is None: raise ValueError(...)) would make the failure immediately diagnosable.

Comment on lines +125 to +129
if _INJECT is not None:
ik, iv = _INJECT.pop(0)
span = ik.shape[-3]
pk = torch.cat([pk[..., :-span, :, :], ik], dim=-3)
pv = torch.cat([pv[..., :-span, :, :], iv], dim=-3)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 _INJECT.pop(0) is O(n) per block, making injection O(n²) over all blocks

inject_kv sets _INJECT = list(kvs) and _functional_forward calls _INJECT.pop(0) once per transformer block. list.pop(0) shifts every remaining element left, so across all B blocks the total cost is O(B²). Using an iterator (e.g. iter(kvs)) and next() instead would give O(1) per block. This only affects training (the deploy path has no _INJECT active), but it runs in the inner training loop over every sample_losses call with a contraction term.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@wenqingw-nv
wenqingw-nv enabled auto-merge July 24, 2026 23:49
…d deploy)

Adds a trained drift corrector for the Omnidreams single-view
integration: a frozen-base LoRA (r16 on the self-attn q/k/v/o
projections, ~7.3M params) trained from the model's own tiled-HDMap
loop rollouts with a counterfactual clean-history teacher in velocity
space.

Deploy: OmnidreamsRunnerConfig.drift_corrector (checkpoint path) +
drift_corrector_gain (default 0.25). The hook pre-merges
alpha*(t) x gain into one cached copy of the target projection weights
per distinct gate value at load (2 sets, +1.8 GiB) and swaps the set
per solver step, driven CPU-side from the denoising schedule -- the
corrected forward issues the same kernels as base, measured 0 ms/chunk
added within noise. alpha*(t) is the measured systematic fraction of
the drift-induced error at each solver timestep.
DRIFT_CORRECTOR_UNFUSED=1 selects the runtime fp32 A/B-delta path
instead. drift_corrector=None (default) is byte-identical to current
behavior.

Recipe under integrations/omnidreams/drift_correction/: pair
construction (mixed-lap tiled-HDMap loops; loop-free fork variant as an
ablation), the step-0 alpha*(t) systematicity gate, v1 counterfactual-
teacher training, v2 DAgger + drift-contraction round with step-tagged
and validation-peak snapshots, dial-grid eval, and a per-timestep
reliability analysis utility.

Measured on held-out 21.5 s rollouts: drift Delta-MUSIQ +0.99 vs base
+2.44 (60% cut), late-window MUSIQ +5% over base, dynamics -17%.

tests/test_drift_corrector.py (ci_cpu, 9 tests): the LoRA wrapper is a
strict identity until a checkpoint is loaded and gated on; the wrap set
matches the training-side module; the gate profile resolves by
nearest-t lookup; the pre-merged weight sets match the unfused delta
and leave the base weights untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: wenqingw <wenqingw@nvidia.com>
@wenqingw-nv
wenqingw-nv force-pushed the clean-forcing-omnidreams branch from 2e328b7 to c6464f2 Compare July 28, 2026 21:09
hakuturu583 pushed a commit to hakuturu583/flashdreams that referenced this pull request Jul 29, 2026
Driving the interactive demo for more than about a minute made the world
fall apart: the road and lane markings dissolved into smeared vegetation
and structures, unrecoverably.

Cause is the KV cache layout. BlockKVCache is [sink | local window] and
only the window rolls, but sink_size was 0, so nothing was pinned. The
scene's real initial frame therefore left the cache after local_attn_size
chunks, and from then on every chunk was conditioned purely on the model's
own previous output -- errors became the new "truth" and compounded.

sink_size was already a manifest field, but _select_config_name rejected
any non-zero value with NotImplementedError and the value was never passed
to the transformer. The underlying support was already complete: BlockKVCache
documents "Sink tokens are never evicted" and _roll_local_window_left shifts
only the region past sink_size. So this drops the guard and wires sink_size
through _transformer_overrides as sink_size_t.

A/B measured on an RTX 3090, same scene, same seed (1234), 512x288,
auto-respawn disabled so a respawn could not silently reset the rollout,
300 chunks (~80 s of generated video) per arm:

  elapsed   sink_size=0                      sink_size=2
  0 s       clean                            clean
  35 s      oversaturated, stylised          clean
  75 s      COLLAPSED (no road, no lanes)    clean

Mean saturation runs away without sinks (65.8 -> 106.3 -> 83.7) but stays
flat with them (49.7 -> 49.0 -> 55.8); luminance std grows +65% versus +16%.
Saturation runaway is an early indicator of the drift.

Cost is 2.8% of steady-state FPS (29.1 -> 28.29, 274.9 -> 282.8 ms/chunk)
and no measurable VRAM change -- the 16.26 GiB peak is set by the text
encoder during init, not by the KV cache.

This is a deploy-side mitigation, not a cure: it prevented collapse at 80 s
but drift is a training-time problem, and a longer rollout may still
degrade. The principled fix is the upstream Clean Forcing drift corrector
(NVIDIA#398).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FuZXfbb4LV9tkFobCq6ZTg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant