test_621 / trackio /run.py
abidlabs's picture
abidlabs HF Staff
Upload folder using huggingface_hub
a36238b verified
Raw
History Blame Contribute Delete
68.3 kB
import os
import shutil
import threading
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import huggingface_hub
from gradio_client import handle_file
from trackio import cas, fragments, references, utils
from trackio.alerts import (
AlertLevel,
format_alert_terminal,
resolve_webhook_min_level,
send_webhook,
should_send_webhook,
)
from trackio.apple_gpu import AppleGpuMonitor, apple_gpu_available
from trackio.artifact import Artifact
from trackio.cpu import CpuMonitor
from trackio.gpu import GpuMonitor, gpu_available
from trackio.histogram import Histogram
from trackio.markdown import Markdown
from trackio.media import TrackioMedia, get_project_media_path
from trackio.pending_uploads import classify_pending_uploads, replay_pending_uploads
from trackio.remote_client import RemoteClient, is_transient_remote_error
from trackio.sqlite_storage import SQLiteStorage
from trackio.table import Table
from trackio.trace import Trace
from trackio.typehints import AlertEntry, LogEntry, SystemLogEntry, UploadEntry
from trackio.utils import MEDIA_DIR, _emit_nonfatal_warning, _get_default_namespace
BATCH_SEND_INTERVAL = 0.5
MAX_BACKOFF = 30
BUCKET_FLUSH_INTERVAL = 30
ARTIFACT_LOG_RETRY_BACKOFFS = (0.5, 1.0, 2.0)
class Run:
def __init__(
self,
url: str | None,
project: str,
client: Any | None,
name: str | None = None,
run_id: str | None = None,
group: str | None = None,
config: dict | None = None,
space_id: str | None = None,
bucket_id: str | None = None,
server_base_url: str | None = None,
write_token: str | None = None,
existing_runs: list[str] | None = None,
initial_last_step: int | None = None,
auto_log_gpu: bool = False,
gpu_log_interval: float = 10.0,
auto_log_cpu: bool = False,
cpu_log_interval: float = 10.0,
webhook_url: str | None = None,
webhook_min_level: AlertLevel | str | None = None,
):
"""
Initialize a Run for logging metrics to Trackio.
Args:
url: The URL or Space id of the Trackio server.
project: The name of the project to log metrics to.
client: A pre-configured Trackio-compatible client instance, or None to
create one automatically in a background thread with retry logic.
Passing None is recommended for normal usage. Passing a client
is useful for testing (e.g., injecting a mock client).
name: The name of this run. If None, a readable name like
"brave-sunset-0" is auto-generated. If space_id is provided,
generates a "username-timestamp" format instead.
group: Optional group name to organize related runs together.
config: A dictionary of configuration/hyperparameters for this run.
Keys starting with '_' are reserved for internal use.
space_id: The HF Space ID if logging to a Space (e.g., "user/space").
If provided, media files will be uploaded to the Space.
bucket_id: The HF Bucket ID attached to the Space, if any. When set,
logs that cannot be delivered to the Space are written as JSONL
fragments directly to the Bucket inbox so they survive ephemeral
environments; the Space imports them once it is running.
existing_runs: Optional pre-fetched run names for this project. Used to
avoid redundant storage or remote lookups during init.
initial_last_step: Optional pre-fetched last step for a resumed run.
auto_log_gpu: Whether to automatically log GPU metrics (utilization,
memory, temperature) at regular intervals.
gpu_log_interval: The interval in seconds between GPU metric logs.
Only used when auto_log_gpu is True.
auto_log_cpu: Whether to automatically log CPU and RAM metrics
(utilization, memory, disk I/O, network I/O, sensors) at regular
intervals.
cpu_log_interval: The interval in seconds between CPU metric logs.
Only used when auto_log_cpu is True.
webhook_url: A webhook URL to POST alert payloads to. Supports
Slack and Discord webhook URLs natively. Can also be set via
the TRACKIO_WEBHOOK_URL environment variable.
webhook_min_level: Minimum alert level that should trigger webhook
delivery. For example, `AlertLevel.WARN` sends only WARN and
ERROR alerts to webhook destinations. Can also be set via
`TRACKIO_WEBHOOK_MIN_LEVEL`.
"""
self.url = url
self.project = project
self._client_lock = threading.Lock()
self._warning_lock = threading.Lock()
self._warned_failures: set[str] = set()
self._local_sender_thread: threading.Thread | None = None
self._client_thread = None
self._client = client
self._space_id = space_id
self._bucket_id = bucket_id if space_id is not None else None
self._server_base_url = server_base_url
self._write_token = write_token
self._remote_storage_key = space_id or server_base_url
self._storage_mode = utils.get_storage_mode()
self._fragment_writer = fragments.FragmentWriter()
self._last_bucket_flush: float | None = None
self._spilled_metric_ids: set[int] = set()
self._spilled_system_ids: set[int] = set()
self.id = run_id or uuid.uuid4().hex
self._existing_runs = existing_runs
self._initial_last_step = initial_last_step
if name is not None:
self.name = name
else:
try:
self.name = utils.generate_readable_name(
self._safe_get_existing_runs(), self._space_id
)
except Exception as e:
self._warn_once(
"init-run-name",
f"trackio.init() could not generate a run name: {e}. Falling back to a random name.",
)
self.name = f"trackio-run-{uuid.uuid4().hex[:8]}"
self.group = group
try:
self.config = utils.to_json_safe(config or {})
except Exception as e:
self._warn_once(
"init-config",
f"trackio.init() failed to serialize the run config: {e}. Continuing without config.",
)
self.config = {}
if isinstance(self.config, dict):
for key in self.config:
if key.startswith("_"):
raise ValueError(
f"Config key '{key}' is reserved (keys starting with '_' are reserved for internal use)"
)
self.config["_Username"] = self._get_username()
self.config["_Created"] = datetime.now(timezone.utc).isoformat()
self.config["_Group"] = self.group
self._queued_logs: list[LogEntry] = []
self._queued_system_logs: list[SystemLogEntry] = []
self._queued_uploads: list[UploadEntry] = []
self._queued_alerts: list[AlertEntry] = []
self._stop_flag = threading.Event()
self._config_logged = False
max_step = self._safe_get_max_step_for_run()
self._next_step = 0 if max_step is None else max_step + 1
self._has_local_buffer = False
self._is_local = space_id is None and server_base_url is None
self._webhook_url = webhook_url or os.environ.get("TRACKIO_WEBHOOK_URL")
self._webhook_min_level = resolve_webhook_min_level(
webhook_min_level or os.environ.get("TRACKIO_WEBHOOK_MIN_LEVEL")
)
if self._is_local:
self._start_background_thread(
"_local_sender_thread",
self._local_batch_sender,
warning_key="local-sender-thread",
description="local Trackio logging thread",
)
else:
self._start_background_thread(
"_client_thread",
self._init_client_background,
warning_key="remote-sender-thread",
description="remote Trackio logging thread",
)
self._gpu_monitor: "GpuMonitor | AppleGpuMonitor | None" = None
if auto_log_gpu:
try:
if gpu_available():
self._gpu_monitor = GpuMonitor(self, interval=gpu_log_interval)
self._gpu_monitor.start()
elif apple_gpu_available():
self._gpu_monitor = AppleGpuMonitor(
self,
interval=gpu_log_interval,
include_cpu_metrics=not auto_log_cpu,
)
self._gpu_monitor.start()
except Exception as e:
self._warn_once(
"gpu-monitor",
f"trackio.init() failed to start automatic GPU logging: {e}. Continuing without system metric auto-logging.",
)
self._cpu_monitor: "CpuMonitor | None" = None
if auto_log_cpu:
try:
self._cpu_monitor = CpuMonitor(self, interval=cpu_log_interval)
self._cpu_monitor.start()
except Exception as e:
self._warn_once(
"cpu-monitor",
f"trackio.init() failed to start automatic CPU logging: {e}. Continuing without CPU metric auto-logging.",
)
def _hf_token_for_remote(self) -> str | None:
return huggingface_hub.utils.get_token() if self._space_id else None
def _remote_source_dict(self) -> dict:
return {
"space_id": self._space_id,
"server_base_url": self._server_base_url,
"write_token": self._write_token,
}
def _get_username(self) -> str | None:
try:
return _get_default_namespace()
except Exception:
return None
def _warn_once(self, key: str, message: str) -> None:
with self._warning_lock:
if key in self._warned_failures:
return
self._warned_failures.add(key)
_emit_nonfatal_warning(message)
def _safe_get_existing_runs(self) -> list[str]:
if self._existing_runs is not None:
return self._existing_runs
try:
return SQLiteStorage.get_runs(self.project)
except Exception as e:
self._warn_once(
"init-existing-runs",
f"trackio.init() could not inspect existing runs for project '{self.project}': {e}. Continuing without prior-run metadata.",
)
return []
def _safe_get_max_step_for_run(self) -> int | None:
if self._initial_last_step is not None:
return self._initial_last_step
try:
return SQLiteStorage.get_max_step_for_run(
self.project, self.name, run_id=self.id
)
except Exception as e:
self._warn_once(
"init-max-step",
f"trackio.init() could not recover the previous step for run '{self.name}': {e}. Continuing from step 0.",
)
return None
def _start_background_thread(
self,
attr_name: str,
target,
*,
warning_key: str,
description: str,
) -> bool:
try:
thread = threading.Thread(target=target, daemon=True)
setattr(self, attr_name, thread)
thread.start()
return True
except Exception as e:
setattr(self, attr_name, None)
self._warn_once(
warning_key,
f"trackio failed to start the {description}: {e}. Logging will continue in degraded mode.",
)
return False
def _thread_is_alive(self, attr_name: str) -> bool:
thread = getattr(self, attr_name, None)
return isinstance(thread, threading.Thread) and thread.is_alive()
def _flush_queues_inline(self) -> None:
if self._is_local:
if self._queued_logs:
logs_to_send = self._queued_logs.copy()
self._queued_logs.clear()
self._write_logs_to_sqlite(logs_to_send)
if self._queued_system_logs:
system_logs_to_send = self._queued_system_logs.copy()
self._queued_system_logs.clear()
self._write_system_logs_to_sqlite(system_logs_to_send)
if self._queued_alerts:
alerts_to_send = self._queued_alerts.copy()
self._queued_alerts.clear()
self._write_alerts_to_sqlite(alerts_to_send)
return
if self._queued_logs:
logs_to_send = self._queued_logs.copy()
self._queued_logs.clear()
self._persist_logs_locally(logs_to_send)
if self._queued_system_logs:
system_logs_to_send = self._queued_system_logs.copy()
self._queued_system_logs.clear()
self._persist_system_logs_locally(system_logs_to_send)
if self._queued_uploads:
uploads_to_send = self._queued_uploads.copy()
self._queued_uploads.clear()
self._persist_uploads_locally(uploads_to_send)
if self._queued_alerts:
alerts_to_send = self._queued_alerts.copy()
self._queued_alerts.clear()
self._write_alerts_to_sqlite(alerts_to_send)
def _local_batch_sender(self):
while (
not self._stop_flag.is_set()
or len(self._queued_logs) > 0
or len(self._queued_system_logs) > 0
or len(self._queued_alerts) > 0
):
if not self._stop_flag.is_set():
self._stop_flag.wait(timeout=BATCH_SEND_INTERVAL)
try:
with self._client_lock:
if self._queued_logs:
logs_to_send = self._queued_logs.copy()
self._queued_logs.clear()
self._write_logs_to_sqlite(logs_to_send)
if self._queued_system_logs:
system_logs_to_send = self._queued_system_logs.copy()
self._queued_system_logs.clear()
self._write_system_logs_to_sqlite(system_logs_to_send)
if self._queued_alerts:
alerts_to_send = self._queued_alerts.copy()
self._queued_alerts.clear()
self._write_alerts_to_sqlite(alerts_to_send)
except Exception as e:
self._warn_once(
"local-sender-loop",
f"trackio's local logging thread hit an internal error: {e}. User code will continue, but some Trackio data may be dropped.",
)
def _stamped_records(self, records: list[dict]) -> list[dict]:
timestamp = datetime.now(timezone.utc).isoformat()
for record in records:
if not record.get("timestamp"):
record["timestamp"] = timestamp
return records
def _attach_run_config(self, records: list[dict]) -> list[dict]:
if not self.config:
return records
if any(record.get("config") for record in records):
return records
for record in records:
if record.get("run_id") == self.id:
record["config"] = utils.to_json_safe(self.config)
break
return records
def _write_records_to_local_inbox(self, records: list[dict], warning_key: str):
try:
self._fragment_writer.write_local(self._stamped_records(records))
except Exception as e:
self._warn_once(
warning_key,
f"trackio failed to write a JSONL fragment for run '{self.name}': {e}. User code will continue, but this batch could not be persisted.",
)
def _write_logs_to_sqlite(self, logs: list[LogEntry]):
if self._storage_mode == "jsonl":
self._write_records_to_local_inbox(
[fragments.metric_record(entry) for entry in logs],
"write-logs-fragment",
)
return
try:
logs_by_run: dict[tuple, dict] = {}
for entry in logs:
key = (entry["project"], entry["run"], entry.get("run_id"))
if key not in logs_by_run:
logs_by_run[key] = {
"metrics": [],
"steps": [],
"log_ids": [],
"config": None,
}
logs_by_run[key]["metrics"].append(entry["metrics"])
logs_by_run[key]["steps"].append(entry.get("step"))
logs_by_run[key]["log_ids"].append(entry.get("log_id"))
if entry.get("config") and logs_by_run[key]["config"] is None:
logs_by_run[key]["config"] = entry["config"]
for (project, run, run_id), data in logs_by_run.items():
has_log_ids = any(lid is not None for lid in data["log_ids"])
SQLiteStorage.bulk_log(
project=project,
run=run,
run_id=run_id,
metrics_list=data["metrics"],
steps=data["steps"],
config=data["config"],
log_ids=data["log_ids"] if has_log_ids else None,
)
except Exception as e:
self._warn_once(
"write-logs-to-sqlite",
f"trackio failed to flush metric logs for run '{self.name}': {e}. User code will continue, but this batch could not be persisted.",
)
def _write_system_logs_to_sqlite(self, logs: list[SystemLogEntry]):
if self._storage_mode == "jsonl":
self._write_records_to_local_inbox(
[fragments.system_metric_record(entry) for entry in logs],
"write-system-logs-fragment",
)
return
try:
logs_by_run: dict[tuple, dict] = {}
for entry in logs:
key = (entry["project"], entry["run"], entry.get("run_id"))
if key not in logs_by_run:
logs_by_run[key] = {"metrics": [], "timestamps": [], "log_ids": []}
logs_by_run[key]["metrics"].append(entry["metrics"])
logs_by_run[key]["timestamps"].append(entry.get("timestamp"))
logs_by_run[key]["log_ids"].append(entry.get("log_id"))
for (project, run, run_id), data in logs_by_run.items():
has_log_ids = any(lid is not None for lid in data["log_ids"])
SQLiteStorage.bulk_log_system(
project=project,
run=run,
run_id=run_id,
metrics_list=data["metrics"],
timestamps=data["timestamps"],
log_ids=data["log_ids"] if has_log_ids else None,
)
except Exception as e:
self._warn_once(
"write-system-logs-to-sqlite",
f"trackio failed to flush system logs for run '{self.name}': {e}. User code will continue, but this batch could not be persisted.",
)
def _write_alerts_to_sqlite(self, alerts: list[AlertEntry]):
records = [fragments.alert_record(entry) for entry in alerts]
if self._remote_storage_key and self._bucket_id is not None:
try:
self._fragment_writer.write_to_bucket(
self._stamped_records(records), self._bucket_id
)
return
except Exception:
pass
if self._storage_mode == "jsonl":
self._write_records_to_local_inbox(records, "write-alerts-fragment")
return
try:
alerts_by_run: dict[tuple, dict] = {}
for entry in alerts:
key = (entry["project"], entry["run"], entry.get("run_id"))
if key not in alerts_by_run:
alerts_by_run[key] = {
"titles": [],
"texts": [],
"levels": [],
"steps": [],
"timestamps": [],
"alert_ids": [],
}
alerts_by_run[key]["titles"].append(entry["title"])
alerts_by_run[key]["texts"].append(entry.get("text"))
alerts_by_run[key]["levels"].append(entry["level"])
alerts_by_run[key]["steps"].append(entry.get("step"))
alerts_by_run[key]["timestamps"].append(entry.get("timestamp"))
alerts_by_run[key]["alert_ids"].append(entry.get("alert_id"))
for (project, run, run_id), data in alerts_by_run.items():
has_alert_ids = any(aid is not None for aid in data["alert_ids"])
SQLiteStorage.bulk_alert(
project=project,
run=run,
run_id=run_id,
titles=data["titles"],
texts=data["texts"],
levels=data["levels"],
steps=data["steps"],
timestamps=data["timestamps"],
alert_ids=data["alert_ids"] if has_alert_ids else None,
)
except Exception as e:
self._warn_once(
"write-alerts-to-sqlite",
f"trackio failed to flush alerts for run '{self.name}': {e}. User code will continue, but this batch could not be persisted.",
)
def _batch_sender(self):
consecutive_failures = 0
while (
not self._stop_flag.is_set()
or len(self._queued_logs) > 0
or len(self._queued_system_logs) > 0
or len(self._queued_uploads) > 0
or len(self._queued_alerts) > 0
or self._has_local_buffer
):
if not self._stop_flag.is_set():
if consecutive_failures:
sleep_time = min(
BATCH_SEND_INTERVAL * (2**consecutive_failures), MAX_BACKOFF
)
else:
sleep_time = BATCH_SEND_INTERVAL
self._stop_flag.wait(timeout=sleep_time)
elif self._has_local_buffer:
self._stop_flag.wait(timeout=BATCH_SEND_INTERVAL)
try:
with self._client_lock:
if self._client is None:
if self._stop_flag.is_set():
if self._queued_logs:
self._persist_logs_locally(self._queued_logs)
self._queued_logs.clear()
if self._queued_system_logs:
self._persist_system_logs_locally(
self._queued_system_logs
)
self._queued_system_logs.clear()
if self._queued_uploads:
self._persist_uploads_locally(self._queued_uploads)
self._queued_uploads.clear()
if self._queued_alerts:
self._write_alerts_to_sqlite(self._queued_alerts)
self._queued_alerts.clear()
return
failed = False
if self._queued_logs:
logs_to_send = self._queued_logs.copy()
self._queued_logs.clear()
try:
self._client.predict(
api_name="/bulk_log",
logs=logs_to_send,
hf_token=self._hf_token_for_remote(),
)
except Exception:
self._persist_logs_locally(logs_to_send)
failed = True
if self._queued_system_logs:
system_logs_to_send = self._queued_system_logs.copy()
self._queued_system_logs.clear()
try:
self._client.predict(
api_name="/bulk_log_system",
logs=system_logs_to_send,
hf_token=self._hf_token_for_remote(),
)
except Exception:
self._persist_system_logs_locally(system_logs_to_send)
failed = True
if self._queued_uploads:
uploads_to_send = self._queued_uploads.copy()
self._queued_uploads.clear()
try:
self._client.predict(
api_name="/bulk_upload_media",
uploads=uploads_to_send,
hf_token=self._hf_token_for_remote(),
)
except Exception:
self._persist_uploads_locally(uploads_to_send)
failed = True
if self._queued_alerts:
alerts_to_send = self._queued_alerts.copy()
self._queued_alerts.clear()
try:
self._client.predict(
api_name="/bulk_alert",
alerts=alerts_to_send,
hf_token=self._hf_token_for_remote(),
)
except Exception:
self._write_alerts_to_sqlite(alerts_to_send)
failed = True
if failed:
consecutive_failures += 1
else:
consecutive_failures = 0
if self._has_local_buffer:
flushed = self._flush_local_buffer()
if (
not flushed
and self._stop_flag.is_set()
and self._bucket_id is not None
):
return
except Exception as e:
consecutive_failures += 1
self._warn_once(
"remote-sender-loop",
f"trackio's remote logging thread hit an internal error: {e}. User code will continue while Trackio retries in the background.",
)
def _persist_records_as_fragments(self, records: list[dict], warning_key: str):
records = self._stamped_records(records)
if self._bucket_id is not None:
try:
self._fragment_writer.write_to_bucket(records, self._bucket_id)
return
except Exception:
pass
self._write_records_to_local_inbox(records, warning_key)
def _persist_logs_locally(self, logs: list[LogEntry]):
if not self._remote_storage_key:
return
if self._storage_mode == "jsonl":
self._persist_records_as_fragments(
self._attach_run_config(
[fragments.metric_record(entry) for entry in logs]
),
"persist-logs-fragment",
)
return
try:
logs_by_run: dict[tuple, dict] = {}
for entry in logs:
key = (entry["project"], entry["run"], entry.get("run_id"))
if key not in logs_by_run:
logs_by_run[key] = {
"metrics": [],
"steps": [],
"log_ids": [],
"config": None,
}
logs_by_run[key]["metrics"].append(entry["metrics"])
logs_by_run[key]["steps"].append(entry.get("step"))
logs_by_run[key]["log_ids"].append(entry.get("log_id"))
if entry.get("config") and logs_by_run[key]["config"] is None:
logs_by_run[key]["config"] = entry["config"]
for (project, run, run_id), data in logs_by_run.items():
SQLiteStorage.bulk_log(
project=project,
run=run,
run_id=run_id,
metrics_list=data["metrics"],
steps=data["steps"],
log_ids=data["log_ids"],
config=data["config"],
space_id=self._remote_storage_key,
)
self._has_local_buffer = True
except Exception as e:
self._warn_once(
"persist-logs-locally",
f"trackio could not persist failed remote metric logs locally for run '{self.name}': {e}. User code will continue, but this batch could be lost.",
)
def _persist_system_logs_locally(self, logs: list[SystemLogEntry]):
if not self._remote_storage_key:
return
if self._storage_mode == "jsonl":
self._persist_records_as_fragments(
[fragments.system_metric_record(entry) for entry in logs],
"persist-system-logs-fragment",
)
return
try:
logs_by_run: dict[tuple, dict] = {}
for entry in logs:
key = (entry["project"], entry["run"], entry.get("run_id"))
if key not in logs_by_run:
logs_by_run[key] = {"metrics": [], "timestamps": [], "log_ids": []}
logs_by_run[key]["metrics"].append(entry["metrics"])
logs_by_run[key]["timestamps"].append(entry.get("timestamp"))
logs_by_run[key]["log_ids"].append(entry.get("log_id"))
for (project, run, run_id), data in logs_by_run.items():
SQLiteStorage.bulk_log_system(
project=project,
run=run,
run_id=run_id,
metrics_list=data["metrics"],
timestamps=data["timestamps"],
log_ids=data["log_ids"],
space_id=self._remote_storage_key,
)
self._has_local_buffer = True
except Exception as e:
self._warn_once(
"persist-system-logs-locally",
f"trackio could not persist failed remote system logs locally for run '{self.name}': {e}. User code will continue, but this batch could be lost.",
)
@staticmethod
def _upload_entry_file_path(entry: UploadEntry) -> str:
file_data = entry.get("uploaded_file")
if isinstance(file_data, dict):
return file_data.get("path", "")
elif hasattr(file_data, "path"):
return str(file_data.path)
return str(file_data)
def _persist_uploads_locally(self, uploads: list[UploadEntry]):
if not self._remote_storage_key:
return
if self._storage_mode == "jsonl" and self._bucket_id is not None:
try:
fragments.upload_media_files_to_bucket(
self._bucket_id,
[
{
"project": entry["project"],
"run": entry.get("run"),
"step": entry.get("step"),
"relative_path": entry.get("relative_path"),
"file_path": self._upload_entry_file_path(entry),
}
for entry in uploads
],
)
return
except Exception as e:
self._warn_once(
"persist-uploads-fragment",
f"trackio could not upload media files to Bucket '{self._bucket_id}' for run '{self.name}': {e}. They will be kept in the local pending buffer instead.",
)
try:
for entry in uploads:
file_path = self._upload_entry_file_path(entry)
SQLiteStorage.add_pending_upload(
project=entry["project"],
space_id=self._remote_storage_key,
run_id=entry.get("run_id"),
run_name=entry.get("run"),
step=entry.get("step"),
file_path=file_path,
relative_path=entry.get("relative_path"),
)
self._has_local_buffer = True
except Exception as e:
self._warn_once(
"persist-uploads-locally",
f"trackio could not persist failed remote file uploads locally for run '{self.name}': {e}. User code will continue, but some artifacts could be lost.",
)
def _warn_missing_uploads(self, count: int, sample: str) -> None:
self._warn_once(
"pending-uploads-missing-files",
f"trackio dropped {count} pending upload(s) whose local files no "
f"longer exist (e.g. {sample!r}). Data for these uploads was not "
"uploaded.",
)
def _send_pending_uploads_to_server(self, buffered: dict) -> None:
"""Group buffered pending_uploads by kind and POST to the right endpoints."""
replay_pending_uploads(
buffered,
self.project,
predict=self._client.predict,
hf_token=self._hf_token_for_remote(),
warn_missing=self._warn_missing_uploads,
)
def _flush_local_buffer(self) -> bool:
try:
buffered_logs = SQLiteStorage.get_pending_logs(self.project)
if buffered_logs:
self._client.predict(
api_name="/bulk_log",
logs=buffered_logs["logs"],
hf_token=self._hf_token_for_remote(),
)
SQLiteStorage.clear_pending_logs(self.project, buffered_logs["ids"])
buffered_sys = SQLiteStorage.get_pending_system_logs(self.project)
if buffered_sys:
self._client.predict(
api_name="/bulk_log_system",
logs=buffered_sys["logs"],
hf_token=self._hf_token_for_remote(),
)
SQLiteStorage.clear_pending_system_logs(
self.project, buffered_sys["ids"]
)
buffered_uploads = SQLiteStorage.get_pending_uploads(self.project)
if buffered_uploads:
self._send_pending_uploads_to_server(buffered_uploads)
self._has_local_buffer = False
return True
except Exception as e:
self._warn_once(
"flush-local-buffer",
f"trackio could not flush buffered remote data for run '{self.name}': {e}. It will retry later if possible.",
)
return False
def _unspilled_pending(
self, pending: dict | None, spilled_ids: set[int]
) -> list[dict]:
if not pending:
return []
return [
entry
for entry_id, entry in zip(pending["ids"], pending["logs"])
if entry_id not in spilled_ids
]
def _flush_pending_uploads_to_bucket(self) -> None:
pending = SQLiteStorage.get_pending_uploads(self.project)
if not pending:
return
classified = classify_pending_uploads(pending)
missing = classified["missing"]
if missing["ids"]:
self._warn_missing_uploads(len(missing["ids"]), missing["paths"][0])
SQLiteStorage.clear_pending_uploads(self.project, missing["ids"])
media = classified["media"]
if media:
fragments.upload_media_files_to_bucket(
self._bucket_id, [upload for upload, _ in media]
)
SQLiteStorage.clear_pending_uploads(
self.project, [upload_id for _, upload_id in media]
)
blobs = classified["artifact_blobs"]
if blobs:
fragments.upload_artifact_blobs_to_bucket(
self._bucket_id, [upload for upload, _ in blobs]
)
SQLiteStorage.clear_pending_uploads(
self.project, [upload_id for _, upload_id in blobs]
)
def _spill_pending_to_bucket(self):
"""
Spill buffered rows to the Bucket while the Space is unreachable. Metric
and system rows are written without clearing them, so the normal
/bulk_log replay still happens once the Space is reachable (it
deduplicates by log_id). Media and artifact-blob uploads are sent to
their bucket paths and cleared, since they are delivered through the
bucket rather than replayed to the Space.
"""
try:
pending = SQLiteStorage.get_pending_logs(self.project)
entries = self._unspilled_pending(pending, self._spilled_metric_ids)
if entries:
records = self._attach_run_config(
[fragments.metric_record(entry) for entry in entries]
)
self._fragment_writer.write_to_bucket(records, self._bucket_id)
self._spilled_metric_ids.update(pending["ids"])
pending_sys = SQLiteStorage.get_pending_system_logs(self.project)
entries = self._unspilled_pending(pending_sys, self._spilled_system_ids)
if entries:
records = [fragments.system_metric_record(entry) for entry in entries]
self._fragment_writer.write_to_bucket(records, self._bucket_id)
self._spilled_system_ids.update(pending_sys["ids"])
self._flush_pending_uploads_to_bucket()
except Exception as e:
self._warn_once(
"bucket-spill",
f"trackio could not upload buffered logs to Bucket '{self._bucket_id}': {e}. It will retry later if possible.",
)
def _drain_pending_to_bucket(self):
if self._bucket_id is None or not self._has_local_buffer:
return
try:
pending = SQLiteStorage.get_pending_logs(self.project)
if pending:
entries = self._unspilled_pending(pending, self._spilled_metric_ids)
if entries:
records = self._attach_run_config(
[fragments.metric_record(entry) for entry in entries]
)
self._fragment_writer.write_to_bucket(records, self._bucket_id)
SQLiteStorage.clear_pending_logs(self.project, pending["ids"])
self._spilled_metric_ids.update(pending["ids"])
pending_sys = SQLiteStorage.get_pending_system_logs(self.project)
if pending_sys:
entries = self._unspilled_pending(pending_sys, self._spilled_system_ids)
if entries:
records = [
fragments.system_metric_record(entry) for entry in entries
]
self._fragment_writer.write_to_bucket(records, self._bucket_id)
SQLiteStorage.clear_pending_system_logs(
self.project, pending_sys["ids"]
)
self._spilled_system_ids.update(pending_sys["ids"])
self._flush_pending_uploads_to_bucket()
self._has_local_buffer = SQLiteStorage.has_pending_data(self.project)
except Exception as e:
self._warn_once(
"bucket-drain",
f"trackio could not upload buffered logs to Bucket '{self._bucket_id}': {e}. It will retry later if possible.",
)
def _spill_queues_while_waiting(self):
if self._bucket_id is None:
return
if not (
self._queued_logs
or self._queued_system_logs
or self._queued_uploads
or self._queued_alerts
or self._has_local_buffer
):
return
now = time.monotonic()
if (
self._last_bucket_flush is not None
and now - self._last_bucket_flush < BUCKET_FLUSH_INTERVAL
):
return
self._last_bucket_flush = now
with self._client_lock:
if self._client is not None:
return
self._flush_queues_inline()
self._spill_pending_to_bucket()
def _wait_for_client_ready(self, timeout: float = 60.0) -> None:
"""Block until `self._client` is initialized by `_init_client_background`."""
deadline = time.monotonic() + timeout
while self._client is None:
if self._stop_flag.is_set():
raise RuntimeError(
"trackio run is finishing; cannot wait for remote client"
)
if time.monotonic() >= deadline:
raise RuntimeError(f"trackio remote client not ready after {timeout}s")
time.sleep(0.1)
def _drain_pending_uploads(self) -> None:
"""Synchronously flush pending_uploads (both kinds). Raises on failure."""
with self._client_lock:
if self._client is None:
raise RuntimeError(
"trackio remote client not ready; cannot drain pending_uploads"
)
buffered = SQLiteStorage.get_pending_uploads(self.project)
if not buffered:
return
self._send_pending_uploads_to_server(buffered)
def _init_client_background(self):
if self._client is None:
fib = utils.fibo()
for sleep_coefficient in fib:
if self._stop_flag.is_set():
break
try:
if self._server_base_url is not None:
client = RemoteClient(
self._server_base_url,
hf_token=None,
write_token=self._write_token,
verbose=False,
)
else:
client = RemoteClient(
self.url,
hf_token=huggingface_hub.utils.get_token(),
verbose=False,
)
with self._client_lock:
self._client = client
break
except Exception:
pass
try:
self._spill_queues_while_waiting()
except Exception:
pass
sleep_time = min(0.1 * sleep_coefficient, MAX_BACKOFF)
self._stop_flag.wait(timeout=sleep_time)
self._batch_sender()
def _queue_upload(
self,
file_path,
step: int | None,
relative_path: str | None = None,
use_run_name: bool = True,
):
try:
if self._is_local:
self._save_upload_locally(file_path, step, relative_path, use_run_name)
else:
upload_entry: UploadEntry = {
"project": self.project,
"run": self.name if use_run_name else None,
"run_id": self.id if use_run_name else None,
"step": step,
"relative_path": relative_path,
"uploaded_file": handle_file(file_path),
}
with self._client_lock:
self._queued_uploads.append(upload_entry)
self._ensure_sender_alive()
if not self._thread_is_alive("_client_thread"):
self._flush_queues_inline()
except Exception as e:
self._warn_once(
"queue-upload",
f"trackio could not queue the artifact '{file_path}' for run '{self.name}': {e}. User code will continue, but this artifact could be missing.",
)
def _save_upload_locally(
self,
file_path,
step: int | None,
relative_path: str | None = None,
use_run_name: bool = True,
):
media_path = get_project_media_path(
project=self.project,
run=self.name if use_run_name else None,
step=step,
relative_path=relative_path,
)
src = Path(file_path)
if src.exists() and str(src.resolve()) != str(Path(media_path).resolve()):
shutil.copy(str(src), str(media_path))
def _process_media(self, value: TrackioMedia, step: int | None) -> dict:
value._save(self.project, self.name, step if step is not None else 0)
if self._space_id or self._server_base_url:
self._queue_upload(value._get_absolute_file_path(), step)
return value._to_dict()
def _scan_and_queue_media_uploads(self, value: Any, step: int | None):
if not self._space_id and not self._server_base_url:
return
if isinstance(value, dict):
if value.get("_type") in [
"trackio.image",
"trackio.video",
"trackio.audio",
]:
file_path = value.get("file_path")
if file_path:
absolute_path = MEDIA_DIR / file_path
self._queue_upload(absolute_path, step)
return
for nested in value.values():
self._scan_and_queue_media_uploads(nested, step)
return
if isinstance(value, list):
for nested in value:
self._scan_and_queue_media_uploads(nested, step)
def _ensure_sender_alive(self):
if self._is_local:
if (
not self._thread_is_alive("_local_sender_thread")
and not self._stop_flag.is_set()
):
self._start_background_thread(
"_local_sender_thread",
self._local_batch_sender,
warning_key="local-sender-thread-restart",
description="local Trackio logging thread",
)
else:
if (
not self._thread_is_alive("_client_thread")
and not self._stop_flag.is_set()
):
self._start_background_thread(
"_client_thread",
self._init_client_background,
warning_key="remote-sender-thread-restart",
description="remote Trackio logging thread",
)
def log(self, metrics: dict, step: int | None = None):
try:
renamed_keys = []
new_metrics = {}
for k, v in metrics.items():
if k in utils.RESERVED_KEYS or k.startswith("__"):
new_key = f"__{k}"
renamed_keys.append(k)
new_metrics[new_key] = v
else:
new_metrics[k] = v
if renamed_keys:
_emit_nonfatal_warning(
f"Reserved keys renamed: {renamed_keys} → '__{{key}}'"
)
metrics = new_metrics
media_step = step if step is not None else self._next_step
for key, value in metrics.items():
if isinstance(value, Table):
metrics[key] = value._to_dict(
project=self.project, run=self.name, step=media_step
)
self._scan_and_queue_media_uploads(metrics[key], media_step)
elif isinstance(value, Trace):
metrics[key] = value._to_dict(
project=self.project, run=self.name, step=media_step
)
self._scan_and_queue_media_uploads(metrics[key], media_step)
elif (
isinstance(value, list)
and value
and all(isinstance(item, Trace) for item in value)
):
converted = [
item._to_dict(
project=self.project, run=self.name, step=media_step
)
for item in value
]
metrics[key] = converted
for item in converted:
self._scan_and_queue_media_uploads(item, media_step)
elif isinstance(value, Histogram):
metrics[key] = value._to_dict()
elif isinstance(value, Markdown):
metrics[key] = value._to_dict()
elif isinstance(value, TrackioMedia):
metrics[key] = self._process_media(value, media_step)
metrics = utils.serialize_values(metrics)
if step is None:
step = self._next_step
self._next_step = max(self._next_step, step + 1)
config_to_log = None
if not self._config_logged and self.config:
config_to_log = utils.to_json_safe(self.config)
self._config_logged = True
log_entry: LogEntry = {
"project": self.project,
"run": self.name,
"run_id": self.id,
"metrics": metrics,
"step": step,
"config": config_to_log,
"log_id": uuid.uuid4().hex,
}
with self._client_lock:
self._queued_logs.append(log_entry)
self._ensure_sender_alive()
if not self._thread_is_alive(
"_local_sender_thread" if self._is_local else "_client_thread"
):
self._flush_queues_inline()
except Exception as e:
_emit_nonfatal_warning(f"trackio.log() failed to process metrics: {e}")
def _artifact_log_with_retry(self, **kwargs) -> dict:
manifest = kwargs.get("manifest", [])
ref_entries = [
entry for entry in manifest if references.is_reference_entry(entry)
]
upgrade_hint = (
"The server likely predates artifact references. To support "
"add_reference, you need to upgrade trackio to a more recent "
"release."
)
attempts = len(ARTIFACT_LOG_RETRY_BACKOFFS) + 1
for attempt in range(attempts):
if attempt > 0:
time.sleep(ARTIFACT_LOG_RETRY_BACKOFFS[attempt - 1])
try:
with self._client_lock:
record = self._client.predict(api_name="/artifact_log", **kwargs)
except Exception as e:
if attempt == attempts - 1 or not is_transient_remote_error(e):
message = str(e)
pre_reference_rejection = ref_entries and (
"Invalid sha256 digest" in message
or (
"blobs not on server" in message
and any(entry["digest"] in message for entry in ref_entries)
)
)
if pre_reference_rejection:
raise RuntimeError(
"The remote trackio server rejected this artifact's "
f"reference entries ({message}). {upgrade_hint} "
"Alternatively, log this artifact without "
"add_reference entries."
) from e
raise
else:
stored = record.get("manifest")
stored_ref_paths = {
entry["path"]
for entry in (stored if isinstance(stored, list) else [])
if isinstance(entry, dict) and references.is_reference_entry(entry)
}
dropped = sorted(
{entry["path"] for entry in ref_entries} - stored_ref_paths
)
if dropped:
preview = dropped[:5]
suffix = "..." if len(dropped) > 5 else ""
raise RuntimeError(
"The remote trackio server stored this artifact's "
f"reference entries as plain file entries "
f"({preview}{suffix}), dropping their reference URIs. "
f"{upgrade_hint} Then re-log this artifact."
)
return record
def log_artifact(
self,
artifact_or_path: Artifact | str | Path,
name: str | None = None,
type: str | None = None,
aliases: list[str] | None = None,
) -> Artifact:
if isinstance(artifact_or_path, Artifact):
if name is not None or type is not None:
raise ValueError(
"name/type can only be passed when logging a path; "
"set them on the Artifact instead."
)
artifact = artifact_or_path
else:
path = Path(artifact_or_path)
artifact = Artifact(
name=name or path.name,
type=type or "unspecified",
)
if path.is_dir():
artifact.add_dir(path)
else:
artifact.add_file(path)
if artifact._logged:
raise RuntimeError(
"Artifact has already been logged or fetched; "
"construct a new Artifact() to log again."
)
user_aliases = cas.validate_aliases(aliases)
manifest = artifact._build_manifest(self.project)
if self._is_local:
record = SQLiteStorage.commit_artifact_version(
project=self.project,
name=artifact.name,
type=artifact.type,
description=artifact.description,
manifest=manifest,
metadata=artifact.metadata,
aliases=user_aliases,
run_name=self.name,
run_id=self.id,
)
else:
self._wait_for_client_ready()
file_entries = [e for e in manifest if not references.is_reference_entry(e)]
digests = [e["digest"] for e in file_entries]
with self._client_lock:
present_response = self._client.predict(
api_name="/check_artifact_blobs",
project=self.project,
digests=digests,
hf_token=self._hf_token_for_remote(),
)
present = set((present_response or {}).get("present", []))
SQLiteStorage.enqueue_artifact_blob_uploads(
project=self.project,
space_id=self._remote_storage_key,
blobs=[
(
entry["digest"],
str(cas.blob_path(self.project, entry["digest"])),
)
for entry in file_entries
if entry["digest"] not in present
],
run_name=self.name,
run_id=self.id,
)
self._drain_pending_uploads()
record = self._artifact_log_with_retry(
project=self.project,
name=artifact.name,
type=artifact.type,
description=artifact.description,
metadata=artifact.metadata,
manifest=manifest,
aliases=user_aliases,
run_name=self.name,
run_id=self.id,
hf_token=self._hf_token_for_remote(),
)
artifact._hydrate_from_db(
project=self.project,
version=record["version"],
aliases=record["aliases"],
manifest=record["manifest"],
manifest_digest=record["manifest_digest"],
size_bytes=record["size_bytes"],
)
if not self._is_local:
artifact._remote_source = self._remote_source_dict()
try:
from trackio import logbook as _logbook
_logbook.auto_note_artifact(
self.project,
artifact.qualified_name,
artifact.size,
artifact_type=artifact.type,
)
except Exception:
pass
return artifact
@staticmethod
def _check_artifact_type(
spec: str, stored_type: str, expected_type: str | None
) -> None:
if expected_type is not None and stored_type != expected_type:
raise ValueError(
f"Artifact {spec!r} has type {stored_type!r}, not {expected_type!r}."
)
def use_artifact(
self,
artifact_or_name: Artifact | str,
type: str | None = None,
) -> Artifact:
"""Resolve an artifact and record this run as a consumer of it."""
if isinstance(artifact_or_name, Artifact):
if not artifact_or_name._logged or artifact_or_name._version is None:
raise ValueError(
"use_artifact() with an Artifact instance requires an "
"artifact that has already been logged or fetched."
)
spec = f"{artifact_or_name.name}:v{artifact_or_name._version}"
project = artifact_or_name._project or self.project
else:
spec = artifact_or_name
project = self.project
if ":" in spec:
name, version_or_alias = spec.split(":", 1)
if not version_or_alias:
raise ValueError(
f"Artifact spec {spec!r} has an empty version/alias after ':'. "
"Use 'name:vN' or 'name:alias', or drop the colon to get latest."
)
else:
name, version_or_alias = spec, None
if self._is_local:
record = SQLiteStorage.get_artifact_manifest(
project, name, version_or_alias
)
else:
self._wait_for_client_ready()
with self._client_lock:
record = self._client.predict(
api_name="/get_artifact_manifest",
project=project,
name=name,
spec=version_or_alias,
)
if record is None:
raise ValueError(f"Artifact {spec!r} not found in project {project!r}.")
self._check_artifact_type(spec, record["type"], type)
art = Artifact(name=record["name"], type=record["type"])
art._hydrate_from_db(
project=project,
version=record["version"],
aliases=record["aliases"],
manifest=record["manifest"],
manifest_digest=record["manifest_digest"],
size_bytes=record["size_bytes"],
description=record["description"],
metadata=record["metadata"],
)
if not self._is_local:
art._remote_source = self._remote_source_dict()
try:
if self._is_local:
SQLiteStorage.insert_run_artifact_link(
project=project,
run_name=self.name,
run_id=self.id,
version_id=record["version_id"],
direction="input",
)
else:
with self._client_lock:
self._client.predict(
api_name="/log_artifact_use",
project=project,
version_id=record["version_id"],
run_name=self.name,
run_id=self.id,
hf_token=self._hf_token_for_remote(),
)
except Exception as e:
self._warn_once(
"artifact-use-lineage",
f"trackio could not record consumer lineage for {spec!r}: {e}",
)
return art
def alert(
self,
title: str,
text: str | None = None,
level: AlertLevel = AlertLevel.WARN,
step: int | None = None,
webhook_url: str | None = None,
):
try:
if step is None:
step = max(self._next_step - 1, 0)
timestamp = datetime.now(timezone.utc).isoformat()
print(format_alert_terminal(level, title, text, step))
alert_entry: AlertEntry = {
"project": self.project,
"run": self.name,
"run_id": self.id,
"title": title,
"text": text,
"level": level.value,
"step": step,
"timestamp": timestamp,
"alert_id": uuid.uuid4().hex,
}
with self._client_lock:
self._queued_alerts.append(alert_entry)
self._ensure_sender_alive()
if not self._thread_is_alive(
"_local_sender_thread" if self._is_local else "_client_thread"
):
self._flush_queues_inline()
url = webhook_url or self._webhook_url
if url and should_send_webhook(level, self._webhook_min_level):
t = threading.Thread(
target=send_webhook,
args=(
url,
level,
title,
text,
self.project,
self.name,
step,
timestamp,
),
daemon=True,
)
t.start()
except Exception as e:
_emit_nonfatal_warning(f"trackio.alert() failed: {e}")
def log_system(self, metrics: dict):
try:
metrics = utils.serialize_values(metrics)
timestamp = datetime.now(timezone.utc).isoformat()
system_log_entry: SystemLogEntry = {
"project": self.project,
"run": self.name,
"run_id": self.id,
"metrics": metrics,
"timestamp": timestamp,
"log_id": uuid.uuid4().hex,
}
with self._client_lock:
self._queued_system_logs.append(system_log_entry)
self._ensure_sender_alive()
if not self._thread_is_alive(
"_local_sender_thread" if self._is_local else "_client_thread"
):
self._flush_queues_inline()
except Exception as e:
_emit_nonfatal_warning(f"trackio.log_system() failed: {e}")
def finish(self):
try:
if self._gpu_monitor is not None:
try:
self._gpu_monitor.stop()
except Exception as e:
self._warn_once(
"finish-gpu-monitor",
f"trackio.finish() could not stop automatic GPU logging cleanly: {e}.",
)
if self._cpu_monitor is not None:
try:
self._cpu_monitor.stop()
except Exception as e:
self._warn_once(
"finish-cpu-monitor",
f"trackio.finish() could not stop automatic CPU logging cleanly: {e}.",
)
self._stop_flag.set()
if self._is_local:
if self._local_sender_thread is not None:
print("* Run finished. Uploading logs to Trackio (please wait...)")
self._local_sender_thread.join(timeout=30)
if self._local_sender_thread.is_alive():
_emit_nonfatal_warning(
"Could not flush all logs within 30s. Some data may be buffered locally."
)
else:
with self._client_lock:
self._flush_queues_inline()
else:
with self._client_lock:
client_connected = self._client is not None
if self._client_thread is not None:
if client_connected:
print(
"* Run finished. Uploading logs to the remote Trackio server (please wait...)"
)
self._client_thread.join(timeout=30)
else:
self._client_thread.join(timeout=5)
if self._client_thread.is_alive():
with self._client_lock:
if self._client is None:
self._flush_queues_inline()
if client_connected or self._bucket_id is None:
_emit_nonfatal_warning(
"Could not flush all logs to the remote server in time. Some data may be buffered locally."
)
else:
with self._client_lock:
self._flush_queues_inline()
try:
has_pending = SQLiteStorage.has_pending_data(self.project)
except Exception as e:
self._warn_once(
"finish-pending-data",
f"trackio.finish() could not inspect pending buffered logs for project '{self.project}': {e}.",
)
has_pending = False
if has_pending and self._bucket_id is not None:
self._has_local_buffer = True
self._drain_pending_to_bucket()
has_pending = self._has_local_buffer
if not has_pending:
print(
f"* Some logs could not be sent to the Space directly: they were uploaded to "
f"the Hugging Face Bucket '{self._bucket_id}' instead and will appear on the "
f"dashboard once the Space is running."
)
if has_pending:
if self._space_id is not None:
retry = f'trackio.init(project="{self.project}", space_id="{self._space_id}")'
else:
retry = f'trackio.init(project="{self.project}", server_url="{self._server_base_url}")'
_emit_nonfatal_warning(
f"* Some logs could not be sent to the remote server (it may still be starting up). "
f"They have been saved locally and will be sent automatically next time you call: "
f"{retry}"
)
except Exception as e:
_emit_nonfatal_warning(f"trackio.finish() failed: {e}")