Reuse formatEta, compute generation ETA once per step, guard re-renders
This commit is contained in:
parent
54d01a505f
commit
2bf9d73b4a
3 changed files with 44 additions and 27 deletions
|
|
@ -64,6 +64,18 @@ class _GenState:
|
|||
# Set when the first step finishes; the ETA rate is measured from there so the
|
||||
# slower first step (warmup) doesn't skew it.
|
||||
first_step_at: float = 0.0
|
||||
# Computed once per step (in the callback) so it's stable between polls.
|
||||
eta_seconds: Optional[float] = None
|
||||
|
||||
|
||||
def _estimate_eta(total_steps: int, step: int, first_step_at: float, now: float) -> Optional[float]:
|
||||
"""Seconds remaining, from the average step time measured after the first step.
|
||||
None until at least one step has elapsed since the first."""
|
||||
steps_since_first = step - 1
|
||||
if not first_step_at or steps_since_first <= 0:
|
||||
return None
|
||||
per_step = (now - first_step_at) / steps_since_first
|
||||
return max(0.0, (total_steps - step) * per_step)
|
||||
|
||||
|
||||
class DiffusionBackend:
|
||||
|
|
@ -332,9 +344,11 @@ class DiffusionBackend:
|
|||
gen = _GenState(total_steps = steps)
|
||||
|
||||
def _on_step(pipe, step_index, timestep, callback_kwargs):
|
||||
now = time.time()
|
||||
gen.step = step_index + 1
|
||||
if gen.first_step_at == 0.0:
|
||||
gen.first_step_at = time.time()
|
||||
gen.first_step_at = now
|
||||
gen.eta_seconds = _estimate_eta(gen.total_steps, gen.step, gen.first_step_at, now)
|
||||
return callback_kwargs
|
||||
|
||||
# Not every pipeline accepts the callback; only pass it where supported
|
||||
|
|
@ -356,18 +370,12 @@ class DiffusionBackend:
|
|||
gen = self._gen
|
||||
if gen is None or gen.total_steps <= 0:
|
||||
return {"active": False, "step": 0, "total_steps": 0, "fraction": 0.0, "eta_seconds": None}
|
||||
step, total = gen.step, gen.total_steps
|
||||
eta = None
|
||||
steps_since_first = step - 1
|
||||
if gen.first_step_at and steps_since_first > 0:
|
||||
per_step = (time.time() - gen.first_step_at) / steps_since_first
|
||||
eta = max(0.0, (total - step) * per_step)
|
||||
return {
|
||||
"active": True,
|
||||
"step": step,
|
||||
"total_steps": total,
|
||||
"fraction": min(step / total, 1.0),
|
||||
"eta_seconds": eta,
|
||||
"step": gen.step,
|
||||
"total_steps": gen.total_steps,
|
||||
"fraction": min(gen.step / gen.total_steps, 1.0),
|
||||
"eta_seconds": gen.eta_seconds,
|
||||
}
|
||||
|
||||
def unload(self) -> dict[str, Any]:
|
||||
|
|
|
|||
|
|
@ -296,20 +296,28 @@ def test_load_progress_fraction_clamped(monkeypatch):
|
|||
assert p["bytes_downloaded"] == 1000 # clamped to the estimate
|
||||
|
||||
|
||||
def test_generate_progress_reports_step_and_eta():
|
||||
import time as _time
|
||||
def test_estimate_eta():
|
||||
from core.inference.diffusion import _estimate_eta
|
||||
|
||||
# No rate yet until a step has elapsed since the first.
|
||||
assert _estimate_eta(8, 1, first_step_at = 100.0, now = 100.0) is None
|
||||
assert _estimate_eta(8, 0, first_step_at = 0.0, now = 100.0) is None
|
||||
# 3 steps in 3s since the first ⇒ 1s/step ⇒ 4 steps left ⇒ ~4s.
|
||||
assert _estimate_eta(8, 4, first_step_at = 100.0, now = 103.0) == 4.0
|
||||
# Last step ⇒ 0 remaining.
|
||||
assert _estimate_eta(8, 8, first_step_at = 100.0, now = 107.0) == 0.0
|
||||
|
||||
|
||||
def test_generate_progress_reads_gen_state():
|
||||
from core.inference.diffusion import _GenState
|
||||
|
||||
backend = DiffusionBackend()
|
||||
assert backend.generate_progress()["active"] is False
|
||||
|
||||
# Halfway through an 8-step run, ~1s/step measured since the first step.
|
||||
backend._gen = _GenState(total_steps = 8, step = 4, first_step_at = _time.time() - 3)
|
||||
backend._gen = _GenState(total_steps = 8, step = 4, eta_seconds = 4.0)
|
||||
p = backend.generate_progress()
|
||||
assert p["active"] is True and p["step"] == 4 and p["total_steps"] == 8
|
||||
assert abs(p["fraction"] - 0.5) < 1e-9
|
||||
assert p["eta_seconds"] is not None and 0 < p["eta_seconds"] < 30 # ~4 steps left
|
||||
assert abs(p["fraction"] - 0.5) < 1e-9 and p["eta_seconds"] == 4.0
|
||||
|
||||
|
||||
def test_begin_load_rejects_concurrent(monkeypatch):
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import type {
|
|||
ModelSelectorChangeMeta,
|
||||
} from "@/components/assistant-ui/model-selector/types";
|
||||
import { ModelLoadDescription } from "@/features/chat/components/model-load-status";
|
||||
import { formatBytes } from "@/features/hub/lib/format";
|
||||
import { formatBytes, formatEta } from "@/features/hub/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
|
|
@ -137,16 +137,12 @@ function formatTimestamp(epochSeconds: number): string {
|
|||
return new Date(epochSeconds * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function formatEta(seconds: number): string {
|
||||
if (seconds < 1) return "<1s";
|
||||
if (seconds < 60) return `${Math.round(seconds)}s`;
|
||||
return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
|
||||
}
|
||||
|
||||
// Bar label for an in-flight generation: step count plus an ETA once it's known.
|
||||
// Bar label for an in-flight generation: step count plus an ETA once it's known
|
||||
// (formatEta returns "" for non-positive, so the last step shows just the step).
|
||||
function genStepLabel(p: DiffusionGenerateProgress): string {
|
||||
const base = `Step ${p.step}/${p.total_steps}`;
|
||||
return p.eta_seconds != null ? `${base} · ~${formatEta(p.eta_seconds)} left` : base;
|
||||
const eta = p.eta_seconds != null ? formatEta(p.eta_seconds) : "";
|
||||
return eta ? `${base} · ~${eta}` : base;
|
||||
}
|
||||
|
||||
// The chat tab's model-load toast styling, reused verbatim so the diffusion
|
||||
|
|
@ -626,7 +622,12 @@ export function ImagesPage() {
|
|||
genPollTimer.current = setInterval(async () => {
|
||||
try {
|
||||
const p = await getGenerateProgress();
|
||||
setGenStep(p.active ? p : null);
|
||||
// Skip the state update (and re-render) when nothing the bar shows moved.
|
||||
setGenStep((prev) => {
|
||||
if (!p.active) return null;
|
||||
if (prev && prev.step === p.step && prev.eta_seconds === p.eta_seconds) return prev;
|
||||
return p;
|
||||
});
|
||||
} catch {
|
||||
// transient; keep polling
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue