perf(core): run visual participants (actor, scene) in parallel within a shared decode stream - #129
Conversation
…gression detection (#1) * feat(benchmarks): add reproducible indexing-latency benchmark Adds a command that generates synthetic media via FFmpeg testsrc2 and measures per-stage indexing throughput, per-stage wall time, and peak memory across configurable modalities. Supports regression detection against a prior baseline report. - : corpus generation, run orchestrator (drives real run_index/ModelRuntime), per-stage aggregation, baseline comparison - CLI command with --modalities, --videos, --duration-seconds, --resolution, --repetitions, --input-mode (transcript/transcribe), --audio-mode, --baseline, --baseline-tolerance - documents the protocol, output schema, and limitations - 23 unit tests for validation, aggregation, clip command building, baseline comparison, and corpus spec * fix(benchmarks): address CodeRabbit review issues - Reject resolutions with extra components (e.g. 320x180x1) - Accumulate record_counts across repetitions instead of overwriting - Move corpus generation inside try block for proper failure handling - Pass reset parameter through instead of hardcoded True - Validate baseline configuration compatibility before comparison
… shared decode stream Refactors _consume_visual_stream so the decode loop runs on a thread and each visual participant (actor via OpenCV, scene via torch) gets its own worker thread consuming from a per-participant queue. This lets actor and scene overlap on CPU since both libraries release the GIL during computation. - Decode thread pushes RGB batches to bounded queues (maxsize=4) for backpressure - Each participant worker filters its own samples and calls process() independently - Exceptions from any thread propagate via a shared error list - Cancellation checked in both decode and participant workers with a 0.2s polling timeout on the queue get to stay responsive - The single-decode property (one pass through iter_frame_batches) is preserved; actor stays in-order (single FIFO consumer) No changes to runner.py, IndexConfig, ResourceScheduler, or any capability's indexing logic. All 5 new threading tests pass.
- Fix a critical shutdown deadlock when a participant queue is full: the sentinel drops, so workers now also exit once decoding is done and their queue is empty. - Stop workers promptly after a sibling records an error instead of processing remaining queued batches. - Normalize duplicate modality names in index_visuals before building participant/reporting structures. - Reorder the cancel timer callback so cancellation happens before unblocking the stream. - Prefer addClassCleanup over tearDownClass and drop an unused rebinding. Internal-only.
There was a problem hiding this comment.
Thanks for taking this on. The shared stream implementation and queue/shutdown tests are a useful foundation.
I think the PR description needs one clarification: the existing implementation already decoded a single shared frame stream for all visual participants, so this change parallelizes participant processing rather than removing multiple decode passes.
Before merging, could we also add before/after latency and memory measurements for both scene alone and scene,actor? The discussion on issue #96 placed the benchmark first, and the scene-only case would help show the overhead of the new thread and queues when only one participant is selected.
| if errors: | ||
| return | ||
| cancellation.raise_if_cancelled() | ||
| report_progress( |
There was a problem hiding this comment.
One production path may need adjustment here. These worker threads can call the progress and cancellation callbacks outside the thread that owns the DBOS workflow context. The production progress callback uses DBOS.set_event(), while DBOS keeps the active workflow state in a ContextVar that raw threading.Threads do not inherit. This means the first progress update may fail the indexing job. The current tests do not exercise that path because they use progress=None and a plain cancellation token. Could we keep these callbacks on the owning thread, or carry the required context into the workers, and add a production-style callback test?
There was a problem hiding this comment.
Addressed in 43d4541: each worker thread now runs inside its own contextvars.Context copied from the owning thread (_in_context in _consume_visual_stream), so the DBOS workflow ContextVar is visible to the progress (DBOS.set_event) and cancellation (DBOS.workflow_id) calls made from the decode and participant workers. Each worker gets a separate copy of the context, since a single Context cannot be entered from more than one thread at a time.
Added test_worker_callbacks_inherit_owning_thread_context in tests/test_visual_threading.py: it wires a ContextVar-gated progress callback (like DBOS.set_event) and a context-dependent cancellation event, then runs _consume_visual_stream with them. Verified the test fails without the context wiring and passes with it; tests/test_visual_threading.py now 8 passed.
The decode and participant worker threads invoke the progress and cancellation callbacks, which in production rely on DBOS state held in ContextVars that raw threading.Threads do not inherit. Run each worker inside its own copy of the owning thread's context so progress events and cancellation checks work from the workers, and cover it with a production-style callback test. Internal-only.
Summary
Runs the
actorandscenevisual participants in parallel over the shared decode stream: one decode pass (already shared before this change), per-participant bounded queues, and sentinel-based shutdown. Previously participants processed the decoded frames sequentially; now each participant consumes the shared sampled-frame stream concurrently. Actor remains in-order; backpressure stalls decode when a participant falls behind. Also fixes a deadlock where a participant failing mid-stream would hang the pipeline — the participant error now propagates instead, and the worker threads carry the owning workflow's context so the production progress/cancellation callbacks work from them.Internal-only; no public API or storage changes.
Motivation
Refs
grayhatdevelopers#96. The visual participants (scene,actor) already shared a single decode pass over the video; the cost is that they processed the decoded frames one after the other, so the scene participant's model work serialized with the actor's. This change feeds the shared sampled-frame stream to both participants concurrently so the slower participant overlaps with the faster one instead of waiting for it.What changed
src/vidxp/capabilities/visual.py— parallel participant scheduling over the shared decode stream:sceneandactorconsumers run concurrently in their own workers;actorremains in-order whilescenemay lag;tests/test_visual_threading.py— 8 unit tests covering concurrent processing, cancellation, decode failures, participant failures, ordering, backpressure/shutdown, and a production-style callback test that asserts progress and cancellation callbacks see the owning thread's context.Design details
contextvars.Contextcopied from the owning thread, so the DBOS workflow ContextVar is visible to the progress (DBOS.set_event) and cancellation (DBOS.workflow_id) calls the workers make.Validation
uv run --no-sync python -m pytest -q tests/test_visual_threading.py— 8 passed.uv run --no-sync python -m pytest -q tests/test_runner.py tests/test_manifest.py tests/test_generation_manifest.py— passed (reported 27 in the fork PR).uv run --no-sync ruff check src/vidxp/capabilities/visual.py tests/test_visual_threading.py— clean.Notes
Internal-only performance/reliability change. No public API, storage schema, CLI, or MCP surface changes. Before/after latency and memory measurements for
scenealone andscene,actorare pending a reproducible benchmark, per the discussion ingrayhatdevelopers#96.