Some progress logs are needed but repeat redundantly: during a long model load
the inference subprocess streams the same keepalive status (e.g. "Downloading
(xet transport)...", "Loading model...") every tick, and the orchestrator logged
each one. Add a ProgressThrottle that logs the first message for a key, any time
the message changes (a phase change), then at most once per interval while it
stays the same; start/completion/errors keep logging at their own sites.
- loggers/progress.py: ProgressThrottle (dedupe + heartbeat), verbose-aware,
interval via UNSLOTH_STUDIO_PROGRESS_LOG_INTERVAL_S (default 10s; 0 = log all).
- core/inference/orchestrator.py: throttle the two "Subprocess status" sites; the
deadline reset still runs on every tick (only the log line is throttled); reset
the key on load completion so the next load logs immediately.
- core/training/training.py: per-step metrics already stream over SSE (not logged
per step), so add a throttled progress heartbeat ("step N/M, loss=..., epoch")
so training progress is visible in the log without a line per step.
- tests/test_progress_throttle.py: first/changed/heartbeat/reset/verbose/zero.