关于 huggingface-zerogpu
面向在 Hugging Face ZeroGPU 上构建 Gradio SDK Spaces 的编码技能,涵盖 GPU 调度、配额与时长设置、进程隔离及并发注意事项。
主要功能
- 01ZeroGPU 装饰器与 GPU 生命周期指南
- 02时长与配额调优
- 03进程隔离和序列化规则
- 04并发安全模式
- 05CUDA 依赖与构建限制
适用场景
- 01在 ZeroGPU 上实现 Gradio 演示
- 02选择 GPU 时长和规格
- 03排查配额或序列化故障
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ huggingface/ skills --skill huggingface-zerogpu安装后在 Claude Code 中输入/huggingface-zerogpu确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/concurrency.mdcuda-and-deps.mdhow-quota-works.mdhow-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"]
关键规则:
- 在模块级实例化模型,并及时调用
.to("cuda")。ZeroGPU 会透明地处理实际设备映射(见下文的 CUDA 可用性模型)。 - 使用
@spaces.GPU装饰 GPU 函数。在 ZeroGPU 之外,该装饰器不执行任何操作,因此在所有环境中保留它都是安全的。 - 将
duration设为符合实际最坏情况工作负载的值(默认值为 60s)。平台会将requested duration与用户的remaining quota进行预检查——而不是根据实际运行时间检查——因此,即使任务只需 10 秒,只要用户的剩余配额低于 60s,它也会在默认值仍为 60s 时立即因quota exceeded而失败。声明的duration越短,在节点级队列中的优先级也越高。请参阅下文的“时长和配额”。 - 不支持
torch.compile。 请改用 PyTorch 预先编译(AoTI)(torch 2.8+)。 - 谨慎使用
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)
问题:
- 回退实现必须模拟
@spaces.GPU的每种调用形式——裸装饰器、duration=...、size=...、生成器、aoti_*助手——并且会随着spacesAPI 的扩展而逐渐偏离。 - 这会导致
requirements.txt中隐藏spaces,尽管 Space 在部署时需要它。 - 这解决的是一个并不存在的问题:真正的包在本地已经是无操作实现。
正确做法
将 spaces 添加到依赖项中,并无条件导入:
import spaces
@spaces.GPU
def generate(prompt: str) -> str:
...
时长与配额
声明 @spaces.GPU(duration=N) 时会发生三件事:
- 层级上限检查 — 每个访客层级都有单次调用的
duration上限。声明的duration超过上限时,无论剩余配额多少,都会立即报错ZeroGPU illegal duration。(层级数字会随时间变化 — 请参阅 ZeroGPU 文档。) - 配额预检查 — 平台会将
requested duration与用户的remaining quota进行比较。如果remaining < requested,调用就会报错ZeroGPU quota exceeded— 即使实际任务所需时间本来在配额范围内也一样。错误消息会显示具体数字,例如"60s requested vs. 30s left"。默认时长为 60 秒的任务即使只需 10 秒,当用户的剩余配额低于 60 秒时,也会被拦截。 - 队列优先级 — 队列按节点划分(同一节点上所有 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槽位 yieldgr.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。
- 不要使用可变的全局状态。 并发请求会互相覆盖。
- 不要为输出使用固定文件路径。 并发请求会覆盖同一个文件。请使用
tempfile创建唯一路径。 - 只读全局变量是安全的。 在启动时加载、并且在请求期间只读取的模型对象、分词器和配置是安全且值得推荐的。
调用粒度
每次进入 @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.txtpyproject.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。