Text-to-Video
Diffusers
video-generation
test-time-search
best-of-n
caching
training-free
wan
cogvideox
hunyuanvideo
ltx-video
Instructions to use shreshthsaini/CachedSearch with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use shreshthsaini/CachedSearch with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("shreshthsaini/CachedSearch", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Add CachedSearch implementation and model card
Browse files- .gitattributes +2 -0
- LICENSE +21 -0
- README.md +192 -0
- assets/fig1_overview.png +3 -0
- assets/fig2_method.png +3 -0
- cachedsearch/__init__.py +23 -0
- cachedsearch/api.py +181 -0
- cachedsearch/caching.py +144 -0
- cachedsearch/verifiers.py +52 -0
- examples/calibrate_new_model.py +46 -0
- examples/run_wan.py +52 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/fig1_overview.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/fig2_method.png filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Shreshth Saini
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: diffusers
|
| 4 |
+
pipeline_tag: text-to-video
|
| 5 |
+
tags:
|
| 6 |
+
- text-to-video
|
| 7 |
+
- video-generation
|
| 8 |
+
- test-time-search
|
| 9 |
+
- best-of-n
|
| 10 |
+
- caching
|
| 11 |
+
- training-free
|
| 12 |
+
- wan
|
| 13 |
+
- cogvideox
|
| 14 |
+
- hunyuanvideo
|
| 15 |
+
- ltx-video
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
# CachedSearch: Training-Free Cached Exploration for Test-Time Search in Video Diffusion
|
| 19 |
+
|
| 20 |
+
[Paper (arXiv 2607.23159)](https://arxiv.org/abs/2607.23159) · [Code](https://github.com/shreshthsaini/CachedSearch) · [Project page](https://shreshthsaini.github.io/CachedSearch/) · [ComfyUI nodes](https://github.com/shreshthsaini/ComfyUI-CachedSearch) · [Diffusers community script](https://github.com/huggingface/diffusers/blob/main/examples/community/README_community_scripts.md#cachedsearch-cheaper-best-of-n-for-video-pipelines)
|
| 21 |
+
|
| 22 |
+
CachedSearch makes best-of-N seed search cheaper for video diffusion models. It generates every candidate with a training-free cache turned on, scores the cached drafts with a verifier, and regenerates only the winning seed at full compute.
|
| 23 |
+
|
| 24 |
+
The method needs no training, so this repository holds no weights. It holds the official implementation: the `cachedsearch` Python package and two example scripts, copied unchanged from the GitHub repository at commit [`cc2a637`](https://github.com/shreshthsaini/CachedSearch/tree/cc2a637c89485af723233aeb8ad76190b572a464).
|
| 25 |
+
|
| 26 |
+

|
| 27 |
+
|
| 28 |
+
## How it works
|
| 29 |
+
|
| 30 |
+
1. Generate every candidate with a training-free cache enabled.
|
| 31 |
+
2. Score the cached drafts with the verifier the search uses.
|
| 32 |
+
3. Select the seed with the highest draft score.
|
| 33 |
+
4. Regenerate only that seed at full compute and return it.
|
| 34 |
+
|
| 35 |
+
```text
|
| 36 |
+
drafts = [cached_generate(prompt, seed) for seed in seeds]
|
| 37 |
+
winner = seeds[argmax(verifier(video, prompt) for video in drafts)]
|
| 38 |
+
return full_generate(prompt, winner)
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
The sampler is deterministic in the seed, so the delivered video matches what full-compute search returns whenever both pick the same seed. Caching can change which candidate wins. It does not change the quality of the video you get back.
|
| 42 |
+
|
| 43 |
+

|
| 44 |
+
|
| 45 |
+
Results reported in the official README:
|
| 46 |
+
|
| 47 |
+
- Ranking survives caching: median Spearman rho is 0.905, with 72% top-1 agreement on VBench.
|
| 48 |
+
- Exploring under caching and committing at full compute retains 94.7% of the best-of-8 gain at 63% of the cost.
|
| 49 |
+
- Ranking failures concentrate where candidates are nearly tied, so they cost little.
|
| 50 |
+
- The approach works with any published cache engine and transfers across six models in four families.
|
| 51 |
+
|
| 52 |
+
## Files
|
| 53 |
+
|
| 54 |
+
| Path | Contents |
|
| 55 |
+
|---|---|
|
| 56 |
+
| `cachedsearch/api.py` | `cached_search`, `cached_search_batch`, `calibrate_tau` |
|
| 57 |
+
| `cachedsearch/caching.py` | `CacheConfig`, `CachedTransformer`, `wrap_pipeline` (the EasyCache-style adaptive cache and a static baseline) |
|
| 58 |
+
| `cachedsearch/verifiers.py` | the default ImageReward verifier |
|
| 59 |
+
| `examples/run_wan.py` | best-of-8 search on Wan2.1 |
|
| 60 |
+
| `examples/calibrate_new_model.py` | threshold calibration for a new model family |
|
| 61 |
+
|
| 62 |
+
## Install
|
| 63 |
+
|
| 64 |
+
Install from GitHub:
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
pip install "git+https://github.com/shreshthsaini/CachedSearch.git@cc2a637c89485af723233aeb8ad76190b572a464"
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Or fetch the package from this repository, with no install step:
|
| 71 |
+
|
| 72 |
+
```python
|
| 73 |
+
import sys
|
| 74 |
+
from huggingface_hub import snapshot_download
|
| 75 |
+
|
| 76 |
+
repo_dir = snapshot_download("shreshthsaini/CachedSearch", allow_patterns=["cachedsearch/*"])
|
| 77 |
+
sys.path.insert(0, repo_dir)
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
Requirements: `torch`, `diffusers`, `numpy`, plus whatever your pipeline needs (`transformers` for the text encoder). The default verifier needs `image-reward`. Pass your own `verifier=` to skip that dependency.
|
| 81 |
+
|
| 82 |
+
## Usage
|
| 83 |
+
|
| 84 |
+
`cached_search` takes any diffusers video pipeline that exposes `.transformer`. The verifier is any callable `(frames, prompt) -> float` where higher is better.
|
| 85 |
+
|
| 86 |
+
The example below runs on CPU in a few seconds. It uses `hf-internal-testing/tiny-wan-pipe`, a tiny Wan pipeline with random weights, and a toy verifier. It checks that the search loop and the cache run end to end. The frames are noise and the scores mean nothing.
|
| 87 |
+
|
| 88 |
+
```python
|
| 89 |
+
import sys
|
| 90 |
+
|
| 91 |
+
import numpy as np
|
| 92 |
+
import torch
|
| 93 |
+
from diffusers import WanPipeline
|
| 94 |
+
from huggingface_hub import snapshot_download
|
| 95 |
+
|
| 96 |
+
# Skip these two lines if you installed the package with pip.
|
| 97 |
+
repo_dir = snapshot_download("shreshthsaini/CachedSearch", allow_patterns=["cachedsearch/*"])
|
| 98 |
+
sys.path.insert(0, repo_dir)
|
| 99 |
+
|
| 100 |
+
from cachedsearch import CacheConfig, cached_search
|
| 101 |
+
from cachedsearch.caching import wrap_pipeline
|
| 102 |
+
|
| 103 |
+
pipe = WanPipeline.from_pretrained("hf-internal-testing/tiny-wan-pipe", torch_dtype=torch.float32)
|
| 104 |
+
gen_kwargs = dict(height=32, width=32, num_frames=9, guidance_scale=5.0)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def toy_verifier(frames, prompt):
|
| 108 |
+
# Stand-in scorer so the example runs without ImageReward. Not a quality metric.
|
| 109 |
+
return float(np.asarray(frames).mean())
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
result = cached_search(
|
| 113 |
+
pipe,
|
| 114 |
+
"a red fox running through deep snow",
|
| 115 |
+
verifier=toy_verifier,
|
| 116 |
+
n=4, # search width
|
| 117 |
+
tau=0.10, # caching threshold
|
| 118 |
+
steps=20,
|
| 119 |
+
mode="commit", # regenerate the winner at full compute
|
| 120 |
+
gen_kwargs=gen_kwargs,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
print(result.seed) # the seed that won
|
| 124 |
+
print(result.draft_scores) # verifier score of every cached draft
|
| 125 |
+
print(np.asarray(result.video).shape) # (9, 32, 32, 3), the full-compute video
|
| 126 |
+
|
| 127 |
+
# Count how many transformer calls one cached rollout skips.
|
| 128 |
+
cache = wrap_pipeline(pipe, CacheConfig(mode="adaptive", tau=0.10, total_steps=20))
|
| 129 |
+
cache.reset()
|
| 130 |
+
pipe(prompt="a red fox running through deep snow", num_inference_steps=20,
|
| 131 |
+
generator=torch.Generator().manual_seed(0), **gen_kwargs)
|
| 132 |
+
print(cache.stats())
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
Output on CPU with `torch` 2.14.1 and `diffusers` 0.41.0:
|
| 136 |
+
|
| 137 |
+
```text
|
| 138 |
+
3
|
| 139 |
+
[0.4587..., 0.4588..., 0.4589..., 0.4590...]
|
| 140 |
+
(9, 32, 32, 3)
|
| 141 |
+
{'transformer0': {'branch0': {'skips': 8, 'computes': 12}, 'branch1': {'skips': 8, 'computes': 12}}}
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
With these settings the cache skips 8 of 20 transformer calls per classifier-free-guidance branch.
|
| 145 |
+
|
| 146 |
+
### Real models
|
| 147 |
+
|
| 148 |
+
For a real model, load the checkpoint on a GPU and drop the `verifier` argument to use ImageReward averaged over 8 frames, the scorer behind the paper's numbers. `examples/run_wan.py` does this for `Wan-AI/Wan2.1-T2V-1.3B-Diffusers`:
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
pip install image-reward imageio imageio-ffmpeg
|
| 152 |
+
python examples/run_wan.py --prompt "a red fox running through deep snow"
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
This script and `examples/calibrate_new_model.py` are the official ones from the GitHub repository. They need a CUDA GPU and were not rerun when this card was written. Only the CPU example above was.
|
| 156 |
+
|
| 157 |
+
## Thresholds for tested models
|
| 158 |
+
|
| 159 |
+
The caching threshold `tau` is the one model-specific number. The table below comes from the official README: the recommended `tau` is the most aggressive setting that still keeps at least 85% of the search gain in the calibration sweep.
|
| 160 |
+
|
| 161 |
+
| Model | Params | Family | Recommended `tau` | Gain capture | Exploration speedup |
|
| 162 |
+
|---|---:|---|---:|---:|---:|
|
| 163 |
+
| Wan2.1-T2V-1.3B | 1.3B | Wan | 0.20 | 88.3% | 2.41x |
|
| 164 |
+
| Wan2.1-T2V-14B | 14B | Wan | 0.10 | 87.5% | 2.05x |
|
| 165 |
+
| Wan2.2-TI2V-5B | 5B | Wan | 0.10 | 86.0% | 2.05x |
|
| 166 |
+
| CogVideoX-5B | 5B | CogVideoX | 0.05 | 85.9% | 1.78x |
|
| 167 |
+
| HunyuanVideo | 13B | HunyuanVideo | 0.05 | 85.1% | 1.77x |
|
| 168 |
+
| LTX-Video | 2B | LTX | 0.02 | 79.6% | 1.71x |
|
| 169 |
+
|
| 170 |
+
No threshold measured for LTX-Video reached 85% capture. For a model that is not listed, run `examples/calibrate_new_model.py` once (about two GPU-hours) and use the threshold it recommends. Do not copy a threshold across model families.
|
| 171 |
+
|
| 172 |
+
## Modes
|
| 173 |
+
|
| 174 |
+
- `mode="commit"` (default): regenerate the winner with caching off. The delivered video is a full-compute sample.
|
| 175 |
+
- `mode="keep"`: return the cached draft of the winner. Cheaper, with mild motion dampening at aggressive thresholds.
|
| 176 |
+
|
| 177 |
+
## Citation
|
| 178 |
+
|
| 179 |
+
```bibtex
|
| 180 |
+
@article{saini2026cachedsearch,
|
| 181 |
+
title = {CachedSearch: Training-Free Cached Exploration for Test-Time Search in Video Diffusion},
|
| 182 |
+
author = {Saini, Shreshth and Birkbeck, Neil and Wang, Yilin and Adsumilli, Balu and Bovik, Alan C.},
|
| 183 |
+
year = {2026},
|
| 184 |
+
eprint = {2607.23159},
|
| 185 |
+
archivePrefix = {arXiv},
|
| 186 |
+
primaryClass = {cs.AI}
|
| 187 |
+
}
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
## License
|
| 191 |
+
|
| 192 |
+
MIT. See [LICENSE](LICENSE).
|
assets/fig1_overview.png
ADDED
|
Git LFS Details
|
assets/fig2_method.png
ADDED
|
Git LFS Details
|
cachedsearch/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CachedSearch: explore cheap, commit full.
|
| 2 |
+
|
| 3 |
+
Drop-in test-time search for any diffusers video pipeline:
|
| 4 |
+
|
| 5 |
+
from cachedsearch import cached_search
|
| 6 |
+
|
| 7 |
+
result = cached_search(pipe, "a red fox in snow", n=8)
|
| 8 |
+
|
| 9 |
+
The verifier defaults to ImageReward averaged over 8 frames, the scorer behind
|
| 10 |
+
the paper's numbers. Pass ``verifier=`` to use your own.
|
| 11 |
+
|
| 12 |
+
The exploration rollouts run under training-free caching; only the winning
|
| 13 |
+
seed is regenerated at full compute, so the returned video is a genuine
|
| 14 |
+
full-compute sample.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from .api import CacheConfig, cached_search, cached_search_batch, calibrate_tau
|
| 18 |
+
from .verifiers import default_verifier, imagereward_verifier
|
| 19 |
+
|
| 20 |
+
__all__ = [
|
| 21 |
+
"cached_search", "cached_search_batch", "calibrate_tau", "CacheConfig",
|
| 22 |
+
"default_verifier", "imagereward_verifier",
|
| 23 |
+
]
|
cachedsearch/api.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The user-facing CachedSearch API.
|
| 2 |
+
|
| 3 |
+
Three functions:
|
| 4 |
+
|
| 5 |
+
cached_search(pipe, prompt, verifier, n=8) run search on one prompt
|
| 6 |
+
cached_search_batch(pipe, prompts, verifier) run search on many prompts
|
| 7 |
+
calibrate_tau(pipe, prompts, verifier) find the threshold for a new model
|
| 8 |
+
|
| 9 |
+
Everything is training-free. The only model-specific quantity is the caching
|
| 10 |
+
threshold tau, which `calibrate_tau` measures in about two GPU-hours.
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import time
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from typing import Callable, Iterable, Sequence
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
from .caching import CacheConfig, wrap_pipeline
|
| 22 |
+
|
| 23 |
+
from .verifiers import Verifier, default_verifier, imagereward_verifier
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class SearchResult:
|
| 28 |
+
video: object # the delivered video (full-compute sample in commit mode)
|
| 29 |
+
seed: int # the seed that produced it
|
| 30 |
+
draft_scores: list # verifier score of every cached draft
|
| 31 |
+
explore_seconds: float
|
| 32 |
+
commit_seconds: float
|
| 33 |
+
|
| 34 |
+
@property
|
| 35 |
+
def total_seconds(self) -> float:
|
| 36 |
+
return self.explore_seconds + self.commit_seconds
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _generate(pipe, prompt: str, seed: int, steps: int, gen_kwargs: dict):
|
| 40 |
+
generator = torch.Generator(device=pipe.device).manual_seed(seed)
|
| 41 |
+
out = pipe(prompt=prompt, num_inference_steps=steps, generator=generator, **gen_kwargs)
|
| 42 |
+
return out.frames[0]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def cached_search(
|
| 46 |
+
pipe,
|
| 47 |
+
prompt: str,
|
| 48 |
+
verifier: Verifier | None = None,
|
| 49 |
+
n: int = 8,
|
| 50 |
+
tau: float = 0.10,
|
| 51 |
+
steps: int = 50,
|
| 52 |
+
mode: str = "commit",
|
| 53 |
+
seeds: Sequence[int] | None = None,
|
| 54 |
+
gen_kwargs: dict | None = None,
|
| 55 |
+
) -> SearchResult:
|
| 56 |
+
"""Best-of-n search where exploration runs under caching.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
pipe: any diffusers video pipeline exposing `.transformer` (Wan, CogVideoX,
|
| 60 |
+
HunyuanVideo, LTX-Video all work).
|
| 61 |
+
prompt: the text prompt.
|
| 62 |
+
verifier: callable (frames, prompt) -> float, higher is better. Any
|
| 63 |
+
scorer works. Defaults to ImageReward averaged over 8 uniformly
|
| 64 |
+
spaced frames, the verifier behind the paper's headline numbers.
|
| 65 |
+
n: search width (number of candidates). Recommit pays off for n >= 4.
|
| 66 |
+
tau: caching threshold. 0.10 is calibrated for Wan2.1; run `calibrate_tau`
|
| 67 |
+
for a new architecture (do not copy this constant across families).
|
| 68 |
+
steps: denoising steps.
|
| 69 |
+
mode: "commit" regenerates the winner at full compute (recommended; the
|
| 70 |
+
delivered video is a genuine full-compute sample). "keep" returns the
|
| 71 |
+
cached draft directly: cheaper, but caching artifacts reach the user
|
| 72 |
+
and motion is mildly dampened.
|
| 73 |
+
seeds: explicit seeds; defaults to range(n).
|
| 74 |
+
gen_kwargs: extra pipeline kwargs (height, width, num_frames, guidance...).
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
SearchResult with the delivered video, its seed, draft scores, and timings.
|
| 78 |
+
"""
|
| 79 |
+
seeds = list(seeds if seeds is not None else range(n))
|
| 80 |
+
gen_kwargs = dict(gen_kwargs or {})
|
| 81 |
+
if verifier is None:
|
| 82 |
+
verifier = default_verifier()
|
| 83 |
+
|
| 84 |
+
# ---- explore: every candidate under caching -------------------------------
|
| 85 |
+
cache = wrap_pipeline(pipe, CacheConfig(mode="adaptive", tau=tau, total_steps=steps))
|
| 86 |
+
drafts, scores = [], []
|
| 87 |
+
t0 = time.time()
|
| 88 |
+
for seed in seeds:
|
| 89 |
+
cache.reset() # cache state must not leak across candidates
|
| 90 |
+
frames = _generate(pipe, prompt, seed, steps, gen_kwargs)
|
| 91 |
+
drafts.append(frames)
|
| 92 |
+
scores.append(float(verifier(frames, prompt)))
|
| 93 |
+
explore_s = time.time() - t0
|
| 94 |
+
|
| 95 |
+
best = int(np.argmax(scores))
|
| 96 |
+
winner_seed = seeds[best]
|
| 97 |
+
|
| 98 |
+
# ---- commit: regenerate only the winner at full compute -------------------
|
| 99 |
+
if mode == "keep":
|
| 100 |
+
return SearchResult(drafts[best], winner_seed, scores, explore_s, 0.0)
|
| 101 |
+
|
| 102 |
+
wrap_pipeline(pipe, CacheConfig(mode="off", total_steps=steps))
|
| 103 |
+
t0 = time.time()
|
| 104 |
+
video = _generate(pipe, prompt, winner_seed, steps, gen_kwargs)
|
| 105 |
+
commit_s = time.time() - t0
|
| 106 |
+
return SearchResult(video, winner_seed, scores, explore_s, commit_s)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def cached_search_batch(
|
| 110 |
+
pipe, prompts: Iterable[str], verifier: Verifier | None = None, **kwargs
|
| 111 |
+
) -> list[SearchResult]:
|
| 112 |
+
"""cached_search over many prompts; returns one SearchResult per prompt."""
|
| 113 |
+
return [cached_search(pipe, p, verifier, **kwargs) for p in prompts]
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def calibrate_tau(
|
| 117 |
+
pipe,
|
| 118 |
+
prompts: Sequence[str],
|
| 119 |
+
verifier: Verifier | None = None,
|
| 120 |
+
taus: Sequence[float] = (0.05, 0.10, 0.20),
|
| 121 |
+
n: int = 8,
|
| 122 |
+
steps: int = 50,
|
| 123 |
+
target_capture: float = 0.90,
|
| 124 |
+
gen_kwargs: dict | None = None,
|
| 125 |
+
) -> dict:
|
| 126 |
+
"""Measure the speed-fidelity frontier of a NEW model and pick tau.
|
| 127 |
+
|
| 128 |
+
Protocol (this is the paper's protocol in miniature): generate every
|
| 129 |
+
(prompt, seed) pair twice, once at full compute and once under caching,
|
| 130 |
+
score both, and report how much of full-compute search's gain the cached
|
| 131 |
+
ranking retains. Roughly two GPU-hours with 25 prompts on a 1.3B model.
|
| 132 |
+
|
| 133 |
+
Returns {tau: {"speedup", "capture", "spearman"}} plus "recommended": the
|
| 134 |
+
largest tau whose capture is at or above target_capture.
|
| 135 |
+
"""
|
| 136 |
+
prompts = list(prompts)
|
| 137 |
+
gen_kwargs = dict(gen_kwargs or {})
|
| 138 |
+
seeds = list(range(n))
|
| 139 |
+
if verifier is None:
|
| 140 |
+
verifier = default_verifier()
|
| 141 |
+
|
| 142 |
+
# full-compute references, shared across all tau values
|
| 143 |
+
wrap_pipeline(pipe, CacheConfig(mode="off", total_steps=steps))
|
| 144 |
+
full_scores, full_latency = {}, []
|
| 145 |
+
for p in prompts:
|
| 146 |
+
row = []
|
| 147 |
+
for s in seeds:
|
| 148 |
+
t0 = time.time()
|
| 149 |
+
frames = _generate(pipe, p, s, steps, gen_kwargs)
|
| 150 |
+
full_latency.append(time.time() - t0)
|
| 151 |
+
row.append(float(verifier(frames, p)))
|
| 152 |
+
full_scores[p] = np.asarray(row)
|
| 153 |
+
cf = float(np.mean(full_latency))
|
| 154 |
+
|
| 155 |
+
report = {}
|
| 156 |
+
for tau in taus:
|
| 157 |
+
cache = wrap_pipeline(pipe, CacheConfig(mode="adaptive", tau=tau, total_steps=steps))
|
| 158 |
+
rhos, captures, latency = [], [], []
|
| 159 |
+
for p in prompts:
|
| 160 |
+
row = []
|
| 161 |
+
for s in seeds:
|
| 162 |
+
cache.reset()
|
| 163 |
+
t0 = time.time()
|
| 164 |
+
frames = _generate(pipe, p, s, steps, gen_kwargs)
|
| 165 |
+
latency.append(time.time() - t0)
|
| 166 |
+
row.append(float(verifier(frames, p)))
|
| 167 |
+
cached = np.asarray(row)
|
| 168 |
+
full = full_scores[p]
|
| 169 |
+
ra, rb = np.argsort(np.argsort(full)), np.argsort(np.argsort(cached))
|
| 170 |
+
rhos.append(float(np.corrcoef(ra, rb)[0, 1]))
|
| 171 |
+
pick, spread = int(np.argmax(cached)), full.max() - full.mean()
|
| 172 |
+
captures.append((full[pick] - full.mean()) / spread if spread > 1e-9 else 1.0)
|
| 173 |
+
report[tau] = {
|
| 174 |
+
"speedup": cf / float(np.mean(latency)),
|
| 175 |
+
"capture": float(np.mean(captures)),
|
| 176 |
+
"spearman": float(np.median(rhos)),
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
ok = [t for t in taus if report[t]["capture"] >= target_capture]
|
| 180 |
+
report["recommended"] = max(ok) if ok else min(taus)
|
| 181 |
+
return report
|
cachedsearch/caching.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training-free caching wrappers for video DiTs (Wan, CogVideoX, HunyuanVideo, LTX via diffusers).
|
| 2 |
+
|
| 3 |
+
Two modes:
|
| 4 |
+
- "adaptive": EasyCache-style online rule. Cache the transformation vector
|
| 5 |
+
delta = v(x_t) - x_t; while an accumulated change indicator stays below
|
| 6 |
+
tau, skip the transformer forward and return x_t + delta.
|
| 7 |
+
- "static": fixed skip pattern over a step range (PAB/uniform-style baseline).
|
| 8 |
+
|
| 9 |
+
CFG handling: diffusers WanPipeline calls the transformer once per branch per
|
| 10 |
+
step. We key cache state on call parity (branch index = call_count % branches)
|
| 11 |
+
so cond/uncond each get independent state.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
import torch
|
| 15 |
+
from dataclasses import dataclass, field
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class _BranchState:
|
| 20 |
+
prev_input: torch.Tensor | None = None
|
| 21 |
+
prev_output: torch.Tensor | None = None
|
| 22 |
+
delta: torch.Tensor | None = None
|
| 23 |
+
accum: float = 0.0
|
| 24 |
+
skips: int = 0
|
| 25 |
+
computes: int = 0
|
| 26 |
+
step: int = 0
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class CacheConfig:
|
| 31 |
+
mode: str = "adaptive" # "adaptive" | "static" | "off"
|
| 32 |
+
tau: float = 0.10 # adaptive: accumulated-change tolerance
|
| 33 |
+
warmup_steps: int = 5 # always compute the first K steps
|
| 34 |
+
cooldown_steps: int = 5 # always compute the last K steps (needs total_steps)
|
| 35 |
+
total_steps: int = 50
|
| 36 |
+
static_keep_every: int = 2 # static: compute every k-th step in the middle
|
| 37 |
+
num_branches: int = 2 # CFG cond+uncond
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class CachedTransformer(torch.nn.Module):
|
| 41 |
+
"""Wraps a diffusers transformer; drop-in via `pipe.transformer = CachedTransformer(pipe.transformer, cfg)`."""
|
| 42 |
+
|
| 43 |
+
def __init__(self, transformer: torch.nn.Module, cfg: CacheConfig):
|
| 44 |
+
super().__init__()
|
| 45 |
+
self.t = transformer
|
| 46 |
+
self.cfg = cfg
|
| 47 |
+
self.dtype = transformer.dtype
|
| 48 |
+
self.config = transformer.config # pipeline reads .config
|
| 49 |
+
self._call = 0
|
| 50 |
+
self._states = [_BranchState() for _ in range(cfg.num_branches)]
|
| 51 |
+
|
| 52 |
+
# pipeline may call other attrs; delegate
|
| 53 |
+
def __getattr__(self, name):
|
| 54 |
+
try:
|
| 55 |
+
return super().__getattr__(name)
|
| 56 |
+
except AttributeError:
|
| 57 |
+
return getattr(self.t, name)
|
| 58 |
+
|
| 59 |
+
def reset(self):
|
| 60 |
+
self._call = 0
|
| 61 |
+
self._states = [_BranchState() for _ in range(self.cfg.num_branches)]
|
| 62 |
+
|
| 63 |
+
def stats(self):
|
| 64 |
+
return {
|
| 65 |
+
f"branch{i}": {"skips": s.skips, "computes": s.computes}
|
| 66 |
+
for i, s in enumerate(self._states)
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
def _should_compute(self, st: _BranchState, x: torch.Tensor) -> bool:
|
| 70 |
+
c = self.cfg
|
| 71 |
+
if c.mode == "off" or st.delta is None:
|
| 72 |
+
return True
|
| 73 |
+
if st.step < c.warmup_steps or st.step >= c.total_steps - c.cooldown_steps:
|
| 74 |
+
return True
|
| 75 |
+
if c.mode == "static":
|
| 76 |
+
return (st.step % c.static_keep_every) == 0
|
| 77 |
+
# adaptive: estimate output change rate from input change (EasyCache-style)
|
| 78 |
+
if st.prev_input is None:
|
| 79 |
+
return True
|
| 80 |
+
dx = (x - st.prev_input).float().norm() / max(st.prev_input.float().norm().item(), 1e-8)
|
| 81 |
+
st.accum += dx.item()
|
| 82 |
+
return st.accum > c.tau
|
| 83 |
+
|
| 84 |
+
def forward(self, hidden_states, *args, **kwargs):
|
| 85 |
+
st = self._states[self._call % self.cfg.num_branches]
|
| 86 |
+
self._call += 1
|
| 87 |
+
x = hidden_states
|
| 88 |
+
|
| 89 |
+
if self._should_compute(st, x):
|
| 90 |
+
out = self.t(hidden_states, *args, **kwargs)
|
| 91 |
+
sample = out[0] if isinstance(out, tuple) else out.sample
|
| 92 |
+
st.delta = (sample - x).detach()
|
| 93 |
+
st.prev_input = x.detach()
|
| 94 |
+
st.prev_output = sample.detach()
|
| 95 |
+
st.accum = 0.0
|
| 96 |
+
st.computes += 1
|
| 97 |
+
st.step += 1
|
| 98 |
+
return out
|
| 99 |
+
# skip: approximate v(x_t) ~= x_t + cached delta
|
| 100 |
+
st.skips += 1
|
| 101 |
+
st.step += 1
|
| 102 |
+
approx = x + st.delta
|
| 103 |
+
return (approx,)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class CacheHandle:
|
| 107 |
+
"""Handle over every wrapped transformer of a pipeline (Wan 2.2 has two)."""
|
| 108 |
+
|
| 109 |
+
def __init__(self, wrapped: list[CachedTransformer]):
|
| 110 |
+
self.wrapped = wrapped
|
| 111 |
+
|
| 112 |
+
def reset(self):
|
| 113 |
+
for w in self.wrapped:
|
| 114 |
+
w.reset()
|
| 115 |
+
|
| 116 |
+
def stats(self):
|
| 117 |
+
return {f"transformer{i}": w.stats() for i, w in enumerate(self.wrapped)}
|
| 118 |
+
|
| 119 |
+
@property
|
| 120 |
+
def cfg(self) -> CacheConfig:
|
| 121 |
+
return self.wrapped[0].cfg
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def wrap_pipeline(pipe, cfg: CacheConfig) -> CacheHandle:
|
| 125 |
+
"""Attach caching to a diffusers video pipeline. Call .reset() between generations.
|
| 126 |
+
|
| 127 |
+
Wraps ``pipe.transformer`` and, when present, ``pipe.transformer_2`` (Wan 2.2).
|
| 128 |
+
Calling it again with a new config reconfigures the existing wrappers in place.
|
| 129 |
+
"""
|
| 130 |
+
wrapped = []
|
| 131 |
+
for name in ("transformer", "transformer_2"):
|
| 132 |
+
model = getattr(pipe, name, None)
|
| 133 |
+
if model is None:
|
| 134 |
+
continue
|
| 135 |
+
if isinstance(model, CachedTransformer):
|
| 136 |
+
model.cfg = cfg
|
| 137 |
+
model.reset()
|
| 138 |
+
else:
|
| 139 |
+
model = CachedTransformer(model, cfg)
|
| 140 |
+
setattr(pipe, name, model)
|
| 141 |
+
wrapped.append(model)
|
| 142 |
+
if not wrapped:
|
| 143 |
+
raise AttributeError("The pipeline exposes no `transformer` to wrap.")
|
| 144 |
+
return CacheHandle(wrapped)
|
cachedsearch/verifiers.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verifiers: the scorer that ranks candidates.
|
| 2 |
+
|
| 3 |
+
CachedSearch is verifier-agnostic. Any callable ``(frames, prompt) -> float``
|
| 4 |
+
works, higher meaning better. If you do not pass one, we use ImageReward
|
| 5 |
+
averaged over uniformly spaced frames, which is the verifier all of the
|
| 6 |
+
paper's headline numbers use.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Callable
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
Verifier = Callable[[object, str], float]
|
| 15 |
+
|
| 16 |
+
_DEFAULT = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def imagereward_verifier(device: str = "cuda", num_frames: int = 8) -> Verifier:
|
| 20 |
+
"""ImageReward averaged over `num_frames` uniformly spaced frames.
|
| 21 |
+
|
| 22 |
+
Requires the `image-reward` package; the model downloads on first use.
|
| 23 |
+
"""
|
| 24 |
+
import ImageReward as RM
|
| 25 |
+
from PIL import Image
|
| 26 |
+
|
| 27 |
+
model = RM.load("ImageReward-v1.0", device=device)
|
| 28 |
+
|
| 29 |
+
def score(frames, prompt: str) -> float:
|
| 30 |
+
idx = np.linspace(0, len(frames) - 1, min(num_frames, len(frames))).astype(int)
|
| 31 |
+
vals = [
|
| 32 |
+
model.score(prompt, Image.fromarray(np.asarray(frames[i]).astype("uint8")))
|
| 33 |
+
for i in idx
|
| 34 |
+
]
|
| 35 |
+
return float(np.mean(vals))
|
| 36 |
+
|
| 37 |
+
return score
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def default_verifier(device: str = "cuda") -> Verifier:
|
| 41 |
+
"""The default scorer, loaded once and reused across calls."""
|
| 42 |
+
global _DEFAULT
|
| 43 |
+
if _DEFAULT is None:
|
| 44 |
+
try:
|
| 45 |
+
_DEFAULT = imagereward_verifier(device=device)
|
| 46 |
+
except ImportError as exc: # keep the failure actionable
|
| 47 |
+
raise ImportError(
|
| 48 |
+
"No verifier was passed and the default (ImageReward) is not "
|
| 49 |
+
"installed. Either `pip install image-reward`, or pass your own "
|
| 50 |
+
"scorer: cached_search(pipe, prompt, verifier=my_scorer)."
|
| 51 |
+
) from exc
|
| 52 |
+
return _DEFAULT
|
examples/calibrate_new_model.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Calibrate the caching threshold for a model we did not test.
|
| 2 |
+
|
| 3 |
+
python examples/calibrate_new_model.py --model <hf-id> --prompts data/calib25.txt
|
| 4 |
+
|
| 5 |
+
Costs about two GPU-hours for a small model (25 prompts x 8 seeds, twice).
|
| 6 |
+
Fidelity tracks architecture family, not parameter size, so calibrate once per
|
| 7 |
+
family and reuse the threshold across sizes within it.
|
| 8 |
+
"""
|
| 9 |
+
import argparse
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from diffusers import DiffusionPipeline
|
| 13 |
+
|
| 14 |
+
from cachedsearch import calibrate_tau
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
ap = argparse.ArgumentParser()
|
| 19 |
+
ap.add_argument("--model", required=True)
|
| 20 |
+
ap.add_argument("--prompts", required=True, help="text file, one prompt per line")
|
| 21 |
+
ap.add_argument("--num-prompts", type=int, default=25)
|
| 22 |
+
ap.add_argument("--taus", default="0.02,0.05,0.10,0.20")
|
| 23 |
+
ap.add_argument("--target-capture", type=float, default=0.90)
|
| 24 |
+
args = ap.parse_args()
|
| 25 |
+
|
| 26 |
+
prompts = [l.strip() for l in open(args.prompts) if l.strip()][: args.num_prompts]
|
| 27 |
+
pipe = DiffusionPipeline.from_pretrained(args.model, torch_dtype=torch.bfloat16).to("cuda")
|
| 28 |
+
|
| 29 |
+
report = calibrate_tau(
|
| 30 |
+
pipe,
|
| 31 |
+
prompts,
|
| 32 |
+
taus=[float(t) for t in args.taus.split(",")],
|
| 33 |
+
target_capture=args.target_capture,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
print(f"{'tau':>6} {'speedup':>9} {'capture':>9} {'median rho':>11}")
|
| 37 |
+
for tau, row in report.items():
|
| 38 |
+
if tau == "recommended":
|
| 39 |
+
continue
|
| 40 |
+
print(f"{tau:>6} {row['speedup']:>8.2f}x {100*row['capture']:>8.1f}% {row['spearman']:>11.3f}")
|
| 41 |
+
print(f"\nrecommended tau for this model: {report['recommended']}")
|
| 42 |
+
print("Use it as: cached_search(pipe, prompt, verifier, tau=<value>)")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|
examples/run_wan.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""End-to-end example: best-of-8 search on Wan2.1 for the price of about 5 rollouts.
|
| 2 |
+
|
| 3 |
+
python examples/run_wan.py --prompt "a red fox running through deep snow"
|
| 4 |
+
|
| 5 |
+
Swap the pipeline for CogVideoX, HunyuanVideo, or LTX-Video and the only thing
|
| 6 |
+
that changes is tau (run examples/calibrate_new_model.py once for a new family).
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from diffusers import AutoencoderKLWan, WanPipeline
|
| 12 |
+
|
| 13 |
+
from cachedsearch import cached_search, imagereward_verifier
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
ap = argparse.ArgumentParser()
|
| 18 |
+
ap.add_argument("--prompt", required=True)
|
| 19 |
+
ap.add_argument("--model", default="Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
|
| 20 |
+
ap.add_argument("--n", type=int, default=8, help="search width")
|
| 21 |
+
ap.add_argument("--tau", type=float, default=0.10, help="caching threshold")
|
| 22 |
+
ap.add_argument("--mode", default="commit", choices=["commit", "keep"])
|
| 23 |
+
ap.add_argument("--out", default="delivered.mp4")
|
| 24 |
+
# Pass --verifier to swap in your own scorer; the default is ImageReward.
|
| 25 |
+
args = ap.parse_args()
|
| 26 |
+
|
| 27 |
+
vae = AutoencoderKLWan.from_pretrained(args.model, subfolder="vae", torch_dtype=torch.float32)
|
| 28 |
+
pipe = WanPipeline.from_pretrained(args.model, vae=vae, torch_dtype=torch.bfloat16).to("cuda")
|
| 29 |
+
|
| 30 |
+
result = cached_search(
|
| 31 |
+
pipe,
|
| 32 |
+
args.prompt,
|
| 33 |
+
n=args.n,
|
| 34 |
+
tau=args.tau,
|
| 35 |
+
mode=args.mode,
|
| 36 |
+
gen_kwargs=dict(height=480, width=832, num_frames=81, guidance_scale=5.0),
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
print(f"winner seed : {result.seed}")
|
| 40 |
+
print(f"draft scores : {[round(s, 3) for s in result.draft_scores]}")
|
| 41 |
+
print(f"explore / commit : {result.explore_seconds:.0f}s / {result.commit_seconds:.0f}s")
|
| 42 |
+
print(f"total : {result.total_seconds:.0f}s "
|
| 43 |
+
f"(full best-of-{args.n} would cost about "
|
| 44 |
+
f"{args.n * result.commit_seconds:.0f}s)")
|
| 45 |
+
|
| 46 |
+
from diffusers.utils import export_to_video
|
| 47 |
+
export_to_video(result.video, args.out, fps=16)
|
| 48 |
+
print(f"wrote {args.out}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
main()
|