Source code for oumi.core.inference.progress_reporter

# Copyright 2025 - Oumi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import threading
import time
from datetime import datetime, timezone
from pathlib import Path

from oumi.utils.logging import logger


[docs] class ProgressFileReporter: """Writes inference progress counters to a JSON file for external pollers. The snapshot format is:: {"total": N, "completed": n, "failed": f, "updated_at": "<iso8601 utc>"} The run is complete when ``completed + failed == total``. Writes are atomic (temp file + ``os.replace``), so a polling process never observes partial JSON, and are throttled to at most one per ``min_write_interval`` seconds (``start()`` and ``finalize()`` always write). Filesystem failures are logged and swallowed: a broken progress path must never kill inference. Counter updates are thread-safe. """ def __init__(self, path: str, total: int, min_write_interval: float = 1.0): """Initializes the reporter. Args: path: Destination file for the JSON snapshot. total: Total number of rows in the run. min_write_interval: Minimum seconds between snapshot writes. """ self._path = Path(path) self._tmp_path = self._path.with_name(self._path.name + ".tmp") self._total = total self._min_write_interval = min_write_interval self._completed = 0 self._failed = 0 self._last_write_time = 0.0 self._warned = False self._lock = threading.Lock()
[docs] def start(self, completed: int = 0, failed: int = 0) -> None: """Initializes counters and writes the first snapshot.""" with self._lock: self._completed = completed self._failed = failed self._write_snapshot()
[docs] def record_completed(self, n: int = 1) -> None: """Records n successfully completed rows.""" with self._lock: self._completed += n self._maybe_write_snapshot()
[docs] def record_failed(self, n: int = 1) -> None: """Records n failed rows.""" with self._lock: self._failed += n self._maybe_write_snapshot()
[docs] def finalize(self) -> None: """Writes a final snapshot, bypassing the throttle.""" with self._lock: self._write_snapshot()
def _maybe_write_snapshot(self) -> None: if time.monotonic() - self._last_write_time >= self._min_write_interval: self._write_snapshot() def _write_snapshot(self) -> None: self._last_write_time = time.monotonic() snapshot = { "total": self._total, "completed": self._completed, "failed": self._failed, "updated_at": datetime.now(timezone.utc).isoformat(), } try: self._path.parent.mkdir(parents=True, exist_ok=True) # Write to a temp file in the same directory so the replace is atomic. with open(self._tmp_path, "w") as f: json.dump(snapshot, f) self._tmp_path.replace(self._path) except Exception as e: if not self._warned: logger.warning(f"Failed to write inference progress file: {e}") self._warned = True else: logger.debug(f"Failed to write inference progress file: {e}")