Skip to content

Commit b02a3af

Browse files
authored
feat(inference): cap consecutive SeaCache reuses (#225)
## Summary Sync the SeaCache consecutive-reuse cap from [imaginaire4!12068](https://gitlab-master.nvidia.com/cosmos-lab/imaginaire4/-/merge_requests/12068) into the OSS inference stack. - tune the default diffusion-cache threshold from `0.35` to `0.25` - cap consecutive cached residual reuses at 2 by default, per CFG pathway - expose `diffusion_cache_max_consecutive_cached` through setup overrides (`0` disables the cap) - forward the override into `DiffusionCache.Config` and include the effective value in startup logging - add OSS-path coverage for config validation, state-machine behavior, argument round-trip, and inference forwarding The production behavior matches the i4 MR; paths and tests are adapted to the OSS repository layout. ## Testing Not run, per request. Static diff and control-flow review completed; `git diff --check` passes.
1 parent d61cbe9 commit b02a3af

6 files changed

Lines changed: 125 additions & 3 deletions

File tree

cosmos_framework/inference/args_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,21 @@ def check_model_equal(actual: pydantic.BaseModel, expected: pydantic.BaseModel):
245245
check_model_equal(OmniSetupOverrides.model_validate(args.model_dump()).build_setup(), args)
246246

247247

248+
def test_diffusion_cache_max_consecutive_cached_round_trip(tmp_path: Path) -> None:
249+
overrides = OmniSetupOverrides(
250+
checkpoint_path=DEFAULT_CHECKPOINT_NAME,
251+
output_dir=tmp_path / "outputs",
252+
diffusion_cache=True,
253+
diffusion_cache_max_consecutive_cached=3,
254+
)
255+
256+
args = overrides.build_setup()
257+
258+
assert args.diffusion_cache is True
259+
assert args.diffusion_cache_max_consecutive_cached == 3
260+
assert OmniSetupOverrides.model_validate(args.model_dump()).diffusion_cache_max_consecutive_cached == 3
261+
262+
248263
def test_sample_args(tmp_path: Path):
249264
setup_args = OmniSetupOverrides(
250265
checkpoint_path=DEFAULT_CHECKPOINT_NAME,

cosmos_framework/inference/common/args.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ class SetupArgs(ABC, CheckpointArgs, ParallelismArgs, QuantizationArgs, Guardrai
885885
diffusion_cache: bool
886886
diffusion_cache_thresh: float | None
887887
diffusion_cache_residual_order: int | None
888+
diffusion_cache_max_consecutive_cached: pydantic.NonNegativeInt | None
888889

889890
# Subclass must implement these fields/methods
890891
# ------------------------------------------------------------
@@ -949,11 +950,15 @@ class SetupOverrides(ABC, CheckpointOverrides, ParallelismOverrides, Quantizatio
949950
"""Accumulated relative-L1 threshold (``diffusion_cache_thresh``), shared by the
950951
conditional and unconditional pathways. Larger values allow more skipping at
951952
the cost of lower fidelity. ``None`` uses the ``DiffusionCache.Config`` default
952-
(0.35). Only takes effect when diffusion cache is enabled."""
953+
(0.25). Only takes effect when diffusion cache is enabled."""
953954
diffusion_cache_residual_order: int | None = None
954955
"""Polynomial order for extrapolating the generation residual on a skipped step via
955956
Newton divided differences: 0 = constant reuse, 1 = linear, 2 = quadratic.
956957
``None`` uses the default (1). Only used when diffusion cache is enabled."""
958+
diffusion_cache_max_consecutive_cached: pydantic.NonNegativeInt | None = None
959+
"""Maximum consecutive residual reuses per CFG pathway before forcing a full
960+
evaluation. ``0`` disables the limit and ``None`` uses the cache default (2).
961+
Only used when diffusion cache is enabled."""
957962

958963
def _build_setup(self):
959964
if self.num_iterations > 1 and not self.benchmark:

cosmos_framework/inference/inference.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,8 @@ def _maybe_install_diffusion_cache(pipe: "OmniInference", setup_args: SetupArgs)
13661366
config_overrides["diffusion_cache_thresh"] = setup_args.diffusion_cache_thresh
13671367
if setup_args.diffusion_cache_residual_order is not None:
13681368
config_overrides["residual_order"] = setup_args.diffusion_cache_residual_order
1369+
if setup_args.diffusion_cache_max_consecutive_cached is not None:
1370+
config_overrides["max_consecutive_cached"] = setup_args.diffusion_cache_max_consecutive_cached
13691371
install_diffusion_cache(
13701372
pipe=pipe,
13711373
enabled=True,

cosmos_framework/inference/inference_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@
1010
import pytest
1111

1212

13+
def test_diffusion_cache_max_consecutive_cached_override(monkeypatch: pytest.MonkeyPatch) -> None:
14+
from cosmos_framework.inference import inference
15+
from cosmos_framework.model.generator.mot import diffusion_cache
16+
17+
install = Mock()
18+
monkeypatch.setattr(diffusion_cache, "install_diffusion_cache", install)
19+
pipe = SimpleNamespace()
20+
setup_args = SimpleNamespace(
21+
diffusion_cache=True,
22+
diffusion_cache_thresh=None,
23+
diffusion_cache_residual_order=None,
24+
diffusion_cache_max_consecutive_cached=3,
25+
)
26+
27+
inference.OmniInference._maybe_install_diffusion_cache(pipe, setup_args)
28+
29+
install.assert_called_once_with(
30+
pipe=pipe,
31+
enabled=True,
32+
sample_args_list=[],
33+
config_overrides={"max_consecutive_cached": 3},
34+
)
35+
36+
1337
def test_finalize_data_batch_does_not_mutate_reusable_video_list() -> None:
1438
torch = pytest.importorskip("torch")
1539

cosmos_framework/model/generator/mot/diffusion_cache.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ class DiffusionCache:
413413

414414
@dataclass(frozen=True, slots=True)
415415
class Config:
416-
diffusion_cache_thresh: float = 0.35
416+
diffusion_cache_thresh: float = 0.25
417417
"""Accumulated relative-L1 budget before a full eval is forced (shared across
418418
the per-step CFG-pass pathways). Larger ⇒ more skipping ⇒ faster but
419419
lower fidelity."""
@@ -425,6 +425,9 @@ class Config:
425425
"""Retention: always run full for the first ``ret_steps`` steps (warmup)."""
426426
cutoff_from_end: int = 1
427427
"""Always run full for the last ``cutoff_from_end`` steps (0 disables)."""
428+
max_consecutive_cached: int = 2
429+
"""Maximum consecutive residual reuses per CFG pathway before forcing a
430+
full evaluation. ``0`` disables the limit."""
428431
power_exp: float = 3.0
429432
"""Exponent of the ``1/|f|^power_exp`` clean-signal power prior in the SEA filter."""
430433
timestep_max: float = 1000.0
@@ -433,6 +436,14 @@ class Config:
433436
def __post_init__(self) -> None:
434437
if self.residual_order < 0:
435438
raise ValueError(f"residual_order must be >= 0, got {self.residual_order}")
439+
if (
440+
isinstance(self.max_consecutive_cached, bool)
441+
or not isinstance(self.max_consecutive_cached, int)
442+
or self.max_consecutive_cached < 0
443+
):
444+
raise ValueError(
445+
f"max_consecutive_cached must be a nonnegative integer, got {self.max_consecutive_cached}"
446+
)
436447

437448
@classmethod
438449
def from_overrides(cls, overrides: dict[str, Any] | None) -> "DiffusionCache.Config":
@@ -457,6 +468,7 @@ class _PathwayState:
457468
accumulated: float = 0.0
458469
prev_indicator: list[torch.Tensor] | None = None
459470
history: list[tuple[int, LMCacheEntry]] = field(default_factory=list)
471+
consecutive_cached: int = 0
460472

461473
@dataclass(slots=True)
462474
class State:
@@ -580,6 +592,7 @@ def patched_lm_forward(self_lm: Any, pack: Any, *args: Any, **kwargs: Any) -> An
580592
and _cache_entry_matches(ps.history[-1][1], in_causal, in_full, gen_only)
581593
)
582594
if reuse:
595+
ps.consecutive_cached += 1
583596
und_out = ps.history[-1][1][0] # understanding: absolute reuse (not a residual)
584597
gen_delta = _extrapolate_gen(ps.history, self.state.step, self.config.residual_order)
585598
out_pack = dict(pack)
@@ -591,6 +604,7 @@ def patched_lm_forward(self_lm: Any, pack: Any, *args: Any, **kwargs: Any) -> An
591604

592605
outputs = original_lm_forward(pack, *args, **kwargs)
593606
if caching:
607+
ps.consecutive_cached = 0
594608
out_pack = outputs[0] if isinstance(outputs, tuple) else outputs
595609
entry = _lm_cache_entry(in_causal, in_full, out_pack["causal_seq"], out_pack["full_only_seq"])
596610
ps.history.append((self.state.step, entry))
@@ -637,9 +651,13 @@ def _should_compute(self, pathway: str, indicator: list[torch.Tensor] | None) ->
637651
step = self.state.step
638652
cfg = self.config
639653

654+
max_consecutive_forced = bool(
655+
cfg.max_consecutive_cached and ps.consecutive_cached >= cfg.max_consecutive_cached
656+
)
640657
forced = (
641658
step < cfg.ret_steps
642659
or step >= self.num_steps - cfg.cutoff_from_end
660+
or max_consecutive_forced
643661
or not ps.history
644662
or indicator is None
645663
or ps.prev_indicator is None
@@ -813,7 +831,8 @@ def install_diffusion_cache(
813831
f"Enabled diffusion cache (diffusion-time inference cache) "
814832
f"max_num_steps={max_num_steps} "
815833
f"diffusion_cache_thresh={cfg.diffusion_cache_thresh} ret_steps={cfg.ret_steps} "
816-
f"cutoff_from_end={cfg.cutoff_from_end} power_exp={cfg.power_exp} "
834+
f"cutoff_from_end={cfg.cutoff_from_end} "
835+
f"max_consecutive_cached={cfg.max_consecutive_cached} power_exp={cfg.power_exp} "
817836
f"residual_order={cfg.residual_order}"
818837
)
819838
return cache
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: OpenMDW-1.1
3+
4+
import pytest
5+
import torch
6+
7+
from cosmos_framework.model.generator.mot.diffusion_cache import DiffusionCache
8+
9+
10+
def _const_indicator(value: float = 1.0) -> list[torch.Tensor]:
11+
return [torch.full((1, 2, 2, 1), value)]
12+
13+
14+
def _dummy_residual() -> tuple[torch.Tensor, torch.Tensor]:
15+
return torch.zeros(1, 2), torch.zeros(1, 2)
16+
17+
18+
@pytest.mark.L0
19+
@pytest.mark.CPU
20+
def test_max_consecutive_cached_forces_full_per_pathway() -> None:
21+
cache = DiffusionCache(
22+
num_steps=20,
23+
config={
24+
"ret_steps": 0,
25+
"cutoff_from_end": 0,
26+
"diffusion_cache_thresh": 1.0,
27+
"max_consecutive_cached": 3,
28+
},
29+
)
30+
cache.state.step = 0
31+
assert cache._should_compute("cfg0", _const_indicator()) is True
32+
cache._pathways["cfg0"].history = [(0, _dummy_residual())]
33+
34+
for step in range(1, 4):
35+
cache.state.step = step
36+
assert cache._should_compute("cfg0", _const_indicator()) is False
37+
cache._pathways["cfg0"].consecutive_cached += 1
38+
39+
cache.state.step = 4
40+
assert cache._should_compute("cfg0", _const_indicator()) is True
41+
42+
43+
@pytest.mark.L0
44+
@pytest.mark.CPU
45+
def test_diffusion_cache_uses_tuned_defaults() -> None:
46+
cache = DiffusionCache(num_steps=10)
47+
48+
assert cache.config.diffusion_cache_thresh == pytest.approx(0.25)
49+
assert cache.config.max_consecutive_cached == 2
50+
51+
52+
@pytest.mark.L0
53+
@pytest.mark.CPU
54+
@pytest.mark.parametrize("value", [-1, 1.5, True])
55+
def test_invalid_max_consecutive_cached_rejected(value: object) -> None:
56+
with pytest.raises(ValueError, match="max_consecutive_cached"):
57+
DiffusionCache(num_steps=10, config={"max_consecutive_cached": value})

0 commit comments

Comments
 (0)