Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

539 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: Apache-2.0
"""
RIFE 4.22.lite frame interpolation for SGLang diffusion pipelines.
RIFE model code is vendored and adapted from:
- https://github.com/hzwer/ECCV2022-RIFE (MIT License)
- https://github.com/hzwer/Practical-RIFE (MIT License)
Copyright (c) 2021 Zhewei Huang
The FrameInterpolator wrapper and integration code are original work.
"""
import os
import time
from typing import Optional
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# Default HuggingFace repo for RIFE 4.22.lite weights
_DEFAULT_RIFE_HF_REPO = "elfgum/RIFE-4.22.lite"
# Module-level cache: model_path -> Model instance
_MODEL_CACHE: dict[str, "Model"] = {}
_MAX_RIFE_BATCH_PAIRS = 16
# ---------------------------------------------------------------------------
# Vendored RIFE 4.22.lite model code
# (IFBlock, IFNet_HDv3 backbone, Model wrapper)
# ---------------------------------------------------------------------------
def warp(tenInput: torch.Tensor, tenFlow: torch.Tensor) -> torch.Tensor:
"""Warp tenInput by tenFlow using grid_sample."""
# Build base grid for the current size
tenHorizontal = (
torch.linspace(-1.0, 1.0, tenFlow.shape[3], device=tenFlow.device)
.view(1, 1, 1, tenFlow.shape[3])
.expand(tenFlow.shape[0], -1, tenFlow.shape[2], -1)
)
tenVertical = (
torch.linspace(-1.0, 1.0, tenFlow.shape[2], device=tenFlow.device)
.view(1, 1, tenFlow.shape[2], 1)
.expand(tenFlow.shape[0], -1, -1, tenFlow.shape[3])
)
tenGrid = torch.cat([tenHorizontal, tenVertical], dim=1)
tenFlow = torch.cat(
[
tenFlow[:, 0:1, :, :] / ((tenInput.shape[3] - 1.0) / 2.0),
tenFlow[:, 1:2, :, :] / ((tenInput.shape[2] - 1.0) / 2.0),
],
dim=1,
)
grid = (tenGrid + tenFlow).permute(0, 2, 3, 1)
return F.grid_sample(
input=tenInput,
grid=grid,
mode="bilinear",
padding_mode="border",
align_corners=True,
)
def _conv(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1):
"""Conv2d + LeakyReLU helper (matches RIFE 4.22 conv())."""
return nn.Sequential(
nn.Conv2d(
in_planes,
out_planes,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
bias=True,
),
nn.LeakyReLU(0.2, True),
)
class ResConv(nn.Module):
"""Residual convolution block with learnable beta scaling (RIFE 4.22)."""
def __init__(self, c: int, dilation: int = 1):
super().__init__()
self.conv = nn.Conv2d(c, c, 3, 1, dilation, dilation=dilation, groups=1)
self.beta = nn.Parameter(torch.ones((1, c, 1, 1)), requires_grad=True)
self.relu = nn.LeakyReLU(0.2, True)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.relu(self.conv(x) * self.beta + x)
class IFBlock(nn.Module):
"""Single-scale optical flow + mask + feature block (RIFE 4.22)."""
def __init__(self, in_planes: int, c: int = 64):
super().__init__()
self.conv0 = nn.Sequential(
_conv(in_planes, c // 2, 3, 2, 1),
_conv(c // 2, c, 3, 2, 1),
)
self.convblock = nn.Sequential(
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
)
self.lastconv = nn.Sequential(
nn.ConvTranspose2d(c, 4 * 13, 4, 2, 1),
nn.PixelShuffle(2),
)
def forward(
self,
x: torch.Tensor,
flow: Optional[torch.Tensor] = None,
scale: float = 1.0,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
x = F.interpolate(
x, scale_factor=1.0 / scale, mode="bilinear", align_corners=False
)
if flow is not None:
flow = (
F.interpolate(
flow,
scale_factor=1.0 / scale,
mode="bilinear",
align_corners=False,
)
* 1.0
/ scale
)
x = torch.cat((x, flow), 1)
feat = self.conv0(x)
feat = self.convblock(feat)
tmp = self.lastconv(feat)
tmp = F.interpolate(
tmp, scale_factor=scale, mode="bilinear", align_corners=False
)
flow = tmp[:, :4] * scale
mask = tmp[:, 4:5]
feat = tmp[:, 5:]
return flow, mask, feat
class Head(nn.Module):
"""Feature encoder producing 4-channel features at full resolution (RIFE 4.22)."""
def __init__(self):
super().__init__()
self.cnn0 = nn.Conv2d(3, 16, 3, 2, 1)
self.cnn1 = nn.Conv2d(16, 16, 3, 1, 1)
self.cnn2 = nn.Conv2d(16, 16, 3, 1, 1)
self.cnn3 = nn.ConvTranspose2d(16, 4, 4, 2, 1)
self.relu = nn.LeakyReLU(0.2, True)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x0 = self.cnn0(x)
x = self.relu(x0)
x1 = self.cnn1(x)
x = self.relu(x1)
x2 = self.cnn2(x)
x = self.relu(x2)
x3 = self.cnn3(x)
return x3
class IFNet(nn.Module):
"""4-scale IFNet optical flow network (RIFE 4.22 backbone)."""
def __init__(self):
super().__init__()
self.block0 = IFBlock(7 + 8, c=192)
self.block1 = IFBlock(8 + 4 + 8 + 8, c=128)
self.block2 = IFBlock(8 + 4 + 8 + 8, c=64)
self.block3 = IFBlock(8 + 4 + 8 + 8, c=32)
self.encode = Head()
def forward(
self,
x: torch.Tensor,
timestep: float = 0.5,
scale_list: Optional[list] = None,
) -> tuple[list, torch.Tensor, list]:
if scale_list is None:
scale_list = [8, 4, 2, 1]
channel = x.shape[1] // 2
img0 = x[:, :channel]
img1 = x[:, channel:]
if not torch.is_tensor(timestep):
timestep = (x[:, :1].clone() * 0 + 1) * timestep
else:
timestep = timestep.repeat(1, 1, img0.shape[2], img0.shape[3])
f0 = self.encode(img0[:, :3])
f1 = self.encode(img1[:, :3])
flow_list = []
merged = []
mask_list = []
warped_img0 = img0
warped_img1 = img1
flow = None
mask = None
block = [self.block0, self.block1, self.block2, self.block3]
for i in range(4):
if flow is None:
flow, mask, feat = block[i](
torch.cat((img0[:, :3], img1[:, :3], f0, f1, timestep), 1),
None,
scale=scale_list[i],
)
else:
wf0 = warp(f0, flow[:, :2])
wf1 = warp(f1, flow[:, 2:4])
fd, m0, feat = block[i](
torch.cat(
(
warped_img0[:, :3],
warped_img1[:, :3],
wf0,
wf1,
timestep,
mask,
feat,
),
1,
),
flow,
scale=scale_list[i],
)
mask = m0
flow = flow + fd
mask_list.append(mask)
flow_list.append(flow)
warped_img0 = warp(img0, flow[:, :2])
warped_img1 = warp(img1, flow[:, 2:4])
merged.append((warped_img0, warped_img1))
mask = torch.sigmoid(mask)
merged[3] = warped_img0 * mask + warped_img1 * (1 - mask)
return flow_list, mask_list[3], merged
class Model:
"""Wraps IFNet, provides load_model() and inference() API."""
def __init__(self):
self.flownet = IFNet()
self.device_type: str = "cpu"
def eval(self) -> "Model":
self.flownet.eval()
return self
def device(self) -> torch.device:
return next(self.flownet.parameters()).device
def load_model(self, path: str, strip_module_prefix: bool = True) -> None:
"""Load weights from {path}/flownet.pkl.
Args:
path: Directory containing ``flownet.pkl``.
strip_module_prefix: If True, strip the ``module.`` prefix that
``DataParallel`` / ``DistributedDataParallel`` adds to keys.
"""
flownet_path = os.path.join(path, "flownet.pkl")
if not os.path.isfile(flownet_path):
raise FileNotFoundError(
f"RIFE weight file not found: {flownet_path}\n"
"Expected layout: <model_path>/flownet.pkl"
)
def convert(param):
if strip_module_prefix:
return {
k.replace("module.", ""): v
for k, v in param.items()
if "module." in k
}
else:
return {k: v for k, v in param.items() if "module." not in k}
state = torch.load(flownet_path, map_location="cpu", weights_only=False)
self.flownet.load_state_dict(convert(state), strict=False)
logger.info("Loaded RIFE weights from %s", flownet_path)
def inference(
self,
img0: torch.Tensor,
img1: torch.Tensor,
scale: float = 1.0,
timestep: float = 0.5,
) -> torch.Tensor:
"""Interpolate a single intermediate frame between img0 and img1."""
n, c, h, w = img0.shape
# Pad to multiples of 32 so that RIFE's downsample/upsample round-trips
# preserve spatial dimensions exactly.
ph = ((h - 1) // 32 + 1) * 32
pw = ((w - 1) // 32 + 1) * 32
pad = (0, pw - w, 0, ph - h)
img0 = F.pad(img0, pad)
img1 = F.pad(img1, pad)
imgs = torch.cat((img0, img1), 1)
scale_list = [8 / scale, 4 / scale, 2 / scale, 1 / scale]
with torch.no_grad():
flow_list, mask, merged = self.flownet(
imgs,
timestep=timestep,
scale_list=scale_list,
)
# Crop back to original resolution
return merged[3][:, :, :h, :w]
# ---------------------------------------------------------------------------
# FrameInterpolator public class
# ---------------------------------------------------------------------------
class FrameInterpolator:
"""
Lazy-loaded RIFE 4.22.lite frame interpolator.
Weights are loaded on first call to `.interpolate()` and cached globally
per model_path to avoid reloading across requests.
"""
def __init__(self, model_path: Optional[str] = None):
self._model_path = model_path
self._resolved_path: Optional[str] = None
def _ensure_model_loaded(self) -> Model:
"""Load RIFE model weights.
Accepts a local directory **or** a HuggingFace repo ID. When *None*
(the default) the weights are downloaded (and cached) automatically
from ``elfgum/RIFE-4.22.lite`` via ``maybe_download_model()``.
"""
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import (
maybe_download_model,
)
model_path = self._model_path or _DEFAULT_RIFE_HF_REPO
# Resolve: local path pass-through, HF repo ID → download & cache
model_path = maybe_download_model(model_path)
self._resolved_path = model_path
if model_path in _MODEL_CACHE:
return _MODEL_CACHE[model_path]
device = current_platform.get_local_torch_device()
model = Model()
model.load_model(model_path, strip_module_prefix=True)
model.eval()
model.flownet = model.flownet.to(device)
_MODEL_CACHE[model_path] = model
logger.info("RIFE model loaded on device: %s", device)
return model
@staticmethod
def _frame_to_tensor(frame: np.ndarray, device: torch.device) -> torch.Tensor:
"""Convert uint8 HWC numpy frame to float32 CHW tensor on device."""
t = torch.from_numpy(frame).permute(2, 0, 1).unsqueeze(0).float() / 255.0
return t.to(device)
@staticmethod
def _tensor_to_frame(t: torch.Tensor) -> np.ndarray:
"""Convert float32 CHW tensor (batch=1) to uint8 HWC numpy frame."""
arr = t.squeeze(0).permute(1, 2, 0).clamp(0.0, 1.0).cpu().numpy()
return (arr * 255.0).astype(np.uint8)
@staticmethod
def _frames_to_tensor(
frames: list[np.ndarray], device: torch.device
) -> torch.Tensor:
t = torch.from_numpy(np.stack(frames, axis=0))
t = t.permute(0, 3, 1, 2).contiguous().float() / 255.0
return t.to(device, non_blocking=True)
@staticmethod
def _tensor_to_frames(t: torch.Tensor) -> list[np.ndarray]:
arr = t.permute(0, 2, 3, 1).clamp(0.0, 1.0).cpu().numpy()
arr = (arr * 255.0).astype(np.uint8)
return [arr[i] for i in range(arr.shape[0])]
def _make_inference(
self, model: Model, I0: torch.Tensor, I1: torch.Tensor, n: int, scale: float
) -> list[torch.Tensor]:
"""
Recursively generate n-1 intermediate frames between I0 and I1.
Returns a list of intermediate frame tensors (not including I0 or I1).
"""
if n == 1:
return [model.inference(I0, I1, scale=scale)]
mid = model.inference(I0, I1, scale=scale)
return (
self._make_inference(model, I0, mid, n // 2, scale)
+ [mid]
+ self._make_inference(model, mid, I1, n // 2, scale)
)
def _interpolate_2x_batched(
self, model: Model, frames: list[np.ndarray], scale: float
) -> list[np.ndarray]:
device = model.device()
source = self._frames_to_tensor(frames, device)
intermediate_frames: list[np.ndarray] = []
with torch.inference_mode():
for start in range(0, len(frames) - 1, _MAX_RIFE_BATCH_PAIRS):
end = min(start + _MAX_RIFE_BATCH_PAIRS, len(frames) - 1)
mids = model.inference(
source[start:end],
source[start + 1 : end + 1],
scale=scale,
)
intermediate_frames.extend(self._tensor_to_frames(mids))
result: list[np.ndarray] = []
for i, mid in enumerate(intermediate_frames):
result.append(frames[i])
result.append(mid)
result.append(frames[-1])
return result
def interpolate(
self,
frames: list[np.ndarray],
exp: int = 1,
scale: float = 1.0,
) -> tuple[list[np.ndarray], int]:
"""
Interpolate frames using RIFE.
Args:
frames: List of uint8 numpy arrays with shape [H, W, 3].
exp: Exponent for interpolation factor. 1 → 2×, 2 → 4×.
scale: RIFE inference scale. Use 0.5 for high-resolution inputs.
Returns:
(interpolated_frames, multiplier) where multiplier = 2**exp.
"""
if len(frames) < 2:
logger.warning(
"Frame interpolation requires at least 2 frames; returning input unchanged."
)
return frames, 1
model = self._ensure_model_loaded()
device = model.device()
n_intermediate = 2**exp // 2 # intermediates per adjacent pair
start_time = time.perf_counter()
if n_intermediate == 1:
result = self._interpolate_2x_batched(model, frames, scale)
logger.info(
"RIFE batched interpolation completed in %.3f seconds for %d frames",
time.perf_counter() - start_time,
len(frames),
)
return result, 2**exp
result: list[np.ndarray] = []
for i in range(len(frames) - 1):
I0 = self._frame_to_tensor(frames[i], device)
I1 = self._frame_to_tensor(frames[i + 1], device)
intermediate_tensors = self._make_inference(
model, I0, I1, n_intermediate, scale
)
result.append(frames[i])
for t in intermediate_tensors:
result.append(self._tensor_to_frame(t))
result.append(frames[-1])
multiplier = 2**exp
logger.info(
"RIFE interpolation completed in %.3f seconds for %d frames",
time.perf_counter() - start_time,
len(frames),
)
return result, multiplier
# ---------------------------------------------------------------------------
# Module-level convenience function
# ---------------------------------------------------------------------------
def interpolate_video_frames(
frames: list[np.ndarray],
exp: int = 1,
scale: float = 1.0,
model_path: Optional[str] = None,
) -> tuple[list[np.ndarray], int]:
"""
Convenience wrapper around FrameInterpolator.
Args:
frames: List of uint8 HWC numpy frames.
exp: Interpolation exponent (1=2×, 2=4×).
scale: RIFE inference scale (default 1.0; use 0.5 for high-res).
model_path: Local directory or HuggingFace repo ID containing
``flownet.pkl``. *None* → default ``elfgum/RIFE-4.22.lite``.
Returns:
(interpolated_frames, multiplier)
"""
interpolator = FrameInterpolator(model_path=model_path)
return interpolator.interpolate(frames, exp=exp, scale=scale)