Hub Python Library documentation

将文件上传到 Hub

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

将文件上传到 Hub

共享您的文件和工作成果是 Hub 的重要组成部分。huggingface_hub 提供了多种将文件上传到 Hub 的选项。您可以独立使用这些函数,也可以将它们集成到您的库中,从而让用户与 Hub 的交互更加便捷。

每当您需要将文件上传到 Hub 时,都需要登录您的 Hugging Face 账户。有关身份验证的更多详细信息,请参阅此部分。

上传文件

使用 create_repo() 创建仓库后,您可以使用 upload_file() 将文件上传到该仓库。

指定要上传的文件路径、在仓库中上传文件的目标位置,以及要添加该文件的仓库名称。根据仓库类型,您可以选择将仓库类型设置为 dataset、model 或 space。

>>> from huggingface_hub import HfApi
>>> api = HfApi()
>>> api.upload_file(
...     path_or_fileobj="/path/to/local/folder/README.md",
...     path_in_repo="README.md",
...     repo_id="username/test-dataset",
...     repo_type="dataset",
... )

上传文件夹

使用 upload_folder() 函数将本地文件夹上传到现有仓库。指定要上传的本地文件夹路径、在仓库中上传文件夹的目标位置,以及要添加该文件夹的仓库名称。根据仓库类型,您可以选择将仓库类型设置为 dataset、model 或 space。

>>> from huggingface_hub import HfApi
>>> api = HfApi()

# Upload all the content from the local folder to your remote Space.
# By default, files are uploaded at the root of the repo
>>> api.upload_folder(
...     folder_path="/path/to/local/space",
...     repo_id="username/my-cool-space",
...     repo_type="space",
... )

默认情况下,.gitignore 文件会被用于确定哪些文件应提交。默认情况下,我们会检查提交中是否存在 .gitignore 文件;如果不存在,则检查 Hub 上是否存在该文件。请注意,只有位于目录根部的 .gitignore 文件才会被使用。我们不会检查子目录中的 .gitignore 文件。

如果不想使用硬编码的 .gitignore 文件,可以使用 allow_patterns 和 ignore_patterns 参数来筛选要上传的文件。这些参数接受单个模式或模式列表。模式是标准通配符(globbing 模式),文档见 此处。如果同时提供了 allow_patterns 和 ignore_patterns,则两个约束条件都会生效。

除了 .gitignore 文件和允许/忽略模式外,任何子目录中存在的 .git/ 文件夹都会被忽略。

>>> api.upload_folder(
...     folder_path="/path/to/local/folder",
...     path_in_repo="my-dataset/train", # Upload to a specific folder
...     repo_id="username/test-dataset",
...     repo_type="dataset",
...     ignore_patterns="**/logs/*.txt", # Ignore all text logs
... )

你还可以使用 delete_patterns 参数来指定要在同一提交中从仓库删除的文件。 如果你想在推送文件之前清理远程文件夹,但又不清楚其中已存在哪些文件,这会非常有用。

以下示例将本地 ./logs 文件夹上传到远程 /experiment/logs/ 文件夹。仅上传 txt 文件,但在此之前,会先删除仓库中所有之前的日志。所有这些操作都在一次提交中完成。

>>> api.upload_folder(
...     folder_path="/path/to/local/folder/logs",
...     repo_id="username/trained-model",
...     path_in_repo="experiment/logs/",
...     allow_patterns="*.txt", # Upload all local text files
...     delete_patterns="*.txt", # Delete all remote text files before
... )

文件如何上传

当 hf_xet 已安装(默认情况下即如此)时,upload_folder() 会通过流式管道上传文件:文件会与 Hub 进行比对,上传至 Xet 存储后端(该后端内部会执行分块、去重和传输重试),并以自适应批次进行提交,所有步骤均并行执行。实际上这意味着:

  • 任意大小的文件夹:较小的文件夹会在单次提交中上传,而包含大量文件的文件夹会自动拆分为多个提交,以确保不超过服务器限制。当发生这种情况时,后续提交的提交信息会带有 (part 2)、(part 3)、… 这样的后缀。
  • 可恢复:如果上传因任何原因中断,只需重新运行相同的调用即可。已提交的文件会被检测并跳过,已上传的数据块会被去重——重新上传它们几乎不会传输任何数据。整个过程不涉及本地状态:您甚至可以从另一台机器恢复上传。一个例外是:当使用 create_pr=True 时,重新运行会打开一个新的拉取请求。我们建议在恢复上传时,改为使用 revision="refs/pr/N" 重新运行。
  • 无重复读取:文件在分块上传时即进行哈希计算,仅需一次读取。上传开始前没有单独的“哈希”阶段。

实时进度显示会跟踪以下三个阶段:

Found 5,000 files to upload
  Preparing   ████████████████████  5,000 / 5,000 ✓
  Uploading   ██████████████░░░░░░  423 / 603 files  3.8GB · 19.7MB/s
  Committing  ██████████████████░░  4,580 / 5,000  6 commits

如果未安装 hf_xet,upload_folder() 将回退到旧版行为:先对所有内容进行哈希计算,通过 HTTP 上传,然后创建单个提交。我们始终建议保持 hf_xet 处于已安装状态,以获得更好的健壮性

从 CLI 上传

您可以在终端中使用 hf upload 命令将文件直接上传到 Hub。该命令内部使用了上述的 upload_file() 和 upload_folder() 辅助函数。

您可以上传单个文件或整个文件夹:

# Usage:  hf upload [repo_id] [local_path] [path_in_repo]
>>> hf upload Wauplin/my-cool-model ./models/model.safetensors model.safetensors
https://huggingface.co/Wauplin/my-cool-model/blob/main/model.safetensors

>>> hf upload Wauplin/my-cool-model ./models .
https://huggingface.co/Wauplin/my-cool-model/tree/main

local_path 和 path_in_repo 是可选的,可以隐式推断。如果未设置 local_path,该工具会检查是否存在与 repo_id 同名的本地文件夹或文件。如果存在,其内容将被上传。否则,将抛出异常,要求用户显式设置 local_path。无论如何,如果未设置 path_in_repo,文件将上传到仓库的根目录。

目标也可以表示为单个 hf:// URI,其语法为 hf://[<TYPE>/]<ID>[@<REVISION>][/<PATH>](完整语法请参阅 HF URIs 参考)。仓库类型、修订版本和 path_in_repo 将从 URI 中读取,因此不能与 --repo-type 和 --revision 选项同时使用:

# Upload a single file to a dataset on a specific branch
>>> hf upload hf://datasets/Wauplin/my-cool-dataset@my-branch/data/train.csv ./train.csv
https://huggingface.co/datasets/Wauplin/my-cool-dataset/blob/my-branch/data/train.csv

为了在上传大文件时获得最大吞吐量,请设置 HF_XET_HIGH_PERFORMANCE=1 环境变量。这将启用 hf_xet 的高性能模式,该模式会充分利用可用的带宽和 CPU 核心。注意:由于 hf_transfer 已被移除并替换为 hf_xet,因此不再使用旧版的 HF_HUB_ENABLE_HF_TRANSFER=1 标志——请改为设置 HF_XET_HIGH_PERFORMANCE=1。

有关 CLI 上传命令的更多详细信息,请参阅 CLI 指南。

上传大文件夹

upload_folder() 和 hf upload 命令是将文件上传到 Hub 的常用解决方案,即使是非常大的文件夹也适用。文件会通过多次提交流式传输到 Hub,如果过程中断,流程会自动恢复。只需重新运行相同的调用,已上传的文件会被跳过。

>>> api.upload_folder(
...     repo_id="HuggingFaceM4/Docmatix",
...     repo_type="dataset",
...     folder_path="/path/to/local/docmatix",
... )

或从终端:

hf upload HuggingFaceM4/Docmatix --repo-type=dataset /path/to/local/docmatix

大型上传的技巧与窍门

在处理仓库中的大量数据时,有一些限制需要注意。考虑到流式传输数据所需的时间,如果在流程末尾上传/推送失败,或者在 hf.co 上或本地操作时遇到体验下降的情况,都会非常令人烦恼。

请参阅我们的仓库限制与建议指南,了解如何在 Hub 上组织仓库的最佳实践。接下来,让我们分享一些实用技巧,让您的上传过程尽可能顺畅。

  • 从小处着手:我们建议先用少量数据来测试你的上传脚本。当失败只花费很少时间时,迭代脚本会更容易。
  • 预期会出现失败:流式传输大量数据是一项具有挑战性的任务。你无法预知会发生什么,但最好始终假设至少会有一次失败——无论原因是你的机器、你的网络连接还是我们的服务器。例如,如果你计划上传大量文件,最好在上传下一批文件之前,在本地记录哪些文件已经上传过。可以确保已提交的 LFS 文件永远不会被重复上传,但在客户端进行检查仍然可以节省一些时间。upload_folder() 正是为你完成这项工作的。
  • 使用 hf_xet:它利用了 Hub 的新存储后端,使用 Rust 编写,现已向所有人开放。实际上,在使用 huggingface_hub 时,hf_xet 已默认启用。为了获得最佳性能,请将 HF_XET_HIGH_PERFORMANCE=1 设置为环境变量。请注意,启用高性能模式后,该工具将尝试使用所有可用的带宽和 CPU 核心。

高级功能

在大多数情况下,你只需要使用 upload_file() 和 upload_folder() 即可将文件上传到 Hub。 不过,huggingface_hub 还提供了更多高级功能,让操作更加便捷。让我们来看看这些功能!

更快的上传

利用 hf_xet 实现更快的上传,它是 xet-core 库的 Python 绑定,通过基于块的重复数据删除来加速上传和下载。hf_xet 可与 huggingface_hub 无缝集成,但它使用的是 Rust xet-core 库和 Xet 存储,而非 LFS。

hf_xet 使用 Xet 存储系统,该系统将文件拆分为不可变的块,并将这些块的集合(称为块或 xorbs)存储在远程,在请求时检索它们以重新组装文件。在上传时,在确认用户有权写入此仓库后,hf_xet 会扫描文件,将其拆分为块,并将这些块收集到 xorbs 中(并在已知块之间进行去重),然后会上传这些 xorbs 到 Xet 内容寻址服务(CAS),该服务将验证 xorbs 的完整性,注册 xorb 元数据以及 LFS SHA256 哈希(以支持查找/下载),并将 xorbs 写入远程存储。

要启用此功能,只需安装最新版本的 huggingface_hub:

pip install -U "huggingface_hub"

从 huggingface_hub 0.32.0 版本开始,此操作还将安装 hf_xet。

所有其他 huggingface_hub API 将继续正常工作,无需任何修改。要了解 Xet 存储和 hf_xet 的优势,请参阅此 部分。

集群 / 分布式文件系统上传注意事项

从集群上传时,待上传的文件通常位于分布式或网络文件系统(如 NFS、EBS、Lustre、Fsx 等)上。Xet 存储会将这些文件分块,并在本地将其写入块(也称为 xorbs),待块写入完成后即进行上传。为了在从分布式文件系统上传时获得更好的性能,请确保将 HF_XET_CACHE 设置为位于本地磁盘(例如本地 NVMe 或 SSD 磁盘)上的目录。Xet 缓存的默认位置在 HF_HOME 下的(~/.cache/huggingface/xet),而该位置通常位于用户主目录中,往往也处于分布式文件系统上。

非阻塞上传

在某些情况下,你希望在不阻塞主线程的情况下推送数据。这在继续训练的同时上传日志和工件时尤为有用。为此,你可以在 upload_file() 和 upload_folder() 中使用 run_as_future 参数。这将返回一个 concurrent.futures.Future 对象,你可以用它来检查上传状态。

>>> from huggingface_hub import HfApi
>>> api = HfApi()
>>> future = api.upload_folder( # Upload in the background (non-blocking action)
...     repo_id="username/my-model",
...     folder_path="checkpoints-001",
...     run_as_future=True,
... )
>>> future
Future(...)
>>> future.done()
False
>>> future.result() # Wait for the upload to complete (blocking action)
...

使用 run_as_future=True 时,后台任务会被放入队列。这意味着可以确保任务按正确的顺序执行。

尽管后台任务主要用于上传数据/创建提交,但你可以使用 run_as_future() 将任意方法加入队列。例如,你可以使用它在后台创建仓库,然后向其上传数据。上传方法中内置的 run_as_future 参数只是它的一个别名。

>>> from huggingface_hub import HfApi
>>> api = HfApi()
>>> api.run_as_future(api.create_repo, "username/my-model", exists_ok=True)
Future(...)
>>> api.upload_file(
...     repo_id="username/my-model",
...     path_in_repo="file.txt",
...     path_or_fileobj=b"file content",
...     run_as_future=True,
... )
Future(...)

在仓库之间复制文件

使用 copy_files() 可在 Hub 上的不同仓库之间复制文件或文件夹,而无需下载或重新上传大型数据。当您需要在不同模型变体之间复制权重、在仓库之间复制数据集文件,或在您的多个仓库之间重新组织文件时,此功能非常有用。其底层机制是通过 CommitOperationCopy 操作创建一个提交。

>>> from huggingface_hub import HfApi
>>> api = HfApi()

# Copy a single file between repos
>>> api.copy_files(
...     "hf://username/source-model/weights.safetensors",
...     "hf://username/target-model/weights.safetensors",
... )

# Copy an entire folder
>>> api.copy_files(
...     "hf://datasets/username/source-dataset/data/",
...     "hf://datasets/username/target-dataset/data/",
... )

您也可以在同一个仓库内复制:

# Duplicate a file in the same repo
>>> api.copy_files(
...     "hf://username/my-model/config.json",
...     "hf://username/my-model/backup/config.json",
... )

在复制文件夹时,源路径末尾的 / 采用 rsync 风格的语义,表示复制该文件夹的内容,而不将文件夹本身嵌套其中。如果末尾没有 /,则文件夹本身会被嵌套到目标位置。

copy_files() 还支持将文件复制到 Buckets。有关更多详细信息,请参阅 Buckets 指南。

计划上传

Hugging Face Hub 让保存和版本化数据变得轻而易举。然而,当需要数千次更新同一个文件时,存在一些限制。例如,您可能希望保存训练过程的日志,或已部署 Space 的用户反馈。在这些情况下,将数据作为数据集上传到 Hub 是合理的,但正确执行起来可能颇具挑战。主要原因是,您不希望为数据的每次更新都创建版本,因为这会使 git 仓库变得无法使用。CommitScheduler 类为这一问题提供了解决方案。

其思路是运行一个后台任务,定期将本地文件夹推送到 Hub。假设你有一个 Gradio Space,它接收一些文本作为输入并生成两个翻译版本,然后用户可以选择他们偏好的翻译。对于每次运行,你希望保存输入、输出以及用户偏好,以便分析结果。这是 CommitScheduler 的一个完美用例:你需要将数据保存到 Hub(可能包含数百万条用户反馈),但你并不需要实时保存每个用户的输入。相反,你可以将数据保存在本地的 JSON 文件中,并每 10 分钟上传一次。例如:

>>> import json
>>> import uuid
>>> from pathlib import Path
>>> import gradio as gr
>>> from huggingface_hub import CommitScheduler

# Define the file where to save the data. Use UUID to make sure not to overwrite existing data from a previous run.
>>> feedback_file = Path("user_feedback/") / f"data_{uuid.uuid4()}.json"
>>> feedback_folder = feedback_file.parent

# Schedule regular uploads. Remote repo and local folder are created if they don't already exist.
>>> scheduler = CommitScheduler(
...     repo_id="report-translation-feedback",
...     repo_type="dataset",
...     folder_path=feedback_folder,
...     path_in_repo="data",
...     every=10,
... )

# Define the function that will be called when the user submits its feedback (to be called in Gradio)
>>> def save_feedback(input_text:str, output_1: str, output_2:str, user_choice: int) -> None:
...     """
...     Append input/outputs and user feedback to a JSON Lines file using a thread lock to avoid concurrent writes from different users.
...     """
...     with scheduler.lock:
...         with feedback_file.open("a") as f:
...             f.write(json.dumps({"input": input_text, "output_1": output_1, "output_2": output_2, "user_choice": user_choice}))
...             f.write("\n")

# Start Gradio
>>> with gr.Blocks() as demo:
>>>     ... # define Gradio demo + use `save_feedback`
>>> demo.launch()

就是这样!用户输入/输出和反馈将作为数据集在 Hub 上可用。通过使用唯一的 JSON 文件名,可以确保您不会覆盖之前运行产生的数据,也不会覆盖其他同时向同一仓库推送数据的 Spaces/副本。

有关 CommitScheduler 的更多详细信息,以下是您需要了解的内容:

  • 仅追加:假设您只会向该文件夹中添加内容。您只能向现有文件追加数据或创建新文件。删除或覆盖文件可能会导致您的仓库损坏。
  • git history: 调度器将每 every 分钟提交一次该文件夹。为避免过度污染 git 仓库,建议将最小值设置为 5 分钟。此外,调度器被设计为避免空提交。如果在文件夹中未检测到新内容,则计划中的提交将被丢弃。
  • errors: 调度器作为后台线程运行。在实例化该类时启动,并持续运行,直到调用 stop() 或进程退出。如果上传过程中发生错误(例如:连接问题),该错误会被记录,调度器将在下一次计划提交时重试。
  • scripts: 在生命周期有限的脚本(例如训练)中,将调度器用作上下文管理器。离开 with 块时会触发最后一次上传,等待其完成并停止调度器。
  • 线程安全:在大多数情况下,可以安全地假设您可以写入文件而无需担心锁文件。当调度器正在上传时,即使您向该文件夹写入内容,调度器也不会崩溃或损坏。实际上,对于高负载的应用程序,可能会发生并发问题。在这种情况下,我们建议使用 scheduler.lock 锁来确保线程安全。该锁仅在调度器扫描文件夹以检测更改时被阻塞,而在上传数据时不会阻塞。您可以放心地认为,这不会影响您 Space 上的用户体验。

Space 持久化演示

将 Space 中的数据持久化到 Hub 上的 Dataset 是 CommitScheduler 的主要用例。根据具体用例,您可能希望以不同的方式组织数据。该结构必须能够应对并发用户和重启,这通常意味着需要生成 UUID。除了健壮性之外,您还应以 🤗 Datasets 库可读的格式上传数据,以便后续复用。我们创建了一个 Space,演示如何保存多种不同的数据格式(您可能需要根据自身的具体需求对其进行调整)。

自定义上传

CommitScheduler 假设你的数据是仅追加的,并且应以“原样”上传。不过,你可能希望自定义数据上传的方式。你可以通过创建一个继承自 CommitScheduler 的类并重写 push_to_hub 方法来实现(你可以按任意方式重写它)。它保证会在后台线程中每隔 every 分钟被调用一次。你无需担心并发和错误处理,但必须注意其他方面,例如推送空提交或重复数据。

在下面的(简化)示例中,我们重写了 push_to_hub,将所有 PNG 文件打包成一个归档文件,以避免给 Hub 上的仓库带来过大负载:

class ZipScheduler(CommitScheduler):
    def push_to_hub(self):
        # 1. List PNG files
        png_files = list(self.folder_path.glob("*.png"))
        if len(png_files) == 0:
            return None  # return early if nothing to commit

        # 2. Zip png files in a single archive
        with tempfile.TemporaryDirectory() as tmpdir:
            archive_path = Path(tmpdir) / "train.zip"
            with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zip:
                for png_file in png_files:
                    zip.write(filename=png_file, arcname=png_file.name)

            # 3. Upload archive
            self.api.upload_file(..., path_or_fileobj=archive_path)

        # 4. Delete local png files to avoid re-uploading them later
        for png_file in png_files:
            png_file.unlink()

重写 push_to_hub 时,您可以访问 CommitScheduler 的属性,尤其是:

  • HfApi 客户端:api
  • 文件夹参数:folder_path 和 path_in_repo
  • 仓库参数:repo_id、repo_type、revision
  • 线程锁:lock

如需查看自定义调度器的更多示例,请访问我们的演示 Space,其中包含针对不同使用场景的各种实现。

create_commit

upload_file() 和 upload_folder() 函数是高层 API,通常使用起来非常方便。如果您不需要在更低的层级进行操作,我们建议先尝试使用这些函数。但是,如果您希望在提交(commit)级别进行操作,可以直接使用 create_commit() 函数。

create_commit() 支持三种类型的操作:

  • CommitOperationAdd 用于将文件上传到 Hub。如果该文件已存在,则文件内容会被覆盖。此操作接受两个参数:

    • path_in_repo: 要上传文件的目标仓库路径。
    • path_or_fileobj:文件系统中的文件路径或类文件对象。这是要上传到 Hub 的文件内容。
  • CommitOperationDelete 用于从仓库中删除文件或文件夹。此操作接受 path_in_repo 作为参数。

  • CommitOperationCopy 用于在仓库内部或跨仓库复制文件。此操作接受以下参数:

    • src_path_in_repo:要复制的文件在仓库中的路径。
    • path_in_repo:文件应被复制到的仓库路径。
    • src_revision:可选 - 如果您想从其他分支/修订版本复制文件,则指定要复制的文件修订版本。
    • src_repo_id:可选 - 要从中复制的源仓库(例如 "username/source-model")。默认为目标仓库。
    • src_repo_type:可选 - 源仓库的类型("model"、"dataset" 或 "space")。当设置了 src_repo_id 时必填。

例如,如果您想在 Hub 仓库中上传两个文件并删除一个文件:

  1. 使用适当的 CommitOperation 来添加或删除文件,以及删除文件夹:
>>> from huggingface_hub import HfApi, CommitOperationAdd, CommitOperationDelete
>>> api = HfApi()
>>> operations = [
...     CommitOperationAdd(path_in_repo="LICENSE.md", path_or_fileobj="~/repo/LICENSE.md"),
...     CommitOperationAdd(path_in_repo="weights.h5", path_or_fileobj="~/repo/weights-final.h5"),
...     CommitOperationDelete(path_in_repo="old-weights.h5"),
...     CommitOperationDelete(path_in_repo="logs/"),
...     CommitOperationCopy(src_path_in_repo="image.png", path_in_repo="duplicate_image.png"),
... ]
  1. 将您的操作传递给 create_commit():
>>> api.create_commit(
...     repo_id="lysandre/test-model",
...     operations=operations,
...     commit_message="Upload my model weights and license",
... )

除了 upload_file() 和 upload_folder() 之外,以下函数在底层也使用了 create_commit():

如需更详细的信息,请参阅 HfApi 参考文档。

在提交之前预上传 LFS 文件

在某些情况下,你可能希望在调用 commit 之前将大文件上传到 S3 之前。例如,如果你正在提交一个由多个在内存中生成的分片组成的数据集,就需要逐个上传这些分片,以避免内存溢出问题。一种解决方案是将每个分片作为仓库上的单独 commit 进行上传。虽然这种做法完全有效,但其缺点是会生成数十个 commit,从而可能使 git 历史变得混乱。为了解决这个问题,你可以先将文件逐个上传到 S3,然后在最后创建一个单一的 commit。这可以通过将 preupload_lfs_files() 与 create_commit() 结合使用来实现。

这是一种面向高级用户的方法。在绝大多数情况下,直接使用 upload_file()、upload_folder() 或 create_commit() 来处理文件预上传的底层逻辑是最佳选择。preupload_lfs_files() 的主要注意事项是,在实际创建提交之前,上传的文件在 Hub 上的仓库中尚不可访问。如果您有任何疑问,欢迎在我们的 Discord 或 GitHub issue 中联系我们。

下面是一个简单的示例,说明如何预上传文件:

>>> from huggingface_hub import CommitOperationAdd, preupload_lfs_files, create_commit, create_repo

>>> repo_id = create_repo("test_preupload").repo_id

>>> operations = [] # List of all `CommitOperationAdd` objects that will be generated
>>> for i in range(5):
...     content = ... # generate binary content
...     addition = CommitOperationAdd(path_in_repo=f"shard_{i}_of_5.bin", path_or_fileobj=content)
...     preupload_lfs_files(repo_id, additions=[addition])
...     operations.append(addition)

>>> # Create commit
>>> create_commit(repo_id, operations=operations, commit_message="Commit all shards")

首先,我们逐个创建 CommitOperationAdd 对象。在实际场景中,这些对象将包含生成的分片。每个文件在生成下一个文件之前都会先上传。在 preupload_lfs_files() 步骤中,CommitOperationAdd 对象会被修改。你应该仅将其用于直接传递给 create_commit()。该对象的主要更新是其中移除了二进制内容,这意味着如果你没有保留对它的另一个引用,它将被垃圾回收。这是预期行为,因为我们不希望将已上传的内容保留在内存中。最后,我们将所有操作传递给 create_commit() 来创建提交。你可以传递尚未处理的额外操作(添加、删除或复制),它们将被正确处理。

Update on GitHub