Add approximate H3 cache modes
This commit is contained in:
parent
fb257ef982
commit
c3cc04e98d
5 changed files with 126 additions and 16 deletions
|
|
@ -64,3 +64,9 @@ Exact memory/lifetime options:
|
||||||
|
|
||||||
- `attention: "kj_head_sliced"` slices attention heads and runs the slice backend from `H3_HEAD_SLICE_BACKEND` (`sage2` by default) with `H3_HEAD_SLICE_SIZE` heads per slice (`8` by default).
|
- `attention: "kj_head_sliced"` slices attention heads and runs the slice backend from `H3_HEAD_SLICE_BACKEND` (`sage2` by default) with `H3_HEAD_SLICE_SIZE` heads per slice (`8` by default).
|
||||||
- `--mlp-chunks N` on `tools/serve_hot_runtime.py` or `tools/direct_t2v_preview.py` chunks H3 SwiGLU rows exactly to reduce peak activation memory. Default is `1` (disabled).
|
- `--mlp-chunks N` on `tools/serve_hot_runtime.py` or `tools/direct_t2v_preview.py` chunks H3 SwiGLU rows exactly to reduce peak activation memory. Default is `1` (disabled).
|
||||||
|
|
||||||
|
Approximate cache options are opt-in and must be quality-gated per prompt:
|
||||||
|
|
||||||
|
- `cache_mode: "easycache"` reuses cached denoised deltas while cumulative latent input change stays below `cache_threshold`.
|
||||||
|
- `cache_mode: "h3_cache"` reuses cached denoised deltas when the current per-step latent input change is below `cache_threshold`.
|
||||||
|
- Both modes accept `cache_start_percent`, `cache_end_percent`, and `cache_subsample_factor` in `POST /generate`; the CLI exposes equivalent `--cache-*` flags.
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,14 @@ class H3HotRuntime:
|
||||||
mux_audio: bool = True,
|
mux_audio: bool = True,
|
||||||
ffmpeg_loglevel: str = "error",
|
ffmpeg_loglevel: str = "error",
|
||||||
save_latent: str | Path | None = None,
|
save_latent: str | Path | None = None,
|
||||||
|
cache_mode: str | None = None,
|
||||||
|
cache_threshold: float = 0.0,
|
||||||
|
cache_start_percent: float = 0.0,
|
||||||
|
cache_end_percent: float = 1.0,
|
||||||
|
cache_subsample_factor: int = 2,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
stages: list[dict] = []
|
stages: list[dict] = []
|
||||||
|
cache_stats: dict = {}
|
||||||
|
|
||||||
def timed(stage: str, fn):
|
def timed(stage: str, fn):
|
||||||
_sync()
|
_sync()
|
||||||
|
|
@ -172,6 +178,12 @@ class H3HotRuntime:
|
||||||
audio,
|
audio,
|
||||||
steps=steps,
|
steps=steps,
|
||||||
return_audio=mux_audio,
|
return_audio=mux_audio,
|
||||||
|
cache_mode=cache_mode,
|
||||||
|
cache_threshold=cache_threshold,
|
||||||
|
cache_start_percent=cache_start_percent,
|
||||||
|
cache_end_percent=cache_end_percent,
|
||||||
|
cache_subsample_factor=cache_subsample_factor,
|
||||||
|
cache_stats=cache_stats,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if mux_audio:
|
if mux_audio:
|
||||||
|
|
@ -241,5 +253,6 @@ class H3HotRuntime:
|
||||||
"vae_dtype": self.config.vae_dtype,
|
"vae_dtype": self.config.vae_dtype,
|
||||||
"vae_tile_size": self.config.vae_tile_size,
|
"vae_tile_size": self.config.vae_tile_size,
|
||||||
"stages": stages,
|
"stages": stages,
|
||||||
|
"cache": cache_stats,
|
||||||
"request_seconds": sum(stage["seconds"] for stage in stages),
|
"request_seconds": sum(stage["seconds"] for stage in stages),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ def _decode_audio_latent(audio_carried: torch.Tensor, *, shift_video: float = 12
|
||||||
return audio_carried * (shift_audio / shift_video)
|
return audio_carried * (shift_audio / shift_video)
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_sample(x: torch.Tensor, factor: int) -> torch.Tensor:
|
||||||
|
if factor <= 1:
|
||||||
|
return x
|
||||||
|
if x.ndim == 5:
|
||||||
|
return x[..., ::factor, ::factor]
|
||||||
|
return x[..., ::factor]
|
||||||
|
|
||||||
|
|
||||||
@torch.inference_mode()
|
@torch.inference_mode()
|
||||||
def sample_video_res_multistep(
|
def sample_video_res_multistep(
|
||||||
model,
|
model,
|
||||||
|
|
@ -69,6 +77,12 @@ def sample_video_res_multistep(
|
||||||
model_timesteps: list[torch.Tensor] | tuple[torch.Tensor, ...] | None = None,
|
model_timesteps: list[torch.Tensor] | tuple[torch.Tensor, ...] | None = None,
|
||||||
return_audio: bool = False,
|
return_audio: bool = False,
|
||||||
progress: bool = False,
|
progress: bool = False,
|
||||||
|
cache_mode: str | None = None,
|
||||||
|
cache_threshold: float = 0.0,
|
||||||
|
cache_start_percent: float = 0.0,
|
||||||
|
cache_end_percent: float = 1.0,
|
||||||
|
cache_subsample_factor: int = 2,
|
||||||
|
cache_stats: dict | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""Direct H3 beta/RES sampling with Comfy-equivalent joint AV carry semantics."""
|
"""Direct H3 beta/RES sampling with Comfy-equivalent joint AV carry semantics."""
|
||||||
sigmas = beta_sigmas(steps, device=video.device)
|
sigmas = beta_sigmas(steps, device=video.device)
|
||||||
|
|
@ -77,10 +91,47 @@ def sample_video_res_multistep(
|
||||||
video_history_sigma = audio_history_sigma = None
|
video_history_sigma = audio_history_sigma = None
|
||||||
total_steps = len(sigmas) - 1
|
total_steps = len(sigmas) - 1
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
|
cache_mode = None if cache_mode in {None, "", "disabled", "none"} else cache_mode
|
||||||
|
if cache_mode not in {None, "easycache", "h3_cache"}:
|
||||||
|
raise ValueError(f"Unsupported cache mode: {cache_mode}")
|
||||||
|
if cache_stats is not None:
|
||||||
|
cache_stats.update({"mode": cache_mode, "threshold": cache_threshold, "skipped_steps": 0, "rates": []})
|
||||||
|
cache = {
|
||||||
|
"video_diff": None,
|
||||||
|
"audio_diff": None,
|
||||||
|
"video_prev": None,
|
||||||
|
"audio_prev": None,
|
||||||
|
"prev_norm": None,
|
||||||
|
"cumulative_rate": 0.0,
|
||||||
|
}
|
||||||
for index, sigma in enumerate(sigmas[:-1], start=1):
|
for index, sigma in enumerate(sigmas[:-1], start=1):
|
||||||
step_started = time.perf_counter()
|
step_started = time.perf_counter()
|
||||||
previous_index = index - 1
|
previous_index = index - 1
|
||||||
sigma_down = sigmas[index]
|
sigma_down = sigmas[index]
|
||||||
|
current_percent = previous_index / total_steps
|
||||||
|
can_cache = cache_mode is not None and cache_threshold > 0 and cache_start_percent <= current_percent <= cache_end_percent and cache["video_diff"] is not None
|
||||||
|
skipped = False
|
||||||
|
if can_cache:
|
||||||
|
video_now = _cache_sample(video, cache_subsample_factor)
|
||||||
|
audio_now = _cache_sample(audio_carried, cache_subsample_factor)
|
||||||
|
input_change = (video_now - cache["video_prev"]).flatten().abs().mean() + (audio_now - cache["audio_prev"]).flatten().abs().mean()
|
||||||
|
input_norm = cache["prev_norm"].clamp_min(1e-8)
|
||||||
|
rate = (input_change / input_norm).item()
|
||||||
|
if cache_mode == "easycache":
|
||||||
|
cache["cumulative_rate"] += rate
|
||||||
|
skipped = cache["cumulative_rate"] < cache_threshold
|
||||||
|
if not skipped:
|
||||||
|
cache["cumulative_rate"] = 0.0
|
||||||
|
else:
|
||||||
|
skipped = rate < cache_threshold
|
||||||
|
if cache_stats is not None:
|
||||||
|
cache_stats["rates"].append({"step": previous_index, "rate": rate, "skipped": skipped})
|
||||||
|
if skipped:
|
||||||
|
video_denoised = video + cache["video_diff"]
|
||||||
|
audio_denoised = audio_carried + cache["audio_diff"]
|
||||||
|
if cache_stats is not None:
|
||||||
|
cache_stats["skipped_steps"] += 1
|
||||||
|
else:
|
||||||
sigma_audio = _audio_sigma(sigma)
|
sigma_audio = _audio_sigma(sigma)
|
||||||
carry = sigma_audio / sigma
|
carry = sigma_audio / sigma
|
||||||
native_audio = audio_carried.to(torch.bfloat16) * carry
|
native_audio = audio_carried.to(torch.bfloat16) * carry
|
||||||
|
|
@ -96,6 +147,12 @@ def sample_video_res_multistep(
|
||||||
).float()
|
).float()
|
||||||
video_denoised = video - sigma * velocity_video
|
video_denoised = video - sigma * velocity_video
|
||||||
audio_denoised = audio_carried - sigma * velocity_audio
|
audio_denoised = audio_carried - sigma * velocity_audio
|
||||||
|
if cache_mode is not None:
|
||||||
|
cache["video_diff"] = (video_denoised - video).detach()
|
||||||
|
cache["audio_diff"] = (audio_denoised - audio_carried).detach()
|
||||||
|
cache["video_prev"] = _cache_sample(video, cache_subsample_factor).detach().clone()
|
||||||
|
cache["audio_prev"] = _cache_sample(audio_carried, cache_subsample_factor).detach().clone()
|
||||||
|
cache["prev_norm"] = video.flatten().abs().mean() + audio_carried.flatten().abs().mean()
|
||||||
previous_sigma = sigmas[previous_index - 1] if previous_index else None
|
previous_sigma = sigmas[previous_index - 1] if previous_index else None
|
||||||
video = res_multistep_update(video, video_denoised, sigma, sigma_down, video_history, video_history_sigma, previous_sigma)
|
video = res_multistep_update(video, video_denoised, sigma, sigma_down, video_history, video_history_sigma, previous_sigma)
|
||||||
audio_carried = res_multistep_update(audio_carried, audio_denoised, sigma, sigma_down, audio_history, audio_history_sigma, previous_sigma)
|
audio_carried = res_multistep_update(audio_carried, audio_denoised, sigma, sigma_down, audio_history, audio_history_sigma, previous_sigma)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@ parser.add_argument("--vae-dtype", choices=("float32", "float16", "bfloat16"), d
|
||||||
parser.add_argument("--vae-tile-size", type=int, default=int(os.getenv("H3_VAE_TILE_SIZE", "256")))
|
parser.add_argument("--vae-tile-size", type=int, default=int(os.getenv("H3_VAE_TILE_SIZE", "256")))
|
||||||
parser.add_argument("--mlp-chunks", type=int, default=int(os.getenv("H3_MLP_CHUNKS", "1")))
|
parser.add_argument("--mlp-chunks", type=int, default=int(os.getenv("H3_MLP_CHUNKS", "1")))
|
||||||
parser.add_argument("--mlp-chunk-threshold", type=int, default=int(os.getenv("H3_MLP_CHUNK_THRESHOLD", "4096")))
|
parser.add_argument("--mlp-chunk-threshold", type=int, default=int(os.getenv("H3_MLP_CHUNK_THRESHOLD", "4096")))
|
||||||
|
parser.add_argument("--cache-mode", choices=("disabled", "easycache", "h3_cache"), default="disabled")
|
||||||
|
parser.add_argument("--cache-threshold", type=float, default=0.0)
|
||||||
|
parser.add_argument("--cache-start-percent", type=float, default=0.0)
|
||||||
|
parser.add_argument("--cache-end-percent", type=float, default=1.0)
|
||||||
|
parser.add_argument("--cache-subsample-factor", type=int, default=2)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
last_report = started
|
last_report = started
|
||||||
|
|
@ -101,7 +106,26 @@ if args.model_timesteps_capture is not None:
|
||||||
for index in range(args.steps)
|
for index in range(args.steps)
|
||||||
]
|
]
|
||||||
want_audio = args.save_audio_latent is not None or args.audio_output is not None or args.mux_audio
|
want_audio = args.save_audio_latent is not None or args.audio_output is not None or args.mux_audio
|
||||||
sampled = sample_video_res_multistep(model, H3PromptPacker(checkpoint), text, video, audio, steps=args.steps, model_timesteps=model_timesteps, return_audio=want_audio, progress=args.progress)
|
cache_stats = {}
|
||||||
|
sampled = sample_video_res_multistep(
|
||||||
|
model,
|
||||||
|
H3PromptPacker(checkpoint),
|
||||||
|
text,
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
steps=args.steps,
|
||||||
|
model_timesteps=model_timesteps,
|
||||||
|
return_audio=want_audio,
|
||||||
|
progress=args.progress,
|
||||||
|
cache_mode=args.cache_mode,
|
||||||
|
cache_threshold=args.cache_threshold,
|
||||||
|
cache_start_percent=args.cache_start_percent,
|
||||||
|
cache_end_percent=args.cache_end_percent,
|
||||||
|
cache_subsample_factor=args.cache_subsample_factor,
|
||||||
|
cache_stats=cache_stats,
|
||||||
|
)
|
||||||
|
if cache_stats:
|
||||||
|
report({"cache": cache_stats})
|
||||||
if want_audio:
|
if want_audio:
|
||||||
latent, audio_latent = sampled
|
latent, audio_latent = sampled
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
mux_audio = bool(payload.get("mux_audio", True))
|
mux_audio = bool(payload.get("mux_audio", True))
|
||||||
ffmpeg_loglevel = payload.get("ffmpeg_loglevel", "error")
|
ffmpeg_loglevel = payload.get("ffmpeg_loglevel", "error")
|
||||||
save_latent = payload.get("save_latent")
|
save_latent = payload.get("save_latent")
|
||||||
|
cache_mode = payload.get("cache_mode")
|
||||||
|
cache_threshold = float(payload.get("cache_threshold", 0.0))
|
||||||
|
cache_start_percent = float(payload.get("cache_start_percent", 0.0))
|
||||||
|
cache_end_percent = float(payload.get("cache_end_percent", 1.0))
|
||||||
|
cache_subsample_factor = int(payload.get("cache_subsample_factor", 2))
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
with runtime_lock:
|
with runtime_lock:
|
||||||
result = runtime.generate(
|
result = runtime.generate(
|
||||||
|
|
@ -106,6 +111,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
mux_audio=mux_audio,
|
mux_audio=mux_audio,
|
||||||
ffmpeg_loglevel=ffmpeg_loglevel,
|
ffmpeg_loglevel=ffmpeg_loglevel,
|
||||||
save_latent=save_latent,
|
save_latent=save_latent,
|
||||||
|
cache_mode=cache_mode,
|
||||||
|
cache_threshold=cache_threshold,
|
||||||
|
cache_start_percent=cache_start_percent,
|
||||||
|
cache_end_percent=cache_end_percent,
|
||||||
|
cache_subsample_factor=cache_subsample_factor,
|
||||||
)
|
)
|
||||||
result["wall_seconds"] = time.perf_counter() - started
|
result["wall_seconds"] = time.perf_counter() - started
|
||||||
write_json(self, 200, result)
|
write_json(self, 200, result)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue