Covers the review's adversarial probes: expired-deadline greedy drain
coalesces jobs queued behind an in-flight generation (delay>0 no longer
degrades to sequential), submit-after-stop raises, stop() fails
still-queued futures without hanging, a dispatch exception fans out to
every future in the group, a crashed _run task fails in-flight and
queued futures and rejects later submits, the pipeline capability gate
rejects non-opted-in pipelines (DMD/causal always vetoed), pre-set
latents are never merged, and distinct opaque objects are conservatively
incompatible. Verified the deadline and gate tests fail on the pre-fix
scheduler.
If _run dies between popping a job off the queue and resolving its
future, that job belonged to neither _queue nor _pending, so the crash
drain could not fail it and its submitter hung forever. Re-park it in
_pending on the way out so _on_run_done fails it like everything else.
can_dynamic_batch only inspects request fields, but several shipped
pipelines are structurally batch-size-1 and can corrupt outputs when
requests are merged. Add PipelineConfig.supports_dynamic_batching
(default False) with dynamic_batching_supported() as the effective
gate: the Wan family opts in (the generic Wan denoising path is the
verified parity target), while DMD and causal denoising variants are
always vetoed because those stages do not consume per-item generators
and merged requests would share RNG. The scheduler refuses to start and
_generate_prepared_work_items refuses to merge with a clear error when
the loaded pipeline has not opted in.
SamplingParam.latents was neither signature-compared nor listed as
unsupported, so two requests with different pre-set latents could merge
and corrupt each other. List it as unsupported for dynamic batching, and
harden the signature freezer: unrecognized values fell back to repr(),
which truncates large tensors and can collide for different payloads —
compare unrecognized objects by identity instead so they are
conservatively incompatible unless literally shared.
_jobs_are_compatible swallowed every exception and returned False, so a
broken probe silently degraded dynamic batching to sequential with zero
operator signal. Log a deduplicated warning (still returning False).
The _run exit path only failed futures parked in _pending; jobs still
sitting in _queue when stop() landed mid-dispatch hung forever. Drain
the queue too (skipping None sentinels) and fail those futures with the
same 'stopped before dispatch' RuntimeError.
asyncio.wait_for raises asyncio.TimeoutError on Python 3.10 (aliased to
the builtin only from 3.11), so the first expired batching window killed
the _run task silently: every in-flight future and all later submits
hung while the server kept accepting requests. Catch both exception
names, and add a done-callback on the scheduler task that logs the
crash, marks the scheduler stopped, and fails every job still waiting
in the queue or pending deque with a RuntimeError instead of hanging.
When batching_delay_ms > 0, the collect deadline anchors to the first
job's enqueue time. Any job that waited behind an in-flight generation
arrives with an already-expired deadline and was dispatched as a batch
of 1, degrading dynamic batching to sequential under sustained load.
Fall through to the same non-blocking greedy drain the delay=0 path
uses so queued compatible jobs still coalesce up to batching_max_size.
The new batching unit tests (admission, signature) were not collected by
any CI lane. stages/ is already collected on main, so only batching/ is
added.
The dynamic-batching prompt-file branch let a single failed group raise
out of generate_video, discarding every completed result (and tripping
the strict zip). Restore the legacy per-prompt tolerance: a failed group
now yields an error entry per prompt, keeping results aligned with the
input prompts while completed videos survive.
With the default batching_delay_ms=0, _collect_batch called
wait_for(timeout=0) which timed out before draining the queue, so every
dispatched batch had size 1 and dynamic batching silently no-oped. When
delay is 0, drain already-queued jobs non-blocking (until empty,
max_size, or an incompatible job) instead of waiting.
Batch mode resolves every output path via _prepare_output_path before any
file is written, so the os.path.exists dedupe never fired and duplicate
prompts (or prompts equal in their first 100 sanitized chars) overwrote
each other. Track paths reserved within the batch and suffix duplicates,
matching what the legacy sequential path produces for the same inputs.