mcpField
H
huggingface-zerogpu

huggingface-zerogpu

github.com/huggingface云与 DevOps11.2k1.3k 次安装5 天前更新
代码仓库

关于 huggingface-zerogpu

面向在 Hugging Face ZeroGPU 上构建 Gradio SDK Spaces 的编码技能,涵盖 GPU 调度、配额与时长设置、进程隔离及并发注意事项。

主要功能

  1. 01ZeroGPU 装饰器与 GPU 生命周期指南
  2. 02时长与配额调优
  3. 03进程隔离和序列化规则
  4. 04并发安全模式
  5. 05CUDA 依赖与构建限制

适用场景

  1. 01在 ZeroGPU 上实现 Gradio 演示
  2. 02选择 GPU 时长和规格
  3. 03排查配额或序列化故障

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/huggingface/skills --skill huggingface-zerogpu

安装后在 Claude Code 中输入/huggingface-zerogpu确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
concurrency.md
cuda-and-deps.md
how-quota-works.md
how-zerogpu-works.md

详细介绍

Hugging Face ZeroGPU

适用于使用 ZeroGPU 硬件的 Hugging Face Spaces ML 演示的规则和模式。涵盖 @spaces.GPU、时长和配额调整、进程隔离、CUDA 可用性模型、并发安全性以及 CUDA 构建限制。

范围

本技能适用于使用 ZeroGPU 硬件的 Gradio SDK Spaces。Docker 和 Static Spaces 无法调度到 ZeroGPU;Streamlit 应用现在则以 Docker Spaces 运行,因此本技能仅适用于 Gradio。有关常规 Gradio 编码(组件、布局、事件监听器),请参阅此仓库中的 huggingface-gradio 技能。权威的 ZeroGPU 文档位于 https://huggingface.co/docs/hub/spaces-zerogpu——当前底层 GPU、运行时版本列表和层级阈值都会随时间变化,请以该文档为准。

参考文件

参考文件阅读时机
references/concurrency.md编写 ZeroGPU 代码时,务必与 SKILL.md 一起阅读——处理程序默认并行运行
references/how-zerogpu-works.md分析冷启动、工作进程复用、为什么模块级预热不会延续到请求中,或为什么返回 CUDA 张量会导致卡住时
references/how-quota-works.md选择 duration 值、排查 illegal duration 与 quota exceeded 错误,或解释默认 60s 时长为何会阻止短任务时
references/cuda-and-deps.md安装依赖 CUDA 的软件包(例如 flash-attn)、固定 torch 配套包版本或读取 wheel 文件名标签时

硬件

ZeroGPU 提供两种 GPU 规格,分别映射到底层显卡的一部分:

size底层 GPU 的分片配额消耗
large (默认)一半1x
xlarge全部2x

默认的 large 仅提供一半物理 GPU,因此内存带宽和算力都明显低于整张显卡的规格。只有在工作负载确实需要更多内存或算力时才使用 xlarge。

底层 GPU 可能会在不另行通知的情况下变更。 ZeroGPU 已多次更换 GPU 世代;较早的文章可能会提到 A100 或 H200,但这些信息已经过时。要了解当前底层 GPU 和每种规格对应的准确 VRAM,请在确定工作负载规格前查看 ZeroGPU 文档。

基本模式

import spaces
import torch
from transformers import pipeline

pipe = pipeline("text-generation", model="...", device="cuda")

@spaces.GPU
def generate(prompt: str) -> str:
    return pipe(prompt, max_new_tokens=100)[0]["generated_text"]

关键规则:

  1. 在模块级实例化模型,并及时调用 .to("cuda")。ZeroGPU 会透明地处理实际设备映射(见下文的 CUDA 可用性模型)。
  2. 使用 @spaces.GPU 装饰 GPU 函数。在 ZeroGPU 之外,该装饰器不执行任何操作,因此在所有环境中保留它都是安全的。
  3. 将 duration 设为符合实际最坏情况工作负载的值(默认值为 60s)。平台会将 requested duration 与用户的 remaining quota 进行预检查——而不是根据实际运行时间检查——因此,即使任务只需 10 秒,只要用户的剩余配额低于 60s,它也会在默认值仍为 60s 时立即因 quota exceeded 而失败。声明的 duration 越短,在节点级队列中的优先级也越高。请参阅下文的“时长和配额”。
  4. 不支持 torch.compile。 请改用 PyTorch 预先编译(AoTI)(torch 2.8+)。
  5. 谨慎使用 size="xlarge"。 它会分配完整的底层 GPU,但配额消耗为 2x,且通常需要排队更久。
@spaces.GPU(duration=120)
def generate_image(prompt: str):
    return pipe(prompt).images[0]

CUDA 可用性模型

真正的 GPU 访问仅在由 @spaces.GPU 装饰的函数内可用。在这些函数之外,GPU 不会附加到进程。

不过,import spaces 会对 torch 进行猴子补丁,使得:

  • torch.cuda.is_available() 在全局范围内返回 True。
  • 在模块级调用 .to("cuda") / device="cuda" 不会报错。

这是有意为之。在模块级调用 model.to("cuda") 会将张量注册到 ZeroGPU 后端;后端会在启动时的“打包”步骤中将它们写入磁盘卸载目录,并释放相应的 RAM。当 @spaces.GPU 调用到达时,派生出的 GPU 工作进程会通过固定内存流水线将这些权重从磁盘传入 VRAM。在同一 GPU 插槽上跨请求复用的热工作进程会让权重常驻 GPU,并跳过从磁盘到 VRAM 的步骤。面向用户的规则是:在模块级写 device="cuda" 即可正常工作——完整生命周期请参阅 references/how-zerogpu-works.md。

操作位置原因
model.to("cuda") / pipe(..., device="cuda")模块级ZeroGPU 会注册张量并管理设备迁移
实际 CUDA 计算(推理等)在 @spaces.GPU 内部只有在装饰后的调用期间才会附加真正的 GPU
根据 torch.cuda.is_available() 进行分支判断避免依赖此判断由于猴子补丁,该函数始终返回 True

不要在模块级运行推理或 CUDA 内核——此时没有附加真正的 GPU,因此操作可能会悄悄改在 CPU 上运行,也可能失败。

设备选择惯用写法仍然有效

标准惯用写法在 ZeroGPU 下仍然正确:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AutoModel.from_pretrained("...").to(device)
  • ZeroGPU — is_available() 为 True(经过猴子补丁),因此模型会注册以自动迁移设备。
  • 专用 GPU Spaces / 本地 GPU — is_available() 的确为 True。
  • CPU Spaces / 本地 CPU — 解析为 "cpu"。

不要硬编码 device="cuda"——这会导致仅有 CPU 的环境无法运行。

急切加载才是正确的默认做法

在模块级加载模型,而不要等到首次请求时再延迟加载。Space 进程会在任何用户到来之前启动,因此冷启动成本只需支付一次。延迟加载(global model; if model is None: ...、@lru_cache 包装器、首次调用时实例化模型的工厂函数)只会把这项成本转嫁给首位用户。

本地开发:只需安装 spaces

本地运行时不要用 try/except 包裹 import spaces,也不要将 spaces.GPU 重新定义为无操作的回退实现。在 ZeroGPU 之外,spaces 包本身已经是真正的无操作实现:

  • 重量级行为(CUDA 猴子补丁、客户端初始化、启动钩子)由 SPACES_ZERO_GPU 环境变量控制,而该变量仅在 ZeroGPU 上设置。
  • 在 ZeroGPU 之外,@spaces.GPU 会原样返回未经装饰的函数。
  • 顶层 import spaces 只会执行轻量级导入。

Gradio SDK 基础镜像会在每个硬件层级安装 spaces。因此,即便将 Space 复制到专用 GPU(T4、L4、A10G 等)或基础 CPU 环境,也无需更改代码——import spaces 仍会成功,且 @spaces.GPU 会变为透明直通。

反模式
try:
    import spaces
except ImportError:
    class spaces:  # type: ignore
        @staticmethod
        def GPU(func=None, **kwargs):
            return func if func else (lambda f: f)

问题:

  1. 回退实现必须模拟 @spaces.GPU 的每种调用形式——裸装饰器、duration=...、size=...、生成器、aoti_* 助手——并且会随着 spaces API 的扩展而逐渐偏离。
  2. 这会导致 requirements.txt 中隐藏 spaces,尽管 Space 在部署时需要它。
  3. 这解决的是一个并不存在的问题:真正的包在本地已经是无操作实现。
正确做法

将 spaces 添加到依赖项中,并无条件导入:

import spaces

@spaces.GPU
def generate(prompt: str) -> str:
    ...

时长与配额

声明 @spaces.GPU(duration=N) 时会发生三件事:

  1. 层级上限检查 — 每个访客层级都有单次调用的 duration 上限。声明的 duration 超过上限时,无论剩余配额多少,都会立即报错 ZeroGPU illegal duration。(层级数字会随时间变化 — 请参阅 ZeroGPU 文档。)
  2. 配额预检查 — 平台会将 requested duration 与用户的 remaining quota 进行比较。如果 remaining < requested,调用就会报错 ZeroGPU quota exceeded — 即使实际任务所需时间本来在配额范围内也一样。错误消息会显示具体数字,例如 "60s requested vs. 30s left"。默认时长为 60 秒的任务即使只需 10 秒,当用户的剩余配额低于 60 秒时,也会被拦截。
  3. 队列优先级 — 队列按节点划分(同一节点上所有 Spaces 的请求会争用 GPU 时段),声明的 duration 越短,排名越靠前。

以上三点都说明,应声明最短的合理 duration — 即使是短任务也不例外。对于只需 10 秒的任务,明确设置 @spaces.GPU(duration=15) 可避免过早遭到 quota exceeded 拒绝,并提高队列排名。

xlarge 会使请求时长翻倍。 当 size="xlarge" 时,层级上限检查和配额预检查中的 requested 都是 N * 2。因此,@spaces.GPU(duration=60, size="xlarge") 在内部会被视为 120 秒的请求。

为可变工作负载动态设置时长

对于运行时间取决于输入的工作负载,可传入一个可调用对象,为每个请求估算时长。静态设置较长的 duration 会将低层级用户拒之门外(其层级上限可能低于该静态值),并会为简单输入不必要地预留配额。

def estimate_duration(prompt, steps):
    return int(steps * 3.5)

@spaces.GPU(duration=estimate_duration)
def generate(prompt, steps):
    return pipe(prompt, num_inference_steps=steps).images[0]

关于 illegal duration 与 quota exceeded 的完整区别、每日运行次数限制、24 小时配额窗口以及按量付费,详见 references/how-quota-works.md。

进程隔离与 Pickle

由 @spaces.GPU 装饰的函数会在 ZeroGPU 调度器管理的独立进程中运行。参数和返回值通过 pickle 序列化跨越进程边界。

影响如下:

  • 只能传入或返回可 Pickle 的对象。打开的文件句柄、数据库连接、锁、lambda 函数以及引用了不可 Pickle 状态的闭包都会引发 PicklingError。
  • 切勿直接返回 CUDA 张量。在主进程中对 CUDA 张量进行反序列化会触发 torch.cuda._lazy_init(),而 ZeroGPU 会阻止此操作。请先转换为 CPU 数据:返回 tensor.cpu() 或 tensor.cpu().numpy()。
  • CPU 张量、numpy 数组、PIL Images 和普通 Python 对象都可以正常使用。
  • 大对象会产生序列化开销。优先返回轻量数据(张量、数组、文件路径、base64 字符串),而不是复杂的对象图。
gr.State 跨进程边界的语义

由于处理程序在独立进程中运行,gr.State 的值会在每次 yield 时都进行 Pickle 序列化 — 它们不会按引用共享。

  • 生成器收到的是状态的副本(其 id() 与调用方不同)。
  • 生成器中的原地修改在修改后的状态被显式 yield 回来之前,其他处理程序不可见。
  • 为 gr.State 槽位 yield gr.update() 会跳过更新 — 其他处理程序看到的仍是 yield 前的值。
  • 每次 yield 返回状态对象时,都会通过 Pickle 创建一个新副本。

实践建议:

  • 在 ZeroGPU 上,切勿假设 gr.State 具有引用语义。在生成器中修改状态并期望另一个处理程序看到这些修改的代码,会在不报错的情况下使用过时数据。
  • 每次 yield(包括包含 gr.State 值的 yield)都会触发完整的 Pickle 往返序列化。对于大型状态(模型会话、帧缓冲区),请尽量减少 yield 状态的次数 — 最好只在最后 yield。中间 yield 时,请对状态槽位使用 gr.update()。
  • yield 前必须先将状态中的 CUDA 张量移至 CPU — 原因与上文提到的 torch.cuda._lazy_init() 问题相同。

并发

在 ZeroGPU 上,处理程序默认并发运行。这不是可选行为。在单用户测试中运行正常的代码,在生产环境中可能会悄悄破坏或泄露数据。

有三条规则。详细说明和示例见 references/concurrency.md。

  1. 不要使用可变的全局状态。 并发请求会互相覆盖。
  2. 不要为输出使用固定文件路径。 并发请求会覆盖同一个文件。请使用 tempfile 创建唯一路径。
  3. 只读全局变量是安全的。 在启动时加载、并且在请求期间只读取的模型对象、分词器和配置是安全且值得推荐的。

调用粒度

每次进入 @spaces.GPU 函数都会产生不可忽略的开销——跨进程边界的 pickle 往返、工作进程预热、重新连接 CUDA,以及重新经过节点级队列。在热点循环中调用带装饰器的函数会成倍增加这些开销,并引入新的故障模式:后续迭代可能无法获取 GPU 槽位,导致整个作业中途停滞。

装饰拥有循环的外层函数,而不是每次迭代的工作函数:

# Avoid — N GPU entries for N frames
def process_video(frames):
    return [process_frame(f) for f in frames]

@spaces.GPU(duration=...)
def process_frame(frame):
    ...

# Prefer — one GPU entry for the whole video
@spaces.GPU(duration=...)
def process_video(frames):
    return [process_frame(f) for f in frames]

def process_frame(frame):
    ...

如果循环中混合了大量 CPU 工作和 GPU 工作,将整个循环封装起来会把 CPU 耗时计入用户配额。如果这项开销不可忽略,可以将 GPU 工作批处理,使 CPU 预处理和后处理留在装饰器之外;这是视情况采用的优化,而不是默认做法。

CUDA 构建限制

HF Spaces 在纯 CPU 环境中构建 Docker 镜像。在 ZeroGPU 上,构建阶段没有 nvcc,因为基础镜像是 python:3.13(专用 GPU Spaces 使用 nvidia/cuda:*-devel-*,构建时有 nvcc)。因此,只有 sdist 分发包的 CUDA 依赖包(例如裸的 flash-attn)无法通过 ZeroGPU 上的 requirements.txt 安装。只有预构建的 wheel 可用。

自 2025-07 起,ZeroGPU 运行时提供 nvcc,它从 /cuda-image 挂载而来,该路径对应 CUDA devel 镜像(最初为支持 AoTI 而添加)。这使得在 @spaces.GPU 调用中使用 torch.export / AoTI 工作流成为可能。

总之:所有 CUDA 依赖包都必须从预构建的 wheel 安装。如果 PyPI 上没有可用的 wheel,请在外部构建一个(例如托管在 HF Hub 上),并固定其 URL。对于 flash-attn,上游发布页面提供了相当完整的 wheel 矩阵,涵盖大多数 Python × CUDA × torch 组合。

有关 wheel 标签(cxx11 ABI、cu12torch2.X、cp3XX)的读取方法、torch 系列附属包的版本漂移,以及 kernels-community 回退方案,请参阅 references/cuda-and-deps.md。

示例缓存

gr.Examples 的行为取决于环境。具体来说,在 ZeroGPU 上:

  • cache_examples 默认值为 True(Spaces 设置 GRADIO_CACHE_EXAMPLES=true)。
  • cache_mode 默认值为 "lazy"(Spaces 仅在 ZeroGPU 上设置 GRADIO_CACHE_MODE=lazy)。

ZeroGPU 默认使用 lazy,因为即时缓存会在应用启动时预先运行每个示例,但 ZeroGPU 在启动时没有连接 GPU,只有在处理请求时才会连接。对需要 GPU 的示例进行即时缓存会导致失败。

当 cache_examples=True 时,run_on_click / run_examples_on_click 参数会被静默忽略。如果应用依赖仅在点击后填充的行为,请显式设置 cache_examples=False 以保留该行为。

如需在本地复现 ZeroGPU 的示例缓存行为:

GRADIO_CACHE_EXAMPLES=true GRADIO_CACHE_MODE=lazy python app.py

依赖管理

README frontmatter 中的 python_version 固定版本

对于 ZeroGPU,固定 python_version 实际上是必需的。运行时默认版本目前为 Python 3.10,因此,使用 3.11 或更高版本的本地环境如果没有显式固定版本,就会无法在 Space 上安装。请固定为 ZeroGPU 支持的版本(3.12 是合理的默认值);权威的支持版本列表请参阅 ZeroGPU 文档——不要硬编码完整列表,应引用文档。

# README.md frontmatter
python_version: "3.12"

"3.12" 和 "3.12.12" 两种形式均可接受。

不要在 requirements.txt 中固定 spaces

Space 平台会固定自己的 spaces 版本。在 requirements.txt 中固定了冲突的版本,会导致 pip 在构建时无法完成依赖解析。

规则:不要在 requirements.txt 中包含 spaces。

具体做法取决于你使用的工具:

  • 手写 requirements.txt:直接省略 spaces。
  • uv(由 pyproject.toml 管理):在 pyproject.toml 中声明 spaces,让 uv 一并解析传递依赖约束(尤其是 spaces 固定的 psutil),然后在导出时将其排除:
    uv export --no-hashes --no-dev --no-emit-package spaces -o requirements.txt
    
    如果 pyproject.toml 中没有 spaces,uv 就无法看到它的传递依赖约束,可能会在构建时解析出不兼容的版本。
  • pip-tools(pip-compile)/ Poetry:使用等效的排除机制。
固定 torch 版本以匹配 wheel 标签

如果你通过直接 URL 安装依赖 CUDA 的 wheel,wheel 文件名会编码其构建所依据的 torch 主版本和次版本(例如,cu12torch2.8)。在 requirements.txt 中固定 torch==X.Y.Z 以匹配该版本,否则 pip 可能会解析出不同版本的 torch,导致 Space 首次导入时失败。详情及 kernels-community 替代方案见 references/cuda-and-deps.md。

评论

支持 Markdown