DenseFeed / docs /zero-gputeo.md
j14i's picture
feat: ZeroGPU backend for Hugging Face Spaces deployment (#4)
864480d unverified
|
Raw
History Blame Contribute Delete
35.7 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

Research: Building an Orchestrator/Queue System for Hugging Face ZeroGPU Beyond 120s

Date: 2026-06-06 Status: Research brief with honest feasibility assessment


Executive Summary

ZeroGPU is not viable for tasks that genuinely exceed ~120 seconds of continuous GPU work. The hard limit is enforced at the infrastructure level β€” the forked GPU process is killed by the ZeroGPU scheduler, and this kill cannot be caught or intercepted. However, there is a meaningful middle ground: tasks that conceptually need more than 120s can be decomposed into discrete GPU calls of ≀120s each, with state persisted between calls. This requires careful architecture and comes with significant trade-offs. For production workloads that need unbounded GPU time, dedicated GPU hosting ($0.50–$5.00/hr) is the honest answer.


1. ZeroGPU Limitations: What Actually Happens

1.1 The 120-Second Timeout

Hard limit: 120 seconds. The @spaces.GPU(duration=...) parameter accepts values up to 120 seconds reliably. Values between 120–240s sometimes partially work but are unreliable and may be rejected with "ZeroGPU illegal duration" errors. Values above 240s are rejected outright. (HF Forums β€” illegal duration, zero-gpu-explorers discussions)

What happens at timeout: The ZeroGPU runtime forks the main process, attaches a GPU slice, runs the decorated function, and then kills the fork. At timeout, the fork is killed externally β€” this is not a Python exception you can catch with try/except. The process receives a kill signal (effectively SIGKILL), and the GPU slice is torn down. The Gradio UI sees a "GPU Task aborted" or "ZeroGPU worker error" message. (HF blog β€” zerogpu-aoti, HF Forums β€” GPU Task aborted)

Key implications:

  • No try/finally cleanup runs at timeout β€” the process is just dead
  • No graceful shutdown, no state serialization at timeout boundary
  • The main process (Gradio server) survives; only the fork dies
  • You cannot heartbeat or keepalive your way past the timeout

1.2 State Persistence Between Calls

Model loading works across calls. Models loaded at module level with .to('cuda') use ZeroGPU's CUDA emulation mode. The model weights reside in the main process's memory (CPU-side CUDA emulation). When @spaces.GPU is called, the fork inherits the process memory space including model weights, and a real GPU is attached. The model is effectively "warm" for each fork. (Official docs)

What persists:

  • Model weights loaded at module level (inherited by each fork)
  • Global Python state (module-level variables, caches)
  • Files on disk (ephemeral β€” lost on Space restart)
  • Files in HF Storage Buckets (persistent across restarts)

What does NOT persist:

  • GPU memory state (each fork starts with a fresh GPU)
  • CUDA context, kernels, compiled graphs (unless AoT-compiled and saved to disk)
  • In-progress tensor computations

1.3 Re-entrant / Multiple @spaces.GPU Calls

Yes, you can call @spaces.GPU functions multiple times from within the same request. Each call triggers a separate fork β†’ GPU allocation β†’ execute β†’ kill cycle. The main process orchestrates these calls sequentially.

# This IS valid - two separate GPU calls in one request

@spaces.GPU(duration=60)

def encode_text(prompt):

return text\_encoder(prompt)

@spaces.GPU(duration=60)

def run_diffusion(latents, steps):

return unet(latents, num\_inference\_steps=steps)

def generate(prompt):

latents \= encode\_text(prompt)     \# First GPU call

images \= run\_diffusion(latents, 20\)  \# Second GPU call

return images

Caveat: Each call consumes the user's GPU quota independently. Two 60s calls = 120s of quota consumed. The user waits through two separate GPU queue + allocation cycles.

1.4 Duration Quota System

Account Type Daily GPU Quota Queue Priority
Unauthenticated 2 minutes Low
Free account 5 minutes Medium
Pro ($9/mo) 40 minutes Highest
Team/Enterprise 40–60 minutes Highest

Pro/Team/Enterprise users can extend beyond quota using credits at $1 per 10 minutes of GPU time. (Official docs)

Important: Quota is charged based on the reserved duration, not actual runtime. A @spaces.GPU(duration=120) call that finishes in 30s still consumes 120s of quota.

1.5 Hardware Specs

Current ZeroGPU hardware: NVIDIA RTX Pro 6000 Blackwell (successor to H200).

  • large (default): half GPU, 48GB VRAM, 1Γ— quota cost
  • xlarge: full GPU, 96GB VRAM, 2Γ— quota cost

2. Existing Solutions: How Big Demos Handle This

2.1 The AoT Compilation Strategy (Most Successful)

The official Hugging Face approach for video generation (Wan, LTX, Flux) is not to work around the timeout β€” it's to make inference fast enough to fit within 120s. The blog post Make your ZeroGPU Spaces go brrr with ahead-of-time compilation documents 1.3×–1.8Γ— speedups using:

  1. PyTorch AoT compilation (torch.export + AOTInductor) β€” compile once, reload instantly
  2. FP8 quantization β€” additional 1.2Γ— speedup on H200's FP8 hardware
  3. Flash Attention 3 β€” pre-built kernels via kernels library
  4. Regional compilation β€” compile repeated transformer blocks once, reduce cold-start from 6min to 30sec

Real-world example: The LTX-Video Playground uses @spaces.GPU(duration=120) with dynamic duration estimation:

def get_duration(steps, duration_seconds, ...):

if steps \> 4 and duration\_seconds \> 2:

    return 90

elif steps \> 4 or duration\_seconds \> 2:

    return 75

else:

    return 60

@spaces.GPU(duration=get_duration)

def generate_video(...):

...

Real-world example: The Wan 2.2 AoTI Space uses a sophisticated duration estimator:

def get_inference_duration(...):

BASE\_FRAMES\_HEIGHT\_WIDTH \= 81 \* 832 \* 624

BASE\_STEP\_DURATION \= 15

factor \= num\_frames \* width \* height / BASE\_FRAMES\_HEIGHT\_WIDTH

step\_duration \= BASE\_STEP\_DURATION \* factor \*\* 1.5

gen\_time \= int(steps) \* step\_duration

return max(60, min(gen\_time, 120))

2.2 The "Keep It Short" Pattern

Most production ZeroGPU Spaces simply limit what users can request:

  • Cap resolution (512Γ—512 instead of 1024+)
  • Cap step count (20–30 steps)
  • Cap video length (2–4 seconds)
  • Offer quality presets (Fast/Balanced/Quality) with different step counts

This is the dominant pattern. No major demo tries to exceed 120s.

2.3 cbensimon's ZeroGPU Queue

The Space cbensimon/zero-gpu-queue is a minimal test of ZeroGPU queue mechanics. The related Gradio PRs (#4937, #5129) fix Gradio's internal queue max_size and concurrency_count for ZeroGPU. This is about queue management, not about exceeding the timeout.

2.4 Agent Zero Orchestration

ScottzillaSystems/agent-zero-orchestration uses @spaces.GPU(duration=180) for agent loops. This pushes past 120s but relies on the "sometimes works up to 240s" zone β€” not reliable for production.


3. Architecture Patterns

Pattern A: Checkpoint-and-Resume (Feasible, Complex)

Concept: Break work into chunks, save intermediate state to disk/Storage Bucket between @spaces.GPU calls.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Main Process (CPU) β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ Chunk 1 │──>β”‚ Chunk 2 │──>β”‚ Chunk N │──> Result β”‚

β”‚ β”‚ @GPU(60) β”‚ β”‚ @GPU(60) β”‚ β”‚ @GPU(60) β”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β”‚ β”‚ β”‚ β”‚

β”‚ v v β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ HF Storage Bucket /data β”‚ β”‚

β”‚ β”‚ checkpoint_step_001.pt β”‚ β”‚

β”‚ β”‚ checkpoint_step_002.pt β”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Pseudo-code:

import spaces

import torch

import json

from pathlib import Path

from huggingface_hub import CommitOperationAdd, HfApi

STATE_DIR = Path("/data/checkpoints") # Storage Bucket mount

@spaces.GPU(duration=60)

def run_chunk(prompt_embeds, chunk_idx, total_chunks):

\# Load checkpoint if resuming

state\_path \= STATE\_DIR / f"chunk\_{chunk\_idx}.pt"

if state\_path.exists():

    latents \= torch.load(state\_path)

else:

    latents \= torch.randn(1, 4, 64, 64, device="cuda")



\# Run N diffusion steps for this chunk

steps\_per\_chunk \= 10

for step in range(steps\_per\_chunk):

    latents \= unet\_step(latents, prompt\_embeds, step \+ chunk\_idx \* steps\_per\_chunk)



\# Save checkpoint

torch.save(latents.cpu(), state\_path)



return chunk\_idx \+ 1  \# Return next chunk index

def generate_long(prompt, total_steps=100):

prompt\_embeds \= encode\_prompt(prompt)  \# CPU or quick GPU call



chunk\_size \= 10  \# steps per GPU call

total\_chunks \= total\_steps // chunk\_size

chunk\_idx \= 0



while chunk\_idx \< total\_chunks:

    chunk\_idx \= run\_chunk(prompt\_embeds, chunk\_idx, total\_chunks)



\# Final decode

return decode\_latents(chunk\_idx)

Trade-offs:

  • βœ… Works within ZeroGPU constraints
  • βœ… Each chunk respects the timeout
  • βœ… Can resume from checkpoint if Space restarts
  • ❌ Latent tensor serialization to disk is slow (seconds per save)
  • ❌ Each GPU call goes through queue β†’ allocate β†’ fork β†’ execute β†’ kill cycle
  • ❌ User quota consumed per chunk (10 chunks Γ— 60s = 600s quota for 100-step generation)
  • ❌ Gradio UI must stay alive across multiple sequential GPU calls

When it works: Video generation where you can chunk frames (e.g., generate 4 frames per GPU call, accumulate).

Pattern B: Step-Wise Execution via Generators (Feasible, Better UX)

Concept: Use Gradio's generator/streaming pattern to yield intermediate results while making sequential @spaces.GPU calls.

import gradio as gr

import spaces

import torch

pipe = ... # loaded at module level

@spaces.GPU(duration=30)

def diffusion_step(latents, prompt_embeds, step):

"""Run ONE diffusion step per GPU call."""

latents \= pipe.scheduler.scale\_model\_input(latents, step)

noise\_pred \= pipe.unet(latents, step, encoder\_hidden\_states=prompt\_embeds).sample

latents \= pipe.scheduler.step(noise\_pred, step, latents).prev\_sample

return latents

def generate_streaming(prompt, total_steps=50):

"""Generator that yields intermediate images every N steps."""

prompt\_embeds \= encode(prompt)  \# quick

latents \= torch.randn(1, 4, 64, 64\)



for step in range(total\_steps):

    latents \= diffusion\_step(latents, prompt\_embeds, step)

    

    if step % 5 \== 0:

        \# Decode and yield intermediate result

        image \= decode(latents)

        yield image, f"Step {step}/{total\_steps}"



yield decode(latents), "Done\!"

gr.Interface(

fn=generate\_streaming,

inputs=\[gr.Text(), gr.Slider(10, 100)\],

outputs=\[gr.Image(), gr.Text()\],

).launch()

Trade-offs:

  • βœ… Streaming UI shows progress
  • βœ… Each step is a separate GPU call with its own timeout
  • ❌ Extremely quota-expensive: 50 steps Γ— 30s duration = 1500s quota for what should be a 20s job
  • ❌ Each step goes through GPU allocation queue (latency per step: 2–10s)
  • ❌ Model weights are loaded into GPU memory fresh for each fork (slow if not using AoT)

Honest assessment: This pattern is technically possible but wasteful. You're paying 25Γ— in quota and 10Γ— in wall-clock time for the privilege of streaming individual diffusion steps. The only scenario where it makes sense is if each "step" is itself a substantial computation (e.g., one frame of video generation that takes 30–60s).

Pattern C: Dedicated Queue Space + Worker Spaces (Advanced, Fragile)

Concept: One Space acts as a queue manager (CPU-only), delegating work to multiple ZeroGPU worker Spaces via API calls.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Queue Space (CPU-only) β”‚

β”‚ - SQLite / in-memory queue β”‚

β”‚ - REST API for job submit β”‚

β”‚ - Polling for results β”‚

β”‚ - Gradio UI for users β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

       β”‚ HTTP API calls

       v

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Worker Space A (ZeroGPU) β”‚

β”‚ @spaces.GPU(duration=120) β”‚

β”‚ POST /run β†’ process chunk β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

       ...

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Worker Space B (ZeroGPU) β”‚

β”‚ @spaces.GPU(duration=120) β”‚

β”‚ POST /run β†’ process chunk β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

How it works:

  1. User submits job via Queue Space's Gradio UI
  2. Queue Space writes job to HF Dataset or Storage Bucket
  3. Queue Space calls Worker Space's API endpoint (Gradio's REST API)
  4. Worker reads input from Storage, runs GPU computation, writes result to Storage
  5. Queue Space polls for result, assembles final output

Pseudo-code (Queue Space):

import gradio as gr

import requests

import time

from huggingface_hub import HfApi

WORKER_URL = "https://your-worker-space.hf.space/api/predict"

JOB_STORAGE = "username/job-storage" # HF Dataset

def submit_job(prompt, config):

\# 1\. Write job to storage

job\_id \= write\_job\_to\_dataset(JOB\_STORAGE, prompt, config)



\# 2\. Call worker

response \= requests.post(WORKER\_URL, json={

    "data": \[job\_id, prompt, config\]

})



\# 3\. Poll for completion

while not job\_complete(JOB\_STORAGE, job\_id):

    time.sleep(2)

    yield None, "Processing..."



\# 4\. Assemble result

result \= read\_result\_from\_dataset(JOB\_STORAGE, job\_id)

yield result, "Done\!"

gr.Interface(submit_job, [gr.Text(), gr.JSON()], [gr.Image(), gr.Text()]).launch()

Trade-offs:

  • βœ… Can parallelize across multiple workers
  • βœ… Queue management is clean
  • ❌ 10 ZeroGPU Space limit per Pro account β€” can't scale to many workers
  • ❌ Each Space requires separate deployment and maintenance
  • ❌ Cross-Space communication adds latency (HTTP API + Storage I/O)
  • ❌ Each Space consumes the user's quota when they call it
  • ❌ Gradio's API client adds overhead; no native inter-Space RPC

Pattern D: Hybrid CPU Orchestration + GPU Bursts (Most Practical)

Concept: Do everything except inference on CPU in the main process, using @spaces.GPU only for the actual computation kernel. Use HF Scheduled Jobs for orchestration if needed.

import gradio as gr

import spaces

import torch

# Model loaded at module level (CUDA emulation handles .to('cuda'))

pipe = DiffusionPipeline.from_pretrained(...).to('cuda')

@spaces.GPU(duration=get_duration)

def gpu_kernel(prompt, steps, resolution, seed):

"""Single GPU call β€” all computation happens here."""

generator \= torch.Generator("cuda").manual\_seed(seed)

return pipe(

    prompt,

    num\_inference\_steps=steps,

    height=resolution,

    width=resolution,

    generator=generator,

).images\[0\]

def get_duration(prompt, steps, resolution, seed):

"""Estimate duration to request minimal quota."""

if resolution \<= 512 and steps \<= 20:

    return 30

elif resolution \<= 768 and steps \<= 30:

    return 60

else:

    return 120

# Limit what users can request to fit within 120s

with gr.Blocks() as demo:

prompt \= gr.Text()

resolution \= gr.Dropdown(\[256, 512, 768\], value=512)

steps \= gr.Slider(1, 50, value=20)

output \= gr.Image()



generate\_btn \= gr.Button("Generate")

generate\_btn.click(gpu\_kernel, \[prompt, steps, resolution, gr.Number(42)\], output)

demo.launch()

This is what the big demos actually do. They don't exceed 120s β€” they constrain the problem to fit.

Pattern E: HF Scheduled Jobs for Long Orchestration (Emerging)

HF now supports Scheduled Jobs that can run on GPU hardware:

hf jobs scheduled uv run --flavor a10g-small --with torch @hourly python orchestrator.py

This is a relatively new feature and could be used for:

  • Periodic batch processing
  • Pre-computing common outputs
  • Fine-tuning jobs that run in scheduled windows

However, this uses paid dedicated GPU, not ZeroGPU.


4. Can You Pass Model State Between ZeroGPU Invocations?

Yes, but with caveats:

Method Works? Speed Notes
Global model .to('cuda') at module level βœ… Yes Fast Weights in main process memory, inherited by fork
Pickle/torch.save to ephemeral disk βœ… Yes Slow Lost on Space restart
Save to HF Storage Bucket βœ… Yes Very slow Network I/O per save
Save to HF Dataset βœ… Yes Very slow Git-based, not ideal for frequent writes
Keep tensor in global variable ⚠️ Partial Fast Survives between calls but NOT across Space restarts
CUDA graph persistence ❌ No N/A Each fork gets fresh GPU context

Recommendation: For model weights, use the standard pattern (load at module level, .to('cuda')). For intermediate computation state, use ephemeral disk /data/ for speed, with HF Storage Buckets only if you need persistence across restarts.


5. The Gradio UI Timeout Problem

When chaining multiple @spaces.GPU calls, the Gradio UI must stay responsive. The key mechanisms:

  1. Generators (yield): Gradio supports generator functions that yield intermediate results. The UI stays alive between yields.
  2. gr.Progress(): Shows a progress bar during long operations.
  3. Queue mode: Gradio's queue system handles concurrent users.

Problem: If the total time across all GPU calls exceeds ~5 minutes, users may see "connection timed out" from Gradio's WebSocket. (HF Forums β€” connection timed out)

Solution: Use generator pattern with periodic yields to keep the WebSocket alive, and poll for completion rather than blocking.


6. Component Diagram: The Recommended Architecture

For tasks that genuinely need >120s and you insist on using ZeroGPU:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ USER'S BROWSER β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ Submit Job │──>β”‚ See Progress │──>β”‚ Download Result β”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ (polling/SSE) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

                           β”‚ WebSocket

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ GRADIO SPACE (Main Process, CPU) β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ Job Orchestrator β”‚ β”‚

β”‚ β”‚ (Python generator)β”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β”‚ β”‚ β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ v v v β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ @GPU(60) β”‚ β”‚ @GPU(60) β”‚ β”‚ @GPU(60) β”‚ ← Sequential β”‚

β”‚ β”‚ Chunk 1 β”‚ β”‚ Chunk 2 β”‚ β”‚ Chunk N β”‚ GPU forks β”‚

β”‚ β”‚ encode+gen β”‚ β”‚ generate β”‚ β”‚ generate+dec β”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β”‚ β”‚ β”‚ β”‚ β”‚

β”‚ v v v β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ HF Storage Bucket (/data/) β”‚ β”‚

β”‚ β”‚ job_123_chunk_1.pt β†’ job_123_chunk_2.pt β†’ result.pt β”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key design decisions:

  1. Single Space (not multi-Space orchestration) to avoid the 10-Space limit
  2. Generator pattern keeps UI alive between chunks
  3. State persisted to Storage Bucket between GPU calls
  4. Each chunk must complete in <60s (leaving buffer before 120s hard limit)
  5. Dynamic duration estimator minimizes quota waste

7. Honest Feasibility Assessment

Tasks where ZeroGPU + orchestration COULD work:

Task Feasible? Strategy
Video gen (4–8s clips, optimized) βœ… Yes AoT + FP8, fit in 120s
Batch image gen (10 images) βœ… Yes Sequential @spaces.GPU calls, 1 image each
Long text generation (LLM streaming) βœ… Yes Already works β€” streaming is natural
Image gen at high res (1024+) ⚠️ Maybe AoT compilation + FP8 might squeeze it in
Video gen (>10s) ⚠️ Difficult Chunk frames, checkpoint between calls
Fine-tuning / training ❌ No Not what ZeroGPU is designed for
Batch processing (100+ items) ❌ No Quota exhaustion after a handful of items
Real-time video gen (>30s) ❌ No Fundamentally incompatible

Tasks where ZeroGPU WILL NOT work:

  • Model fine-tuning β€” requires minutes to hours of continuous GPU
  • Large-scale batch processing β€” quota burns too fast
  • Training / LoRA adaptation β€” optimizer state is hard to checkpoint between forks
  • Real-time video longer than ~10s β€” even AoT can't make it fast enough
  • Multi-stage pipelines with tight dependencies β€” inter-GPU-call latency kills throughput

8. Cost Comparison: ZeroGPU vs. Alternatives

ZeroGPU (if you could make it work)

Tier Cost What You Get
Pro ($9/mo) $9/month + $0.10/min over quota 40 min/day GPU, H200-class, shared
Credits $1 per 10 min GPU time Extends beyond daily quota
Effective hourly rate ~$6/hr (at credit rate) But shared, queued, and time-limited

Dedicated GPU Hosting (No Timeout)

Provider GPU Price/hr Notes
HF Inference Endpoints T4 16GB $0.50/hr Cheapest GPU, good for inference
HF Inference Endpoints A10G 24GB $1.00/hr Good mid-range
HF Inference Endpoints A100 80GB $2.50–$5.00/hr Serious workloads
RunPod Secure Cloud A100 80GB $2.17/hr Cheapest A100
RunPod Secure Cloud H100 80GB $3.35/hr Cheapest H100
Modal A100 80GB $2.10/hr Python-native, $30/mo free credit
Fal.ai A100 80GB $0.99/hr Cheapest per-GPU-second
Replicate A100 80GB $5.04/hr Simplest API, most expensive

Sources: HF Endpoints pricing, RunPod pricing, HostFleet comparison, Replicate pricing

Cost Analysis Example: Processing 100 video generations at 5 min each

Option Total GPU Time Cost
ZeroGPU (Pro credits) 500 min $50 in credits + $9/mo subscription
HF Endpoints (A10G) 500 min = ~8.3 hrs ~$8.30 (shut down between jobs)
RunPod Serverless (A100) 500 min = ~8.3 hrs ~$18.00
Fal.ai (A100) 500 min = ~8.3 hrs ~$8.25
Modal (A100) 500 min = ~8.3 hrs ~$17.40 ($30 free credit covers it)

Conclusion: For any sustained GPU workload, dedicated hosting is cheaper than ZeroGPU credits. ZeroGPU's value is the free tier (40 min/day for Pro) for demos and prototyping, not production workloads.


9. Minimal-Cost Alternative Recommendations

Tier 1: "I need free/cheap and can tolerate constraints"

β†’ ZeroGPU with AoT compilation. Optimize your model to fit in 120s. Use the strategies from the official blog post:

  1. AoT compile the transformer/denoiser
  2. Apply FP8 quantization
  3. Use Flash Attention 3
  4. Limit resolution, steps, video length in the UI
  5. Use dynamic duration to minimize quota waste

Tier 2: "I need reliable long-running GPU but want to stay cheap"

β†’ Fal.ai or RunPod Serverless.

  • Fal.ai A100 at $0.99/hr is the cheapest per-GPU-hour option available
  • RunPod Serverless with per-millisecond billing for bursty workloads
  • Both support custom Docker containers with your full pipeline
  • No timeout constraints, no queue system to fight with
  • Add a simple FastAPI + Redis queue on top for orchestration

Tier 3: "I need production-grade inference at scale"

β†’ HF Inference Endpoints or Modal.

  • HF Endpoints: managed, auto-scaling, integrates with HF Hub
  • Modal: Python-native, excellent DX, $30/mo free credit, proper autoscaling
  • Both support scale-to-zero to minimize idle costs
  • Add Celery/Redis or Modal's native queuing for task management

Architecture for Tier 2/3:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Gradio UI │────>β”‚ FastAPI │────>β”‚ Redis Queue β”‚

β”‚ (any host) β”‚<────│ + Celery β”‚<────│ β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜

                      β”‚                   β”‚

                      v                   v

                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

                β”‚  GPU Worker (RunPod/Modal/Fal)    β”‚

                β”‚  \- Pulls from Redis queue        β”‚

                β”‚  \- Runs full pipeline, no timeout β”‚

                β”‚  \- Writes result to S3/HF Storage β”‚

                β”‚  \- Marks job complete in Redis    β”‚

                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

10. Key Takeaways

  1. The 120s timeout is a hard, uncircumventable limit. The forked process is killed externally β€” no Python exception, no graceful shutdown.

  2. The big demos (Flux, Wan, LTX) do NOT exceed 120s. They use AoT compilation + quantization to make inference fast enough, and constrain user inputs to fit within the budget.

  3. Chunking/checkpointing is theoretically possible but impractical for most use cases. Each chunk goes through the full GPU allocation pipeline (queue β†’ allocate β†’ fork β†’ execute β†’ kill), adding 5–15s overhead per chunk and burning quota at the reserved duration rate.

  4. ZeroGPU is designed for demos, not production workloads. The daily quota, per-call timeout, shared infrastructure, and queue-based scheduling all point to this being a demo/prototyping tool.

  5. If your task genuinely needs >120s of continuous GPU, use dedicated GPU hosting. At $0.50–$5.00/hr, it's cheaper than ZeroGPU credits and has no timeout constraints.

  6. The most promising pattern for "long" tasks on ZeroGPU is: AoT compile + FP8 quantize to fit more work per second, use dynamic duration to minimize quota waste, and limit user-facing parameters to stay within 120s. For video: generate short clips (2–4s) and stitch client-side.

  7. HF Storage Buckets are the right persistence mechanism for checkpointing between GPU calls β€” mounted as local filesystem, persists across Space restarts, S3-like semantics.


Sources

Official Documentation

Community Sources

Example Implementations

Pricing Sources


Provenance

Claim Source Confidence
120s hard duration limit HF Forums, official docs High
Process killed (SIGKILL-like) at timeout HF blog ("kills the fork"), community reports High
Duration >120s sometimes works up to 240s Community reports Medium
Quota charged at reserved duration, not actual HF Forums discussion High
AoT gives 1.3–1.8Γ— speedup HF official blog post High
FP8 gives additional 1.2Γ— speedup HF official blog post High
Model weights persist across GPU calls Official docs, spaces source High
10 ZeroGPU Spaces limit per Pro account Official docs High
Storage Buckets persist across Space restarts Official docs High
Pricing figures Vendor pricing pages as of April 2026 High