Buckets:
| # Artifacts | |
| ## Introduction | |
| An **artifact** is a versioned, named bundle of files attached to a project — a trained model, a dataset, a set of evaluation outputs. Logging files as artifacts (rather than leaving them on disk) gives you: | |
| - **Versioning** — each time you log under the same name, Trackio records a new version (`v0`, `v1`, ...). Identical content is de-duplicated, so re-logging unchanged files reuses the existing version. | |
| - **Aliases** — moving pointers such as `latest` (assigned automatically) or your own (`prod`, `best`) that always resolve to a single version. | |
| - **Lineage** — artifacts are linked to the runs that produced and consumed them, so you can trace which run created a model and which runs used it. | |
| Artifacts work fully offline against local storage, and sync to a Hugging Face Space (or self-hosted server) when one is configured, exactly like metrics. | |
| ## Logging an artifact | |
| The quickest way is to log a file or directory directly. This creates a new artifact, adds the path, and records it as an output of the current run: | |
| ```python | |
| import trackio | |
| trackio.init(project="my_project") | |
| # ... training ... | |
| trackio.log_artifact("checkpoints/model.safetensors", type="model") | |
| ``` | |
| When you pass a path, the artifact `name` defaults to `run-<run_id>-<basename>` and `type` defaults to `"unspecified"`. Pass `name` and `type` to set them explicitly: | |
| ```python | |
| trackio.log_artifact("checkpoints/", name="my-model", type="model") | |
| ``` | |
| ### Artifact types | |
| Artifact `type` is a free-form category string. Trackio does not enforce a fixed list of types, but common conventions are: | |
| | Type | Use it for | | |
| | --- | --- | | |
| | `"model"` | Model checkpoints, adapter weights, tokenizers, model configs | | |
| | `"dataset"` | Training, validation, test, or generated datasets | | |
| | `"evaluation"` | Evaluation outputs, predictions, benchmark results, score files | | |
| | `"report"` | Figures, plots, tables, notebooks, or human-readable analysis bundles | | |
| | `"unspecified"` | The default when no type is provided | | |
| Use stable type names within a project so you can filter artifacts consistently and use `trackio.use_artifact(..., type="...")` to assert that a run is consuming the expected kind of artifact. | |
| ### Building an artifact explicitly | |
| For finer control — multiple files, custom logical paths, a description, or metadata — construct an [Artifact](/docs/trackio/pr_658/en/api#trackio.Artifact), add files to it, then log it: | |
| ```python | |
| artifact = trackio.Artifact( | |
| name="my-model", | |
| type="model", | |
| description="Fine-tuned on the v2 dataset", | |
| metadata={"base_model": "bert-base", "epochs": 3}, | |
| ) | |
| artifact.add_file("checkpoints/model.safetensors") | |
| artifact.add_file("checkpoints/config.json") | |
| artifact.add_dir("tokenizer/") | |
| logged = trackio.log_artifact(artifact) | |
| ``` | |
| After logging, the artifact is frozen and its `version`, `aliases`, `size`, and `manifest` are populated: | |
| ```python | |
| print(logged.version) # "v0" | |
| print(logged.aliases) # ("latest",) | |
| ``` | |
| ### Versions and aliases | |
| Each log under an existing name creates the next integer version and moves the `latest` alias onto it. Assign your own aliases with `aliases=`: | |
| ```python | |
| trackio.log_artifact(artifact, aliases=["prod", "best"]) | |
| ``` | |
| Aliases are moving pointers: logging a new version with the same alias re-points it. Version specifiers (`v0`, `v1`, ...) are reserved and cannot be used as aliases. | |
| If you log content identical to an existing version, Trackio reuses that version instead of creating a new one. Any explicit `aliases` you pass still rotate onto it, but `latest` is left where it is — re-logging identical (or older) content never moves it backward. | |
| ## Using an artifact | |
| [use_artifact()](/docs/trackio/pr_658/en/api#trackio.use_artifact) fetches an artifact and records it as an **input** to the current run, which is what builds lineage between a consuming run and the artifact it used: | |
| ```python | |
| trackio.init(project="my_project") | |
| artifact = trackio.use_artifact("my-model") # resolves to :latest | |
| artifact = trackio.use_artifact("my-model:v2") # pin a version | |
| artifact = trackio.use_artifact("my-model:prod") # resolve an alias | |
| ``` | |
| Pass `type` to assert the artifact's type, raising if it doesn't match: | |
| ```python | |
| artifact = trackio.use_artifact("my-model", type="model") | |
| ``` | |
| ### Downloading files | |
| `use_artifact` returns an [Artifact](/docs/trackio/pr_658/en/api#trackio.Artifact) whose files you materialize with `download()`: | |
| ```python | |
| artifact = trackio.use_artifact("my-model:latest") | |
| path = artifact.download() | |
| # files are now under ./.trackio/artifact-downloads/my_project/my-model_v2/ | |
| ``` | |
| By default, files are written to `./.trackio/artifact-downloads/<project>/<name>_v<version>/`, keyed by project so same-named artifacts from different projects never collide; pass `root` to choose another directory. `download()` is idempotent — files already present are skipped — and when the run is backed by a Space, any file missing locally is fetched from the remote. | |
| ## Referencing external data | |
| Some data is too large to copy, or already lives in durable storage you don't want duplicated. `add_reference` records such data by **URI** in the manifest *without staging any bytes* into Trackio's storage, so you still get versioning, de-duplication, aliases, and lineage over data that never moves: | |
| ```python | |
| artifact = trackio.Artifact(name="training-set", type="dataset") | |
| artifact.add_reference("file:///mnt/nvme/datasets/corpus.parquet") | |
| artifact.add_reference("file:///mnt/nvme/datasets/shards/") # a directory — expands to one entry per file | |
| artifact.add_reference("https://example.com/data/eval.json") | |
| artifact.add_reference("hf://datasets/username/my-dataset/train.parquet") | |
| trackio.log_artifact(artifact) | |
| ``` | |
| Built-in schemes are `file://`, `http(s)://`, and `hf://` — all served by Trackio's existing dependencies. Any other scheme (`s3://`, `gs://`, Azure blob URLs, ...) is supported by registering a custom handler; see [Referencing other stores](#referencing-other-stores-s3-gcs-azure) below for complete S3, GCS, and Azure examples. With `checksum=True` (the default), Trackio probes the source to fill in `size` and a checksum: | |
| - `file://` is stream-hashed to a sha256; its size is read from disk. | |
| - `hf://` uses the LFS sha256 (or the git blob id); `http(s)://` uses the server `ETag`. | |
| Every checksum is stored in a single `digest` field — the sha256 for local/LFS references, or the provider's ETag for HTTP or object-store references. | |
| A URI that denotes a directory (`file://`) or a repo path (`hf://`) is **expanded** into one reference entry per object, each named by its path relative to the prefix (nested under `name=` when given). `max_objects` (default 10,000) caps the expansion and raises if exceeded. | |
| When no checksum can be obtained — for example a server with no `ETag` — the reference is still recorded, using **the URI itself as the `digest`**, and a warning is emitted. A local `file://` path that doesn't exist is the exception: it raises rather than being recorded, since a missing local file is almost always a mistake — use `add_file` to stage a local file. Pass `checksum=False` to skip the per-object checksum — a single object then records just its URI — and `name=` to set the logical path. | |
| A reference's version identity comes from its URI, size, and `digest`, so re-logging identical references de-duplicates to one version, while a changed URI, size, or checksum creates a new one — exactly like file content. When the URI is used as the `digest`, identity therefore tracks the URI and not the content. | |
| URIs are recorded verbatim, and manifests can be synced to a Hugging Face Dataset. A URI that embeds a credential — a presigned `https://` URL or an Azure SAS token — would expose that credential to anyone who can read the Dataset, so `add_reference` warns when a URI's query string looks signed; prefer the object's canonical unsigned URI (e.g. `s3://bucket/key`). | |
| Reference bytes are never uploaded to a Space or bucket, but they can be fetched on demand. `download()` downloads each referenced object into the directory (via `httpx`, `huggingface_hub`, or your registered handler), which can transfer a large amount of data. Before fetching, each object's source is re-probed and its current checksum compared against the recorded one — a mismatch warns that the source may have changed but the download proceeds. Each object is written atomically, and one already present on disk is skipped without re-checking the source, so repeated calls are cheap and idempotent: | |
| ```python | |
| artifact = trackio.use_artifact("training-set:latest") | |
| path = artifact.download() # downloads staged files AND referenced objects | |
| ``` | |
| To work with the pointers *without* downloading, read them from the artifact instead: | |
| ```python | |
| print(artifact.references) # [{'path': ..., 'ref': 's3://...', 'size': ..., 'digest': ...}, ...] | |
| print(artifact.get_entry_uri("eval.json")) # "s3://my-bucket/eval.json" | |
| ``` | |
| An unfetchable reference (a missing local file, an unreachable URL, or a scheme with no registered handler) raises when `download()` reaches it. | |
| ## Referencing other stores (S3, GCS, Azure) | |
| Trackio deliberately ships no cloud-vendor SDKs. To reference objects in a store it doesn't handle natively, subclass `trackio.ReferenceHandler` and register an instance with `trackio.register_reference_handler`. A handler implements four methods: | |
| - `matches(scheme, uri)` — whether this handler owns the URI. Registered handlers are consulted *before* the built-ins, so a handler may also claim specific `https://` hosts (as the Azure example below does) ahead of the generic HTTP handler. | |
| - `resolve(uri, checksum, max_objects)` — probe the source and return a list of `trackio.ResolvedReference` entries: one entry with `relkey=None` for a single object, or one entry per object (with `relkey` set to the object's path relative to the prefix) when the URI denotes a prefix. Set `size` and `digest` when the source can be probed (the provider's ETag works fine as a digest — Trackio treats reference digests as opaque tokens); return them as `None` to record the reference un-checksummed, in which case Trackio falls back to the URI as the digest and warns. | |
| - `fetch(uri, dest)` — download the single object at `uri` to the local path `dest` (called by `Artifact.download()`). | |
| - `hint()` — a one-line actionable message shown in the warning when `resolve` produced no checksum. | |
| Keys discovered during prefix expansion should be percent-encoded when synthesizing each entry's `uri` (and decoded in `fetch`), since manifest URIs may not contain spaces. | |
| ### S3 (`s3://`, via `boto3`) | |
| ```python | |
| from urllib.parse import quote, unquote, urlsplit | |
| import boto3 | |
| import trackio | |
| class S3Handler(trackio.ReferenceHandler): | |
| def matches(self, scheme, uri): | |
| return scheme == "s3" | |
| def resolve(self, uri, checksum, max_objects): | |
| parts = urlsplit(uri) | |
| bucket, key = parts.netloc, unquote(parts.path.lstrip("/")) | |
| client = boto3.client("s3") | |
| if key and not key.endswith("/"): | |
| head = client.head_object(Bucket=bucket, Key=key) | |
| return [ | |
| trackio.ResolvedReference( | |
| relkey=None, | |
| uri=uri, | |
| size=int(head["ContentLength"]), | |
| digest=head["ETag"].strip('"') if checksum else None, | |
| ) | |
| ] | |
| base = key[: key.rfind("/") + 1] | |
| entries = [] | |
| pages = client.get_paginator("list_objects_v2").paginate( | |
| Bucket=bucket, Prefix=key | |
| ) | |
| for page in pages: | |
| for obj in page.get("Contents", []): | |
| if obj["Key"].endswith("/"): | |
| continue | |
| if len(entries) >= max_objects: | |
| raise ValueError( | |
| f"{uri} expands to more than {max_objects} objects; " | |
| "pass max_objects= to raise the limit." | |
| ) | |
| entries.append( | |
| trackio.ResolvedReference( | |
| relkey=obj["Key"][len(base):], | |
| uri=f"s3://{bucket}/{quote(obj['Key'], safe='/')}", | |
| size=int(obj["Size"]), | |
| digest=obj["ETag"].strip('"') if checksum else None, | |
| ) | |
| ) | |
| return entries | |
| def fetch(self, uri, dest): | |
| parts = urlsplit(uri) | |
| with open(dest, "wb") as f: | |
| boto3.client("s3").download_fileobj( | |
| parts.netloc, unquote(parts.path.lstrip("/")), f | |
| ) | |
| def hint(self): | |
| return "Configure AWS credentials so the object can be probed for an ETag." | |
| trackio.register_reference_handler(S3Handler()) | |
| artifact = trackio.Artifact(name="training-set", type="dataset") | |
| artifact.add_reference("s3://my-bucket/shards/") # expands to one entry per object | |
| trackio.log_artifact(artifact) | |
| ``` | |
| ### Google Cloud Storage (`gs://`, via `google-cloud-storage`) | |
| ```python | |
| from urllib.parse import quote, unquote, urlsplit | |
| from google.cloud import storage | |
| import trackio | |
| class GcsHandler(trackio.ReferenceHandler): | |
| def matches(self, scheme, uri): | |
| return scheme == "gs" | |
| def resolve(self, uri, checksum, max_objects): | |
| parts = urlsplit(uri) | |
| bucket, key = parts.netloc, unquote(parts.path.lstrip("/")) | |
| client = storage.Client() | |
| if key and not key.endswith("/"): | |
| blob = client.bucket(bucket).get_blob(key) | |
| if blob is None: | |
| raise ValueError(f"No such GCS object: {uri}") | |
| return [ | |
| trackio.ResolvedReference( | |
| relkey=None, | |
| uri=uri, | |
| size=blob.size, | |
| digest=blob.etag.strip('"') if checksum else None, | |
| ) | |
| ] | |
| base = key[: key.rfind("/") + 1] | |
| entries = [] | |
| for blob in client.list_blobs(bucket, prefix=key): | |
| if blob.name.endswith("/"): | |
| continue | |
| if len(entries) >= max_objects: | |
| raise ValueError( | |
| f"{uri} expands to more than {max_objects} objects; " | |
| "pass max_objects= to raise the limit." | |
| ) | |
| entries.append( | |
| trackio.ResolvedReference( | |
| relkey=blob.name[len(base):], | |
| uri=f"gs://{bucket}/{quote(blob.name, safe='/')}", | |
| size=blob.size, | |
| digest=blob.etag.strip('"') if checksum else None, | |
| ) | |
| ) | |
| return entries | |
| def fetch(self, uri, dest): | |
| parts = urlsplit(uri) | |
| key = unquote(parts.path.lstrip("/")) | |
| storage.Client().bucket(parts.netloc).blob(key).download_to_filename( | |
| str(dest) | |
| ) | |
| def hint(self): | |
| return "Configure GCP credentials so the object can be probed for an ETag." | |
| trackio.register_reference_handler(GcsHandler()) | |
| ``` | |
| ### Azure Blob Storage (via `azure-storage-blob` + `azure-identity`) | |
| Azure blobs are addressed by plain `https://` URLs (`https://<account>.blob.core.windows.net/<container>/<blob>`), so this handler claims them by host suffix — registering it makes it win over Trackio's generic HTTP handler: | |
| ```python | |
| from urllib.parse import quote, unquote, urlsplit | |
| from azure.identity import DefaultAzureCredential | |
| from azure.storage.blob import BlobServiceClient | |
| import trackio | |
| class AzureHandler(trackio.ReferenceHandler): | |
| def matches(self, scheme, uri): | |
| return scheme in ("http", "https") and urlsplit( | |
| uri | |
| ).netloc.lower().endswith(".blob.core.windows.net") | |
| def _split(self, uri): | |
| parts = urlsplit(uri) | |
| container, _, blob_name = parts.path.lstrip("/").partition("/") | |
| return f"{parts.scheme}://{parts.netloc}", container, unquote(blob_name) | |
| def _service(self, account_url): | |
| return BlobServiceClient(account_url, credential=DefaultAzureCredential()) | |
| def resolve(self, uri, checksum, max_objects): | |
| account_url, container, blob_name = self._split(uri) | |
| service = self._service(account_url) | |
| if blob_name and not blob_name.endswith("/"): | |
| props = service.get_blob_client(container, blob_name).get_blob_properties() | |
| return [ | |
| trackio.ResolvedReference( | |
| relkey=None, | |
| uri=uri, | |
| size=props.size, | |
| digest=props.etag.strip('"') if checksum else None, | |
| ) | |
| ] | |
| base = blob_name[: blob_name.rfind("/") + 1] | |
| entries = [] | |
| container_client = service.get_container_client(container) | |
| for blob in container_client.list_blobs(name_starts_with=blob_name): | |
| if blob.name.endswith("/"): | |
| continue | |
| if len(entries) >= max_objects: | |
| raise ValueError( | |
| f"{uri} expands to more than {max_objects} objects; " | |
| "pass max_objects= to raise the limit." | |
| ) | |
| entries.append( | |
| trackio.ResolvedReference( | |
| relkey=blob.name[len(base):], | |
| uri=f"{account_url}/{container}/{quote(blob.name, safe='/')}", | |
| size=blob.size, | |
| digest=blob.etag.strip('"') if checksum else None, | |
| ) | |
| ) | |
| return entries | |
| def fetch(self, uri, dest): | |
| account_url, container, blob_name = self._split(uri) | |
| downloader = ( | |
| self._service(account_url) | |
| .get_blob_client(container, blob_name) | |
| .download_blob() | |
| ) | |
| with open(dest, "wb") as f: | |
| downloader.readinto(f) | |
| def hint(self): | |
| return "Configure Azure credentials so the blob can be probed for an ETag." | |
| trackio.register_reference_handler(AzureHandler()) | |
| ``` | |
| These handlers raise when the source can't be probed; if you'd rather record an un-checksummed reference in that case (for example, when cataloging data behind auth you can't reach at log time), catch the provider error inside `resolve` and return `[trackio.ResolvedReference(relkey=None, uri=uri)]` — Trackio will fall back to the URI as the digest and warn, using your `hint()`. | |
| Register handlers in every process that adds *or downloads* such references: `Artifact.download()` needs the handler to fetch the bytes, and a URI whose scheme has no registered handler raises. | |
| ## Browsing artifacts in the dashboard | |
| The dashboard's **Artifacts** tab (shown once a project has artifacts) lists artifacts grouped by type, with each version's aliases, size, and file count. Selecting a version shows its file manifest — click a file to download it — plus its metadata and lineage: the run that produced it and the runs that used it, each linking to the run's detail page. | |
| Run detail pages show the same lineage from the other direction, with **Output artifacts** and **Input artifacts** sections linking back to the Artifacts tab. | |
| ## Remote storage | |
| When your run targets a Hugging Face Space or self-hosted server (see [Track](track)), artifact files are content-addressed and uploaded once: Trackio skips blobs the server already has, so re-logging shared files is cheap. Artifact metadata and blobs are persisted alongside your other run data to the configured HF Dataset or bucket. | |
Xet Storage Details
- Size:
- 19.6 kB
- Xet hash:
- 82be993452acf7025e1001e155004426d39ef8893a2327b89c29cd6838dbe0f0
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.