test-images / trackio /fragments.py
saba9's picture
saba9 HF Staff
Upload folder using huggingface_hub
be2e65f verified
Raw History Blame Contribute Delete
11.8 kB
"""Append-only JSONL fragments used as a durable fallback for metric logging.
Fragments are immutable JSONL files, one writer (process) per subdirectory, so
concurrent training processes never contend on a shared file. They are written
either to a Hugging Face Bucket inbox (when a Space is unreachable) or to a
local inbox directory (when SQLite is unsafe, e.g. on network filesystems), and
are later imported into the project SQLite database by the process that owns it
(the Space or the dashboard server). Records carry the same ``log_id``/
``alert_id`` UUIDs as the HTTP logging endpoints, and imports use
``INSERT OR IGNORE``, so importing a fragment is idempotent: fragments are
deleted only after a successful import, and re-importing after a crash is
harmless.
"""
import tempfile
import threading
import uuid
from pathlib import Path
from typing import Any
import huggingface_hub
import orjson
from trackio import utils
from trackio.typehints import AlertEntry, LogEntry, SystemLogEntry
FRAGMENT_VERSION = 1
INBOX_DIR_NAME = "inbox"
BUCKET_INBOX_PREFIX = "trackio/inbox"
BUCKET_MEDIA_PREFIX = "trackio/media"
METRIC_KIND = "metric"
SYSTEM_METRIC_KIND = "system_metric"
ALERT_KIND = "alert"
KINDS = {METRIC_KIND, SYSTEM_METRIC_KIND, ALERT_KIND}
def local_inbox_dir() -> Path:
return utils.TRACKIO_DIR / INBOX_DIR_NAME
def metric_record(entry: LogEntry | dict) -> dict:
return {
"v": FRAGMENT_VERSION,
"kind": METRIC_KIND,
"project": entry["project"],
"run": entry["run"],
"run_id": entry.get("run_id"),
"metrics": utils.serialize_values(entry.get("metrics") or {}),
"step": entry.get("step"),
"timestamp": entry.get("timestamp"),
"config": utils.serialize_values(entry.get("config"))
if entry.get("config")
else None,
"log_id": entry.get("log_id"),
}
def system_metric_record(entry: SystemLogEntry | dict) -> dict:
return {
"v": FRAGMENT_VERSION,
"kind": SYSTEM_METRIC_KIND,
"project": entry["project"],
"run": entry["run"],
"run_id": entry.get("run_id"),
"metrics": utils.serialize_values(entry.get("metrics") or {}),
"timestamp": entry.get("timestamp"),
"log_id": entry.get("log_id"),
}
def alert_record(entry: AlertEntry | dict) -> dict:
return {
"v": FRAGMENT_VERSION,
"kind": ALERT_KIND,
"project": entry["project"],
"run": entry["run"],
"run_id": entry.get("run_id"),
"title": entry["title"],
"text": entry.get("text"),
"level": entry.get("level"),
"step": entry.get("step"),
"timestamp": entry.get("timestamp"),
"alert_id": entry.get("alert_id"),
}
def bucket_media_path(
project: str,
run: str | None,
step: int | None,
relative_path: str | None,
filename: str,
) -> str:
parts = [BUCKET_MEDIA_PREFIX, utils.canonical_project_name(project)]
if run:
parts.append(run)
if step is not None:
parts.append(str(step))
else:
parts.append("files")
if relative_path:
parts.append(str(relative_path))
parts.append(filename)
return "/".join(parts)
class FragmentWriter:
"""Writes immutable JSONL fragments for a single writer (process)."""
def __init__(self, writer_id: str | None = None):
self.writer_id = writer_id or uuid.uuid4().hex[:16]
self._seq = 0
self._lock = threading.Lock()
def _next_fragment_name(self) -> str:
with self._lock:
name = f"{self._seq:08d}.jsonl"
self._seq += 1
return name
@staticmethod
def serialize_records(records: list[dict]) -> bytes:
return b"".join(orjson.dumps(record) + b"\n" for record in records)
def write_local(
self, records: list[dict], inbox_dir: Path | None = None
) -> Path | None:
if not records:
return None
inbox = inbox_dir or local_inbox_dir()
writer_dir = inbox / self.writer_id
fragment_path = writer_dir / self._next_fragment_name()
data = self.serialize_records(records)
for attempt in range(3):
writer_dir.mkdir(parents=True, exist_ok=True)
try:
with tempfile.NamedTemporaryFile(
mode="wb", dir=writer_dir, suffix=".tmp", delete=False
) as tmp:
tmp.write(data)
tmp_path = Path(tmp.name)
tmp_path.replace(fragment_path)
return fragment_path
except FileNotFoundError:
if attempt == 2 or writer_dir.exists():
raise
def write_to_bucket(self, records: list[dict], bucket_id: str) -> str | None:
if not records:
return None
remote_path = (
f"{BUCKET_INBOX_PREFIX}/{self.writer_id}/{self._next_fragment_name()}"
)
huggingface_hub.batch_bucket_files(
bucket_id,
add=[(self.serialize_records(records), remote_path)],
token=huggingface_hub.utils.get_token(),
)
return remote_path
def parse_fragment_bytes(data: bytes) -> list[dict]:
records = []
for line in data.splitlines():
line = line.strip()
if not line:
continue
try:
record = orjson.loads(line)
except orjson.JSONDecodeError:
continue
if isinstance(record, dict) and record.get("kind") in KINDS:
records.append(record)
return records
def _group_by_run(records: list[dict]) -> dict[tuple, list[dict]]:
grouped: dict[tuple, list[dict]] = {}
for record in records:
key = (record.get("project"), record.get("run"), record.get("run_id"))
grouped.setdefault(key, []).append(record)
return grouped
def import_records(records: list[dict]) -> int:
from trackio.sqlite_storage import SQLiteStorage # noqa: PLC0415
metric_records = [r for r in records if r.get("kind") == METRIC_KIND]
system_records = [r for r in records if r.get("kind") == SYSTEM_METRIC_KIND]
alert_records = [r for r in records if r.get("kind") == ALERT_KIND]
imported = 0
for (project, run, run_id), group in _group_by_run(metric_records).items():
if not project or not run:
continue
config = next((r["config"] for r in group if r.get("config")), None)
has_timestamps = all(r.get("timestamp") for r in group)
SQLiteStorage.bulk_log(
project=project,
run=run,
run_id=run_id,
metrics_list=[r.get("metrics") or {} for r in group],
steps=[r.get("step") for r in group],
timestamps=[r["timestamp"] for r in group] if has_timestamps else None,
config=config,
log_ids=[r.get("log_id") for r in group],
)
imported += len(group)
for (project, run, run_id), group in _group_by_run(system_records).items():
if not project or not run:
continue
has_timestamps = all(r.get("timestamp") for r in group)
SQLiteStorage.bulk_log_system(
project=project,
run=run,
run_id=run_id,
metrics_list=[r.get("metrics") or {} for r in group],
timestamps=[r["timestamp"] for r in group] if has_timestamps else None,
log_ids=[r.get("log_id") for r in group],
)
imported += len(group)
for (project, run, run_id), group in _group_by_run(alert_records).items():
if not project or not run:
continue
has_timestamps = all(r.get("timestamp") for r in group)
SQLiteStorage.bulk_alert(
project=project,
run=run,
run_id=run_id,
titles=[r.get("title") or "" for r in group],
texts=[r.get("text") for r in group],
levels=[r.get("level") or "WARN" for r in group],
steps=[r.get("step") for r in group],
timestamps=[r["timestamp"] for r in group] if has_timestamps else None,
alert_ids=[r.get("alert_id") for r in group],
)
imported += len(group)
return imported
def import_inbox_dir(inbox_dir: Path | None = None) -> int:
inbox = inbox_dir or local_inbox_dir()
if not inbox.exists():
return 0
imported = 0
for fragment_path in sorted(inbox.rglob("*.jsonl")):
try:
data = fragment_path.read_bytes()
except OSError:
continue
records = parse_fragment_bytes(data)
if records:
imported += import_records(records)
try:
fragment_path.unlink()
except OSError:
pass
for writer_dir in inbox.glob("*"):
if writer_dir.is_dir():
try:
writer_dir.rmdir()
except OSError:
pass
return imported
def list_bucket_inbox_paths(bucket_id: str) -> list[str]:
try:
items = huggingface_hub.list_bucket_tree(
bucket_id,
prefix=BUCKET_INBOX_PREFIX,
recursive=True,
token=huggingface_hub.utils.get_token(),
)
except Exception:
return []
return sorted(
item.path
for item in items
if getattr(item, "type", None) == "file"
and getattr(item, "path", "").endswith(".jsonl")
)
def import_inbox_from_bucket(bucket_id: str) -> int:
paths = list_bucket_inbox_paths(bucket_id)
if not paths:
return 0
imported = 0
consumed: list[str] = []
with tempfile.TemporaryDirectory() as tmp_dir:
for i, remote_path in enumerate(paths):
local_path = Path(tmp_dir) / f"{i}.jsonl"
try:
huggingface_hub.download_bucket_files(
bucket_id,
files=[(remote_path, str(local_path))],
token=huggingface_hub.utils.get_token(),
)
records = parse_fragment_bytes(local_path.read_bytes())
except Exception:
continue
if records:
imported += import_records(records)
consumed.append(remote_path)
if consumed:
try:
huggingface_hub.batch_bucket_files(
bucket_id,
delete=consumed,
token=huggingface_hub.utils.get_token(),
)
except Exception:
pass
return imported
def _add_files_to_bucket(bucket_id: str, additions: list[tuple[str, str]]) -> None:
if additions:
huggingface_hub.batch_bucket_files(
bucket_id,
add=additions,
token=huggingface_hub.utils.get_token(),
)
def upload_media_files_to_bucket(bucket_id: str, uploads: list[dict[str, Any]]) -> None:
_add_files_to_bucket(
bucket_id,
[
(
str(p),
bucket_media_path(
project=upload["project"],
run=upload.get("run"),
step=upload.get("step"),
relative_path=upload.get("relative_path"),
filename=p.name,
),
)
for upload in uploads
if (p := Path(upload["file_path"])).exists()
],
)
def upload_artifact_blobs_to_bucket(
bucket_id: str, uploads: list[dict[str, Any]]
) -> None:
_add_files_to_bucket(
bucket_id,
[
(str(p), f"trackio/{p.relative_to(utils.TRACKIO_DIR).as_posix()}")
for upload in uploads
if (p := Path(upload["file_path"])).exists()
],
)