这个模块负责控制 LingBot-Map 在视频流或超长视频上的推理方式,核心目标是:
LingBot-Map 是面向 streaming reconstruction 的 feed-forward 模型。推理时不是把整段视频一次性塞进模型,而是持续接收帧,并维护一组流式状态:
text当前帧 ↓ 图像特征 / 几何 token ↓ 短期 pose-reference window ↓ 长期 trajectory memory / anchor context ↓ paged KV cache ↓ 当前帧位姿、深度、点云 / 重建结果
其中本模块主要管理:
textframe range 只跑哪一段 keyframe interval 哪些帧写入长期上下文 KV cache backend 用什么缓存后端 windowed mode 超长视频如何分窗 state reset 什么时候清空历史状态
keyframe_interval 控制长期上下文写入频率keyframe_interval 是流式推理里最常调的参数。
它决定每隔多少帧选一个 keyframe,并把该 keyframe 的上下文写入 KV cache / trajectory memory。
常见设置:
| 配置 | 含义 | 适用场景 |
|---|---|---|
--keyframe_interval 1 |
每帧都是 keyframe | 最稳,适合短视频、快速运动、精度优先 |
--keyframe_interval 2 |
每 2 帧写一次长期上下文 | 平衡速度、显存和质量 |
--keyframe_interval 4 |
每 4 帧写一次长期上下文 | 长视频、运动较平滑、显存紧张 |
--keyframe_interval 8 或更大 |
很稀疏地写上下文 | 只建议粗略预览或运动很慢的视频 |
一个 10,000 帧视频,如果每帧都进入长期上下文,缓存长度就是 10,000 级别;如果设置:
bash--keyframe_interval 4
长期缓存中的 keyframe 数量大约变成:
textceil(10000 / 4) = 2500
这会明显降低长序列推理的显存压力和注意力计算开销。
需要注意的是:keyframe_interval > 1 时,非 keyframe 仍然可以被模型推理,但它们不应该写入长期 KV cache。README 里提到的 FlashInfer KV cache 修复正是针对这个问题:旧版本在 --keyframe_interval > 1 时可能错误缓存非 keyframe,建议使用最新 main 分支。
LingBot-Map 的长序列能力依赖 KV cache。它缓存的是 Transformer 注意力中的历史 key/value,而不是原始 RGB 帧。
简化后的生命周期如下:
textreset_state() ↓ 读入 frame 0 ↓ 如果是 keyframe:写入 KV cache ↓ 读入 frame 1 ↓ 从 KV cache 读取历史上下文 ↓ 如果不是 keyframe:只推理,不写入长期 cache ↓ 读入 frame 2 ↓ 如果是 keyframe:写入 KV cache ↓ ...
常见后端:
| 后端 | 说明 |
|---|---|
flashinfer |
推荐后端,适合长序列 paged KV cache,性能最好 |
sdpa |
PyTorch SDPA 后端,依赖少;README 中说明长序列 bug 已修复,但长视频仍推荐 FlashInfer |
常见参数组合:
bash--backend flashinfer --dtype bf16
如果设备不支持 BF16,可以退回:
bash--backend flashinfer --dtype fp16
或者:
bash--backend sdpa --dtype fp16
windowed mode 把超长视频切成多个局部流README 中建议长序列,尤其是超过 3000 帧的视频,使用 windowed inference。
没有 windowed mode 时:
textframe 0 ───────────────────────────── frame 12000 一个持续增长的流式状态 / KV cache
启用 windowed mode 后:
textframe 0 2999 | 3000 5999 | 6000 8999 | ... window 0 | window 1 | window 2 | reset / flush | reset / flush | reset / flush |
它的作用是给 KV cache 和流式状态设置上界,避免缓存随着视频长度无限增长。
常见配置:
bash--windowed \ --window_size 3000
如果同时设置:
bash--keyframe_interval 3 \ --window_size 3000
那么每个窗口内最多大约缓存:
textceil(3000 / 3) = 1000
个 keyframe 对应的 KV。
这比把 25,000 帧全部放进同一个流式状态中稳定得多。
长视频调试时,经常只想跑其中一段,例如:
text原视频:0 ... 24999 只跑:9000 ... 12000
这类配置通常对应:
bash--start_frame 9000 \ --end_frame 12000
建议把 frame range 理解成半开区间:
text[start_frame, end_frame)
也就是包含 9000,不包含 12000。实际脚本如果有不同约定,以 python demo.py --help 输出为准。
需要特别注意:如果直接从第 9000 帧开始并重置状态,模型看不到 9000 帧之前的历史上下文。这样跑出来的结果可能和“从第 0 帧一直跑到第 12000 帧”不完全一致。
两种常见用法:
bashpython demo.py \ --video data/long_walkthrough.mp4 \ --output outputs/debug_9000_12000 \ --start_frame 9000 \ --end_frame 12000 \ --keyframe_interval 2 \ --backend flashinfer \ --dtype bf16
适合快速检查中间某段是否能正常推理。
bashpython demo.py \ --video data/long_walkthrough.mp4 \ --output outputs/debug_9000_12000_warmup \ --start_frame 8500 \ --end_frame 12000 \ --keyframe_interval 2 \ --backend flashinfer \ --dtype bf16
实际分析时只看 9000 之后的输出,但让模型先用 8500 到 8999 建立一段上下文。
流式推理状态不能跨无关场景复用。以下情况应该重置:
| 场景 | 是否重置 | 原因 |
|---|---|---|
| 换了一个视频文件 | 必须重置 | 上一个视频的 pose / cache 会污染新视频 |
| 同一个视频中间发生硬切镜头 | 建议重置 | 前后不是连续运动轨迹 |
| 分辨率变化 | 必须重置 | token shape / cache shape 可能不一致 |
| 相机内参变化很大 | 建议重置 | 几何上下文不再稳定 |
| windowed mode 进入下一个窗口 | 通常重置或 flush | 限制缓存长度 |
| 只跑一个 frame range | 通常在 range 起点重置 | 明确边界,避免旧状态残留 |
简化伪代码:
pythonstate.reset()
for video in videos:
state.reset()
for window in split_into_windows(video):
if cfg.windowed:
state.reset()
for frame_idx, frame in window:
is_keyframe = frame_idx % cfg.keyframe_interval == 0
output = model.step(
frame,
state=state,
write_kv_cache=is_keyframe,
)下面命令假设已经完成安装,并且当前目录是 Robbyant/lingbot-map 仓库根目录。具体参数名以当前分支的 python demo.py --help 和 python demo_render/batch_demo.py --help 为准。
每帧都写入长期上下文,适合几十秒以内的视频或需要最高稳定性的场景:
bashpython demo.py \ --video data/room_short.mp4 \ --output outputs/room_short_k1 \ --keyframe_interval 1 \ --backend flashinfer \ --dtype bf16
每 2 帧选一个 keyframe:
bashpython demo.py \ --video data/room_walk.mp4 \ --output outputs/room_walk_k2 \ --keyframe_interval 2 \ --backend flashinfer \ --dtype bf16
bashpython demo.py \ --video data/long_walkthrough.mp4 \ --output outputs/long_walkthrough_windowed \ --keyframe_interval 2 \ --windowed \ --window_size 3000 \ --backend flashinfer \ --dtype bf16
长视频更适合使用离线 batch pipeline:
bashpython demo_render/batch_demo.py \ --video data/indoor_25000_frames.mp4 \ --output outputs/indoor_25000_render \ --keyframe_interval 3 \ --windowed \ --window_size 3000 \ --backend flashinfer \ --dtype bf16
bashpython demo.py \ --video data/long_walkthrough.mp4 \ --output outputs/segment_9000_12000 \ --start_frame 9000 \ --end_frame 12000 \ --keyframe_interval 2 \ --windowed \ --window_size 3000 \ --backend flashinfer \ --dtype bf16
下面这个脚本不依赖模型权重,只用来检查某组长序列配置会产生哪些 keyframe、窗口和状态重置点。适合在真正跑模型前确认配置是否符合预期。
保存为:
bashtools/inspect_stream_plan.py
代码:
python#!/usr/bin/env python3
import argparse
from dataclasses import dataclass
@dataclass
class StreamPlan:
num_frames: int
start_frame: int
end_frame: int
keyframe_interval: int
windowed: bool
window_size: int
def iter_windows(plan: StreamPlan):
start = plan.start_frame
end = plan.end_frame
if not plan.windowed:
yield 0, start, end
return
window_id = 0
cur = start
while cur < end:
nxt = min(cur + plan.window_size, end)
yield window_id, cur, nxt
cur = nxt
window_id += 1
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--num_frames", type=int, required=True)
parser.add_argument("--start_frame", type=int, default=0)
parser.add_argument("--end_frame", type=int, default=None)
parser.add_argument("--keyframe_interval", type=int, default=1)
parser.add_argument("--windowed", action="store_true")
parser.add_argument("--window_size", type=int, default=3000)
parser.add_argument("--show_first", type=int, default=10)
args = parser.parse_args()
if args.keyframe_interval < 1:
raise ValueError("--keyframe_interval must be >= 1")
end_frame = args.num_frames if args.end_frame is None else args.end_frame
if not (0 <= args.start_frame < end_frame <= args.num_frames):
raise ValueError(
"expect 0 <= start_frame < end_frame <= num_frames, "
f"got start={args.start_frame}, end={end_frame}, num={args.num_frames}"
)
if args.windowed and args.window_size < 1:
raise ValueError("--window_size must be >= 1")
plan = StreamPlan(
num_frames=args.num_frames,
start_frame=args.start_frame,
end_frame=end_frame,
keyframe_interval=args.keyframe_interval,
windowed=args.windowed,
window_size=args.window_size,
)
total_frames = 0
total_keyframes = 0
print("stream plan")
print(f" frame range : [{plan.start_frame}, {plan.end_frame})")
print(f" frames to process : {plan.end_frame - plan.start_frame}")
print(f" keyframe interval : {plan.keyframe_interval}")
print(f" windowed : {plan.windowed}")
if plan.windowed:
print(f" window size : {plan.window_size}")
print()
for window_id, win_start, win_end in iter_windows(plan):
frames = list(range(win_start, win_end))
keyframes = [
idx for idx in frames
if (idx - plan.start_frame) % plan.keyframe_interval == 0
]
total_frames += len(frames)
total_keyframes += len(keyframes)
print(f"window {window_id}")
print(f" reset state : yes")
print(f" frame range : [{win_start}, {win_end})")
print(f" frame count : {len(frames)}")
print(f" keyframe count : {len(keyframes)}")
print(f" first keyframes : {keyframes[:args.show_first]}")
print()
print("summary")
print(f" total frames : {total_frames}")
print(f" total keyframes : {total_keyframes}")
print(f" approx cache cap : {total_keyframes if not plan.windowed else 'bounded by each window'}")
if __name__ == "__main__":
main()运行:
bashpython tools/inspect_stream_plan.py \ --num_frames 25000 \ --start_frame 0 \ --end_frame 25000 \ --keyframe_interval 3 \ --windowed \ --window_size 3000
示例输出节选:
textstream plan frame range : [0, 25000) frames to process : 25000 keyframe interval : 3 windowed : True window size : 3000 window 0 reset state : yes frame range : [0, 3000) frame count : 3000 keyframe count : 1000 first keyframes : [0, 3, 6, 9, 12, 15, 18, 21, 24, 27] window 1 reset state : yes frame range : [3000, 6000) frame count : 3000 keyframe count : 1000 first keyframes : [3000, 3003, 3006, 3009, 3012, 3015, 3018, 3021, 3024, 3027] summary total frames : 25000 total keyframes : 8334 approx cache cap : bounded by each window
推荐:
bash--keyframe_interval 1 --backend flashinfer --dtype bf16
如果显存紧张:
bash--keyframe_interval 2
推荐:
bash--keyframe_interval 2 --windowed --window_size 3000 --backend flashinfer --dtype bf16
推荐:
bash--keyframe_interval 3 --windowed --window_size 3000 --backend flashinfer --dtype bf16
如果画面运动很快、转弯很多,优先降低 keyframe_interval,例如从 3 改成 2 或 1。
可以加大 keyframe 间隔:
bash--keyframe_interval 4
但不要把预览配置直接当成最终重建配置。keyframe 太稀疏时,长距离漂移、局部几何断裂和位姿不稳定会更明显。
keyframe_interval 不是越大越好它主要节省长期上下文的写入成本。设置太大时,模型能使用的历史几何锚点会变少,容易出现:
一般先从 1 或 2 开始,再根据显存和速度调大。
windowed mode 的好处是显存和缓存长度可控;代价是窗口之间的长期上下文被截断。
可能出现:
如果最终目标是高质量全局地图,建议:
keyframe_interval;window_size 设得过小;直接设置:
bash--start_frame 9000
通常会在 9000 帧处重新初始化状态。模型没有看到 0 到 8999 的历史上下文,因此输出可能和完整长跑不同。
如果需要更接近完整结果,给一段 warm-up:
bash--start_frame 8500 --end_frame 12000
然后只使用 9000 之后的结果。
批量处理多个视频时,不要让上一个视频的 KV cache、trajectory memory 或 pose-reference window 继续保留到下一个视频。
错误模式:
pythonstate = model.new_state()
for video in videos:
for frame in video:
model.step(frame, state=state)正确模式:
pythonfor video in videos:
state = model.new_state()
for frame in video:
model.step(frame, state=state)KV cache 与 token shape、特征尺度、相机几何假设相关。以下情况建议重置:
README 中说明 SDPA KV cache bug 已修复,但长序列推理仍推荐 FlashInfer。尤其是:
--keyframe_interval > 1--windowed--window_size 3000这类配置下,FlashInfer 的 paged KV cache 更合适。
--compile 适合固定形状的长跑如果使用:
bash--compile
通常可以提升稳定长跑的吞吐,但要注意:
window_size 更容易获得稳定收益;如果对 start_frame / end_frame 的精确性要求很高,建议先把视频解成图片序列:
bashffmpeg -i input.mp4 frames/%06d.jpg
再对图片序列做推理。某些视频编码格式存在 B-frame、可变帧率或 seek 精度问题,直接按视频时间戳跳转可能导致帧号和预期不完全一致。