DeltaStreamer / core /worker.py
BBBBCHAN's picture
Upload DeltaStreamer source code and local demo
0bdbf4d verified
Raw History Blame Contribute Delete
14.9 kB
#!/usr/bin/env python
"""
Shared-queue worker for OVO-Bench agentic evaluation.
设计目标
========
- 4 个 worker(每卡 1 个 TP=1 vLLM 实例)动态共享同一份 sample 列表;
- 用 POSIX O_EXCL 文件原子创建做认领(无外部依赖、NFS-safe);
- 每 worker 每次认领 K 个样本, 跑完再认领下一批 → 真动态负载均衡,
不存在"长样本扎堆某 worker 拖尾, 其它卡闲置"的问题;
- 复用 agent.process_videos_agent 现有逻辑(in-flight 池仍生效),
本脚本只是把 samples 列表替换成"动态认领的子集";
- 每 worker 写独立的 *.worker<N>.jsonl, 由 shell 脚本最后合并。
CLI
===
继承 agent.py 的所有参数, 额外新增:
--worker_id N 本 worker 编号(0-based, 仅用于日志/claim 文件标记)
--num_workers N worker 总数(仅用于日志)
--claims_dir PATH 共享认领目录(每个 sample 一个 .claim 文件)
--claim_batch_size K 每批认领样本数(默认 16)
--skip_score 跳过末尾 score_results(由合并脚本统一打分)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from typing import List, Set
# 让脚本在仓库根/scripts 下都能 import 到 agent / video_processor
HERE = os.path.dirname(os.path.abspath(__file__))
if HERE not in sys.path:
sys.path.insert(0, HERE)
import agent # noqa: E402
import video_processor as ovo # noqa: E402
from agent import ( # noqa: E402
Budget,
build_default_registry,
build_extended_registry,
process_videos_agent,
render_agent_system,
)
# ----------------------------------------------------------------------------
# 原子认领
# ----------------------------------------------------------------------------
def atomic_claim_batch(
claims_dir: str,
candidates: List[str],
batch_size: int,
worker_id: int,
) -> List[str]:
"""对 ``candidates`` 顺序尝试 O_EXCL 创建 .claim 文件,
成功的 id 计为本 worker 认领。最多认领 ``batch_size`` 个。
"""
os.makedirs(claims_dir, exist_ok=True)
claimed: List[str] = []
for sid in candidates:
if len(claimed) >= batch_size:
break
path = os.path.join(claims_dir, f"{sid}.claim")
try:
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
try:
os.write(
fd,
(
f"worker={worker_id}\n"
f"pid={os.getpid()}\n"
f"ts={time.time()}\n"
).encode(),
)
finally:
os.close(fd)
claimed.append(sid)
except FileExistsError:
continue
return claimed
def load_global_processed_ids(output_dir: str, run_tag: str) -> Set[str]:
"""读所有 worker 已写出的 jsonl, 收集已完成的 sample id 集合。"""
import glob as _g
done: Set[str] = set()
output_prefix = os.environ.get("STREAMO_OUTPUT_PREFIX", "ovobench")
for fp in _g.glob(os.path.join(
output_dir, f"{output_prefix}_{run_tag}.worker*.jsonl"
)):
try:
with open(fp) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
r = json.loads(line)
sid = r.get("id")
if sid is not None:
done.add(str(sid))
except json.JSONDecodeError:
continue
except FileNotFoundError:
continue
return done
def pending_claims(
claims_dir: str,
all_ids: List[str],
done: Set[str],
) -> List[str]:
return [
sid
for sid in all_ids
if sid not in done
and os.path.exists(os.path.join(claims_dir, f"{sid}.claim"))
]
def claim_owner_alive(claim_path: str):
"""Return True/False for PID-aware claims, None for legacy claims."""
try:
with open(claim_path, encoding="utf-8") as handle:
text = handle.read()
except OSError:
return None
pid = None
for line in text.splitlines():
if line.startswith("pid="):
try:
pid = int(line.split("=", 1)[1])
except ValueError:
return None
break
if pid is None:
return None
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
except PermissionError:
return True
return True
# ----------------------------------------------------------------------------
# CLI(扩展 agent 的 argparser)
# ----------------------------------------------------------------------------
def _build_argparser() -> argparse.ArgumentParser:
p = agent._build_argparser()
p.description = "OVO-Bench shared-queue agent worker."
g = p.add_argument_group("shared-queue worker options")
g.add_argument("--worker_id", type=int, required=True,
help="0-based worker index, used in log + claim record")
g.add_argument("--num_workers", type=int, default=4,
help="total worker count (informational only)")
g.add_argument("--claims_dir", required=True,
help="shared directory holding <id>.claim atomic stubs")
g.add_argument("--claim_batch_size", type=int, default=16,
help="number of samples each worker claims per round")
g.add_argument("--run_tag", required=True,
help="run tag used for partial output filename")
g.add_argument("--output_dir", required=True,
help="directory to write <prefix>_<tag>.worker<id>.jsonl")
g.add_argument("--output_prefix", default="ovobench",
help="result shard filename prefix")
g.add_argument("--skip_score", action="store_true",
help="skip ovo.score_results at end (merge step does it)")
return p
# ----------------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------------
def main():
args = _build_argparser().parse_args()
os.environ["STREAMO_OUTPUT_PREFIX"] = args.output_prefix
# 1) 引擎初始化(同 agent.main, 保持 TP/gpu_mem 等行为一致)
from swift import VllmEngine
# Ascend NPU 上 torch.compile / aclgraph 捕获不稳定(qwen3_5 在 aot_compile
# 阶段报 dynamo TypeError),默认走 enforce_eager;可用 STREAMO_ENFORCE_EAGER=0 关闭。
_enforce_eager = os.environ.get("STREAMO_ENFORCE_EAGER", "1") not in ("0", "false", "False")
# Qwen3.5 混合架构的 GDN(gated delta net) prefill 内核,vLLM 默认(auto)在 SM90(Hopper)
# 上选 flashinfer JIT 内核;但 flashinfer 0.6.6 打包的 SM90 模板有编译期 bug
# (flat::Unused 缺 consumer_wait/index 等成员 → Ninja/nvcc 编译失败),warmup 在
# try/except 里被吞成 WARNING,worker 反复编译空耗 GPU 起不来。改走 triton 后端
# (forward_native,无需 nvcc JIT)绕开。可用 STREAMO_GDN_PREFILL_BACKEND 覆盖。
_gdn_backend = os.environ.get("STREAMO_GDN_PREFILL_BACKEND", "triton").strip().lower()
_engine_kwargs = {}
if _gdn_backend in ("triton", "flashinfer"):
_engine_kwargs["gdn_prefill_backend"] = _gdn_backend
print(f"[worker{args.worker_id}] gdn_prefill_backend={_gdn_backend or 'auto(default)'}")
engine = VllmEngine(
args.model,
max_model_len=65536,
limit_mm_per_prompt={"image": 600, "video": 1},
tensor_parallel_size=args.tp,
enable_prefix_caching=True,
gpu_memory_utilization=args.gpu_memory_utilization,
enforce_eager=_enforce_eager,
engine_kwargs=_engine_kwargs,
)
if args.no_think:
try:
engine.template.enable_thinking = False
print(f"[worker{args.worker_id}] no_think enabled")
except Exception as e:
print(f"[worker{args.worker_id}] no_think failed: {e}")
# 2) 加载完整 samples + task_filter
ovo.VIDEO_PREFIX = args.video_prefix
samples = ovo.load_dataset_json(args.dataset)
task_set = ovo._normalize_task_filter(args.tasks)
if task_set:
samples = [s for s in samples if s.get("task") in task_set]
sample_by_id = {str(s["id"]): s for s in samples}
all_ids: List[str] = [str(s["id"]) for s in samples]
print(f"[worker{args.worker_id}] loaded {len(samples)} samples "
f"(tasks={sorted(task_set) if task_set else 'all'})")
# 3) registry / system / budget 工厂(与 agent.main 一致)
use_extended = (
args.memory_mode == "brief"
or args.enable_memory_tools
or args.enable_track_tools
or args.enable_audio_tools
or args.enable_efficiency_tools
)
if use_extended:
registry = build_extended_registry(
enable_memory=args.enable_memory_tools or args.memory_mode == "brief",
enable_track=args.enable_track_tools,
enable_audio=args.enable_audio_tools,
enable_efficiency=args.enable_efficiency_tools or args.memory_mode == "brief",
)
else:
registry = build_default_registry()
base_system = ovo.SYSTEM if hasattr(ovo, "SYSTEM") else ""
system = (
base_system if args.no_tools
else render_agent_system(registry, base_system, memory_mode=args.memory_mode,
delta_input_mode=getattr(args, 'delta_input_mode', '2frame'))
)
def _budget_factory():
return Budget(
max_tool_calls=args.budget_max_tool_calls,
max_frames_seen=args.budget_max_frames_seen,
max_tokens=args.budget_max_tokens,
)
# 4) 输出文件:每 worker 自己一份分片
os.makedirs(args.output_dir, exist_ok=True)
out_path = os.path.join(
args.output_dir,
f"{args.output_prefix}_{args.run_tag}.worker{args.worker_id}.jsonl",
)
print(f"[worker{args.worker_id}] output → {out_path}")
# 5) 共享队列主循环
rounds_done = 0
samples_done = 0
claim_wait_started = None
claim_wait_seconds = float(os.environ.get(
"STREAMO_CLAIM_WAIT_SECONDS", "21600"
))
while True:
# 全局已完成(任一 worker 写出过) → 不再认领
global_done = load_global_processed_ids(args.output_dir, args.run_tag)
# 候选 = 全部样本 - 全局已完成 - 已被任何 worker 认领过的
candidates = [
sid for sid in all_ids
if sid not in global_done
and not os.path.exists(os.path.join(args.claims_dir, f"{sid}.claim"))
]
if not candidates:
pending = pending_claims(args.claims_dir, all_ids, global_done)
if pending:
now = time.time()
claim_wait_started = claim_wait_started or now
owners = [
claim_owner_alive(
os.path.join(args.claims_dir, f"{sid}.claim")
)
for sid in pending
]
elapsed = now - claim_wait_started
if elapsed < claim_wait_seconds and any(
owner is not False for owner in owners
):
print(
f"[worker{args.worker_id}] waiting for "
f"{len(pending)} claimed samples "
f"(alive={owners.count(True)}, "
f"legacy={owners.count(None)}, "
f"elapsed={elapsed:.0f}s)"
)
time.sleep(5)
continue
print(
f"[worker{args.worker_id}] pending claims did not "
f"complete before exit: {pending[:12]}"
)
else:
print(
f"[worker{args.worker_id}] no candidates or pending "
f"claims left; exit. rounds={rounds_done} "
f"samples={samples_done}"
)
break
claim_wait_started = None
claimed_ids = atomic_claim_batch(
args.claims_dir, candidates, args.claim_batch_size, args.worker_id,
)
if not claimed_ids:
# 这一轮全被别的 worker 抢走了, 等一下再扫
time.sleep(2)
continue
sub_samples = [sample_by_id[i] for i in claimed_ids]
rounds_done += 1
samples_done += len(sub_samples)
print(f"[worker{args.worker_id}] round {rounds_done}: "
f"claimed {len(sub_samples)} samples → "
f"{[s.get('task')+'/'+str(s.get('id')) for s in sub_samples[:6]]}"
f"{' ...' if len(sub_samples)>6 else ''}")
# 调用现有 process_videos_agent 跑这一批
# 注意:它内部会 a-mode open(out_path), 直接 append; 多 worker
# 写不同 jsonl 文件, 所以不会冲突。
process_videos_agent(
engine=engine,
samples=sub_samples,
system=system,
registry=registry,
max_concurrent=args.max_concurrent,
batch_size=args.batch_size,
output_file=out_path,
task_filter=None, # 已在外层过滤过, 这里禁用避免误删
max_tool_calls_per_round=args.max_tool_calls_per_round,
max_tool_calls_per_sample=args.max_tool_calls_per_sample,
max_tool_calls_per_reply=args.max_tool_calls_per_reply,
memory_mode=args.memory_mode,
memory_token_budget=args.memory_token_budget,
budget_factory=_budget_factory,
trajectory_dir=args.trajectory_dir,
auto_anchor_every=args.auto_anchor_every,
auto_asr_every=args.auto_asr_every,
auto_asr_window=args.auto_asr_window,
auto_track_query_every=args.auto_track_query_every,
memory_render_mode=args.memory_render_mode,
memory_head_max=args.memory_head_max,
memory_tail_max=args.memory_tail_max,
memory_head_seconds=args.memory_head_seconds,
memory_tail_seconds=args.memory_tail_seconds,
memory_recall_topk=args.memory_recall_topk,
cold_start_rounds=args.cold_start_rounds,
degrade_threshold=args.degrade_threshold,
degrade_events_min=args.degrade_events_min,
rolling_window=args.rolling_window,
delta_input_mode=getattr(args, 'delta_input_mode', '2frame'),
enable_verification=getattr(args, 'enable_verification', False),
)
if not args.skip_score:
ovo.score_results(out_path)
if __name__ == "__main__":
main()