longcat: report the real denoise step, over every segment (v0.2.87)

A 304-frame request sat at "generating 0/16 (0%)" for its whole nine-minute
first segment while the service's log showed Denoising: 15/16. Two defects.

The service set _progress["step"] = 0 at the start of every segment and never
again, so the step count existed only inside the vendored pipeline's
tqdm(total=len(timesteps), desc="Denoising") -- which takes no callback.
Nothing reached GET /progress; the poller in video.py faithfully reported the
0 it was handed. Each pipeline module did `from tqdm import tqdm`, so the bar
is instrumented by rebinding the name on the MODULE (patching tqdm.tqdm would
be too late), and only bars labelled "Denoising" are tracked so a
checkpoint-shard bar cannot move a request's progress.

The totals also counted ONE segment: a four-segment render would have run
0->16 four times against a denominator of 16. `step` now carries the completed
segments' offset so it counts the stage end to end, and the bar spans
steps x segments -- with refinement excluded, being a single pass over the
finished video. A request that asked for a duration rather than a segment
count only learns the real count from the service, so the denominator is
corrected on the first report, through a setter that does not restart the bar
and lose the elapsed time the rate is derived from.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent bed4b018
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.2.86" __version__ = "0.2.87"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -86,6 +86,16 @@ def _vid_progress_done(): ...@@ -86,6 +86,16 @@ def _vid_progress_done():
_vid_progress["active"] = False _vid_progress["active"] = False
_vid_progress["phase"] = "idle" _vid_progress["phase"] = "idle"
def _vid_progress_total(total: int):
"""Correct the denominator mid-render, without resetting the bar.
Some paths only learn how much work there is after the worker has started -- a
LongCat request that asked for a duration rather than a segment count is told the
segment count by the service -- and restarting the bar there would throw away the
elapsed time the rate is derived from."""
_vid_progress["total"] = int(total)
def _vid_progress_step(step: int): def _vid_progress_step(step: int):
_vid_progress["current"] = step _vid_progress["current"] = step
elapsed = time.monotonic() - _vid_progress["started_at"] elapsed = time.monotonic() - _vid_progress["started_at"]
...@@ -4240,10 +4250,22 @@ async def _generate_longcat(request: VideoGenerationRequest, model_name: str, ...@@ -4240,10 +4250,22 @@ async def _generate_longcat(request: VideoGenerationRequest, model_name: str,
else "source" if _k == "longcat_source" else _k] = str(model_cfg[_k]) else "source" if _k == "longcat_source" else _k] = str(model_cfg[_k])
# Total steps across the stages we are about to run, so the bar spans the whole # Total steps across the stages we are about to run, so the bar spans the whole
# request rather than restarting per stage. # request rather than restarting per stage -- and across SEGMENTS, because a long
_total = sum(lc.stage_params(s, request.num_inference_steps, # video runs the generative stages once per segment. Counting a segment's worth
request.guidance_scale)["num_inference_steps"] # made a four-segment render read 0/16 four times over instead of 0/64.
for s in stages) def _grand_total(segments: int) -> int:
segments = max(1, int(segments or 1))
return sum(
lc.stage_params(s, request.num_inference_steps,
request.guidance_scale)["num_inference_steps"]
* (1 if s == "refinement" else segments)
for s in stages)
# Refinement is a single pass over the finished video, so it is not multiplied.
# The service owns the real segment count (it derives one from total_frames when
# the caller asked for a duration); the poll loop corrects this once it reports.
_segments_assumed = max(1, int(payload.get("num_segments") or 1))
_total = _grand_total(_segments_assumed)
_vid_progress_reset(_total) _vid_progress_reset(_total)
_stop = threading.Event() _stop = threading.Event()
...@@ -4254,7 +4276,7 @@ async def _generate_longcat(request: VideoGenerationRequest, model_name: str, ...@@ -4254,7 +4276,7 @@ async def _generate_longcat(request: VideoGenerationRequest, model_name: str,
# claimed in its own docstring to "carry cancellation across the process # claimed in its own docstring to "carry cancellation across the process
# boundary" and never did, so a LongCat render — the longest-running video job # boundary" and never did, so a LongCat render — the longest-running video job
# there is — was the only one nothing could stop. # there is — was the only one nothing could stop.
_lc_steps = sum(lc.STAGE_DEFAULTS.get(_s, (0, 0))[0] for _s in stages) or 1 _lc_steps = _total or 1
_tid = task_registry.register( _tid = task_registry.register(
"video", title=(payload.get("prompt") or mode or "")[:80], "video", title=(payload.get("prompt") or mode or "")[:80],
model=model_name or "", total=_lc_steps) model=model_name or "", total=_lc_steps)
...@@ -4270,6 +4292,10 @@ async def _generate_longcat(request: VideoGenerationRequest, model_name: str, ...@@ -4270,6 +4292,10 @@ async def _generate_longcat(request: VideoGenerationRequest, model_name: str,
returns with an error. That is also exactly what eviction does to it, so the returns with an error. That is also exactly what eviction does to it, so the
path is already exercised.""" path is already exercised."""
_seen_stage = "" _seen_stage = ""
# What the stage now running reports as its own total, segments included, so a
# finished stage contributes what it actually ran rather than a default.
_stage_total = 0
_live = {"segments": _segments_assumed, "total": _total}
while not _stop.wait(2.0): while not _stop.wait(2.0):
try: try:
if not _cancelled["flag"]: if not _cancelled["flag"]:
...@@ -4292,12 +4318,21 @@ async def _generate_longcat(request: VideoGenerationRequest, model_name: str, ...@@ -4292,12 +4318,21 @@ async def _generate_longcat(request: VideoGenerationRequest, model_name: str,
st = str(p.get("stage") or "") st = str(p.get("stage") or "")
if st and st != _seen_stage: if st and st != _seen_stage:
if _seen_stage: if _seen_stage:
_done_steps["base"] += lc.STAGE_DEFAULTS.get( _done_steps["base"] += _stage_total or lc.STAGE_DEFAULTS.get(
_seen_stage, (0, 0))[0] _seen_stage, (0, 0))[0]
_seen_stage = st _seen_stage = st
_stage_total = 0
_stage_total = int(p.get("steps") or 0) or _stage_total
_segs_now = int(p.get("segments") or 0)
if _segs_now and _segs_now != _live["segments"]:
_live["segments"] = _segs_now
_live["total"] = _grand_total(_segs_now)
_vid_progress_total(_live["total"])
# `step` counts the whole stage, every segment of it: the service adds
# the completed segments' offset before reporting the bar's position.
_step = _done_steps["base"] + int(p.get("step") or 0) _step = _done_steps["base"] + int(p.get("step") or 0)
_vid_progress_step(_step) _vid_progress_step(_step)
task_registry.step(_tid, _step) task_registry.step(_tid, _step, total=_live["total"])
except Exception: except Exception:
pass pass
......
This diff is collapsed.
...@@ -70,6 +70,13 @@ _state = { ...@@ -70,6 +70,13 @@ _state = {
} }
_progress = {"active": False, "stage": "", "segment": 0, "segments": 0, _progress = {"active": False, "stage": "", "segment": 0, "segments": 0,
"step": 0, "steps": 0} "step": 0, "steps": 0}
# Denoise-step bookkeeping for /progress. The vendored pipelines render their step
# count onto a tqdm bar and nothing else, so until this was wired the bar the caller
# polls reported 0/N for an entire render. ``offset`` is how many steps earlier
# SEGMENTS of the current stage already contributed, so ``_progress["step"]`` counts
# the stage end to end instead of restarting at every segment boundary;
# ``pass_steps`` is one segment's step count, learnt from the first bar's total.
_bar = {"offset": 0, "pass_steps": 0}
# Set by POST /yield: stop at the next SEGMENT boundary and return what has been # Set by POST /yield: stop at the next SEGMENT boundary and return what has been
# generated, so another model can have the card without the request being lost. # generated, so another model can have the card without the request being lost.
_yield_flag = threading.Event() _yield_flag = threading.Event()
...@@ -79,6 +86,58 @@ def log(msg): ...@@ -79,6 +86,58 @@ def log(msg):
print(f"[longcat] {msg}", flush=True) print(f"[longcat] {msg}", flush=True)
def _install_progress_bars() -> None:
"""Teach the pipelines' denoising bars to publish their step count.
Upstream's generate_* methods drive a bare ``tqdm(total=len(timesteps),
desc="Denoising")`` and take no callback, so the only way to observe a step
without forking the vendored pipeline is to replace the ``tqdm`` the pipeline
module resolved at import time. Each module did ``from tqdm import tqdm``, so the
name has to be rebound on the MODULE -- patching ``tqdm.tqdm`` would be too late.
Only bars labelled "Denoising" are tracked: the pipelines use tqdm for nothing
else today, and a checkpoint-shard bar must not move the request's progress."""
import tqdm as _tqdm_mod
base = _tqdm_mod.tqdm
class _ProgressTqdm(base):
# Checked instead of class identity: this function builds a fresh subclass on
# every call, so identity would re-wrap an already-wrapped bar.
_lc_progress_bar = True
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
self._lc_track = str(kw.get("desc") or "") == "Denoising"
if not self._lc_track:
return
with _lock:
total = int(self.total or 0)
if total:
_bar["pass_steps"] = total
segments = max(1, int(_progress.get("segments") or 1))
_progress["steps"] = total * segments
_progress["step"] = int(_bar["offset"])
def update(self, n=1):
ret = super().update(n)
if getattr(self, "_lc_track", False):
with _lock:
_progress["step"] = int(_bar["offset"]) + int(self.n or 0)
return ret
patched = []
for name in ("longcat_video.pipeline_longcat_video",
"longcat_video.pipeline_longcat_video_avatar"):
mod = sys.modules.get(name)
if mod is not None and not getattr(
getattr(mod, "tqdm", None), "_lc_progress_bar", False):
mod.tqdm = _ProgressTqdm
patched.append(name.rsplit(".", 1)[-1])
if patched:
log(f"progress: denoising bars instrumented ({', '.join(patched)})")
# ── loading ─────────────────────────────────────────────────────────────────── # ── loading ───────────────────────────────────────────────────────────────────
def load_pipeline(checkpoint_dir: str, source_dir: str, dtype: str = "bfloat16", def load_pipeline(checkpoint_dir: str, source_dir: str, dtype: str = "bfloat16",
...@@ -226,6 +285,9 @@ def load_pipeline(checkpoint_dir: str, source_dir: str, dtype: str = "bfloat16", ...@@ -226,6 +285,9 @@ def load_pipeline(checkpoint_dir: str, source_dir: str, dtype: str = "bfloat16",
raise RuntimeError( raise RuntimeError(
"use_distill needs the DiT's enable_loras() — this build of " "use_distill needs the DiT's enable_loras() — this build of "
"longcat_video does not expose it") "longcat_video does not expose it")
# Both pipeline modules are imported by now (the avatar one only for an
# avatar family), so this is the first point where the bars can be rebound.
_install_progress_bars()
_place_pipeline(pipe, offload) _place_pipeline(pipe, offload)
_state.update(pipe=pipe, model=root, source=src, dtype=dtype) _state.update(pipe=pipe, model=root, source=src, dtype=dtype)
log("ready") log("ready")
...@@ -1097,10 +1159,15 @@ def _run_segments(pipe, task: str, stage: str, ctx: dict, log=print): ...@@ -1097,10 +1159,15 @@ def _run_segments(pipe, task: str, stage: str, ctx: dict, log=print):
for index in range(segments): for index in range(segments):
with _lock: with _lock:
_progress.update(segment=index + 1, segments=segments, step=0, # One segment's step count comes from the first bar's own total, which is
steps=LC.stage_params( # authoritative (the avatar distilled pass is 8 steps, the base one 16).
stage, ctx.get("num_inference_steps"), # Before that bar exists, the stage default is the estimate.
ctx.get("guidance_scale"))["num_inference_steps"]) per_pass = int(_bar["pass_steps"]) or int(LC.stage_params(
stage, ctx.get("num_inference_steps"),
ctx.get("guidance_scale"))["num_inference_steps"])
_bar["offset"] = index * per_pass
_progress.update(segment=index + 1, segments=segments,
step=index * per_pass, steps=per_pass * segments)
out = _one_pass(pipe, task, stage, ctx, out = _one_pass(pipe, task, stage, ctx,
cond=cur if (index or first_is_continuation) else None, log=log) cond=cur if (index or first_is_continuation) else None, log=log)
new = _frames_from_output(out) new = _frames_from_output(out)
...@@ -1354,6 +1421,8 @@ def generate(body: dict) -> dict: ...@@ -1354,6 +1421,8 @@ def generate(body: dict) -> dict:
frames = None frames = None
yielded = 0 yielded = 0
with _lock: with _lock:
_bar["offset"] = 0
_bar["pass_steps"] = 0
_progress.update(active=True, stage="", segment=0, segments=segments, _progress.update(active=True, stage="", segment=0, segments=segments,
step=0, steps=0) step=0, steps=0)
try: try:
...@@ -1377,6 +1446,12 @@ def generate(body: dict) -> dict: ...@@ -1377,6 +1446,12 @@ def generate(body: dict) -> dict:
if image is not None: if image is not None:
kw["image"] = image kw["image"] = image
kw["num_cond_frames"] = 1 kw["num_cond_frames"] = 1
with _lock:
# Refinement is one pass over the whole video, so the segment
# offset from the stage before it must not carry over.
_bar["offset"] = 0
_progress.update(segment=1, segments=1, step=0,
steps=params["num_inference_steps"])
log(f"stage 'refinement': refining {len(frames)} frames") log(f"stage 'refinement': refining {len(frames)} frames")
frames = _frames_from_output(pipe.generate_refine(**kw)[0]) frames = _frames_from_output(pipe.generate_refine(**kw)[0])
else: else:
...@@ -1386,6 +1461,7 @@ def generate(body: dict) -> dict: ...@@ -1386,6 +1461,7 @@ def generate(body: dict) -> dict:
finally: finally:
_yield_flag.clear() _yield_flag.clear()
with _lock: with _lock:
_bar["offset"] = 0
_progress.update(active=False, stage="", step=0) _progress.update(active=False, stage="", step=0)
if stages[-1] == "refinement": if stages[-1] == "refinement":
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment