Spaces:
Running on Zero
Running on Zero
Forward the caller's ZeroGPU token to the conditioner so one request bills as one request
Browse files
README.md
CHANGED
|
@@ -158,6 +158,19 @@ one-time `PIPE.to("cuda")` is inside the first row's 339 s and does not reappear
|
|
| 158 |
| `H3_GPU_DURATION` | `900` | Seconds per request; the pool applies a 1.5 duration factor. |
|
| 159 |
| `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
## Secrets
|
| 162 |
|
| 163 |
Nothing this Space loads is private any more: the weights are the public
|
|
|
|
| 158 |
| `H3_GPU_DURATION` | `900` | Seconds per request; the pool applies a 1.5 duration factor. |
|
| 159 |
| `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
|
| 160 |
|
| 161 |
+
## Whose GPU quota pays
|
| 162 |
+
|
| 163 |
+
Two cards are booked per request: this Space's denoise loop and the conditioner's forward. ZeroGPU attributes a
|
| 164 |
+
booking to the `X-IP-Token` header of the request that triggered it, so this Space forwards the caller's token to the
|
| 165 |
+
conditioner (`gradio_client.Client(..., headers={"X-IP-Token": ...})`, from the `gr.Request` gradio injects — the UI
|
| 166 |
+
path and the `/generate` API path alike). One user's request then bills as one request across both halves, the way it
|
| 167 |
+
would if this were a single Space, and no org token is ever spent on it.
|
| 168 |
+
|
| 169 |
+
A caller the router cannot attribute — an unauthenticated API call — falls back to the conditioner's IP-based quota,
|
| 170 |
+
whose ceiling is 120 credits. An `xlarge` booking costs **twice** its seconds there, so the conditioner keeps its
|
| 171 |
+
reservation at 60 s (120 credits) for an encode; asking it to upsample a prompt books 120 s (240 credits) and needs a
|
| 172 |
+
forwarded token.
|
| 173 |
+
|
| 174 |
## Secrets
|
| 175 |
|
| 176 |
Nothing this Space loads is private any more: the weights are the public
|
app.py
CHANGED
|
@@ -65,7 +65,7 @@ PIPE = None
|
|
| 65 |
MANAGER = None
|
| 66 |
LOAD_ERROR: str | None = None
|
| 67 |
LOADED_IN: float | None = None
|
| 68 |
-
|
| 69 |
|
| 70 |
|
| 71 |
def status() -> str:
|
|
@@ -170,17 +170,47 @@ def _arm_decode_hooks(pipe):
|
|
| 170 |
module.decode = armed
|
| 171 |
|
| 172 |
|
| 173 |
-
def conditioner():
|
| 174 |
-
"""The other half, over the gradio API
|
| 175 |
-
global CLIENT
|
| 176 |
-
if CLIENT is None:
|
| 177 |
-
from gradio_client import Client
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
| 181 |
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
"""Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
|
| 185 |
|
| 186 |
`rewrite_prompt` is the conditioner's prompt upsampling: it rewrites the request into MiniMax-H3's trained format
|
|
@@ -191,7 +221,7 @@ def encode_remote(prompt, image_path, last_image_path, canvas, num_frames, rewri
|
|
| 191 |
from gradio_client import handle_file
|
| 192 |
from safetensors import safe_open
|
| 193 |
|
| 194 |
-
path, plan = conditioner().predict(
|
| 195 |
prompt=prompt,
|
| 196 |
image_path=handle_file(image_path) if image_path else None,
|
| 197 |
last_image_path=handle_file(last_image_path) if last_image_path else None,
|
|
@@ -255,7 +285,7 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
|
|
| 255 |
return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
|
| 256 |
|
| 257 |
|
| 258 |
-
def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS, duration=5, steps=28, seed=42, upsample=False, progress=gr.Progress(track_tqdm=True)):
|
| 259 |
"""One request. `upsample` is appended last and defaults off, so an existing API client is untouched by it."""
|
| 260 |
if LOAD_ERROR:
|
| 261 |
raise gr.Error(LOAD_ERROR)
|
|
@@ -273,7 +303,8 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
|
|
| 273 |
progress(0.0, desc=f"Upsampling the prompt on {CONDITIONER_SPACE} ..." if upsample else f"Conditioning on {CONDITIONER_SPACE} ...")
|
| 274 |
conditioned = time.time()
|
| 275 |
prompt_embeds, text_token_tags, metadata, plan = encode_remote(
|
| 276 |
-
prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=upsample
|
|
|
|
| 277 |
)
|
| 278 |
condition_seconds = time.time() - conditioned
|
| 279 |
height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames"))
|
|
|
|
| 65 |
MANAGER = None
|
| 66 |
LOAD_ERROR: str | None = None
|
| 67 |
LOADED_IN: float | None = None
|
| 68 |
+
CLIENTS: dict[str | None, object] = {}
|
| 69 |
|
| 70 |
|
| 71 |
def status() -> str:
|
|
|
|
| 170 |
module.decode = armed
|
| 171 |
|
| 172 |
|
| 173 |
+
def conditioner(ip_token: str | None = None):
|
| 174 |
+
"""The other half, over the gradio API — booked against *the caller's* ZeroGPU quota, not this org's.
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
+
ZeroGPU attributes a booking to the `X-IP-Token` header of the request that triggered it
|
| 177 |
+
(`spaces/zero/client.py`), which the Spaces router puts on every browser request. That header is what pays for
|
| 178 |
+
this Space's own `@spaces.GPU` call, and forwarding it to the conditioner makes the same identity pay for the
|
| 179 |
+
conditioner's — the two halves of one user's request then bill as one request, the way they would if this were a
|
| 180 |
+
single Space.
|
| 181 |
|
| 182 |
+
Without it the conditioner falls back to an IP-based quota, whose ceiling is low enough that an `xlarge` booking
|
| 183 |
+
is refused outright ("The requested GPU duration (Ns) is larger than the maximum allowed"), so a call that does
|
| 184 |
+
not forward a token only works because the conditioner keeps its own reservation small.
|
| 185 |
|
| 186 |
+
Cached per token: building a `Client` costs a round trip to the Space config, and a token is per user session.
|
| 187 |
+
"""
|
| 188 |
+
from gradio_client import Client
|
| 189 |
+
|
| 190 |
+
if ip_token in CLIENTS:
|
| 191 |
+
return CLIENTS[ip_token]
|
| 192 |
+
# No org token: the request runs on the caller side quota, which is the point of forwarding theirs.
|
| 193 |
+
client = Client(CONDITIONER_SPACE, headers={"X-IP-Token": ip_token} if ip_token else None)
|
| 194 |
+
if len(CLIENTS) >= 32:
|
| 195 |
+
CLIENTS.pop(next(iter(CLIENTS)))
|
| 196 |
+
CLIENTS[ip_token] = client
|
| 197 |
+
return client
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def ip_token_of(request) -> str | None:
|
| 201 |
+
"""The caller's ZeroGPU identity, as the Spaces router put it on this request.
|
| 202 |
+
|
| 203 |
+
Present on a browser request and on an API request the router could attribute; absent for a truly anonymous
|
| 204 |
+
caller, which then falls back to the conditioner's IP-based quota. Both the UI path and the `/generate` API path
|
| 205 |
+
reach this through the same `gr.Request` gradio injects for a parameter annotated with it.
|
| 206 |
+
"""
|
| 207 |
+
headers = getattr(request, "headers", None)
|
| 208 |
+
token = None if headers is None else headers.get("x-ip-token")
|
| 209 |
+
print(f"[gen] conditioner call {'forwards the caller ZeroGPU token' if token else 'is anonymous (IP quota)'}", flush=True)
|
| 210 |
+
return token
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def encode_remote(prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=False, ip_token=None):
|
| 214 |
"""Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
|
| 215 |
|
| 216 |
`rewrite_prompt` is the conditioner's prompt upsampling: it rewrites the request into MiniMax-H3's trained format
|
|
|
|
| 221 |
from gradio_client import handle_file
|
| 222 |
from safetensors import safe_open
|
| 223 |
|
| 224 |
+
path, plan = conditioner(ip_token).predict(
|
| 225 |
prompt=prompt,
|
| 226 |
image_path=handle_file(image_path) if image_path else None,
|
| 227 |
last_image_path=handle_file(last_image_path) if last_image_path else None,
|
|
|
|
| 285 |
return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
|
| 286 |
|
| 287 |
|
| 288 |
+
def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS, duration=5, steps=28, seed=42, upsample=False, progress=gr.Progress(track_tqdm=True), request: gr.Request | None = None):
|
| 289 |
"""One request. `upsample` is appended last and defaults off, so an existing API client is untouched by it."""
|
| 290 |
if LOAD_ERROR:
|
| 291 |
raise gr.Error(LOAD_ERROR)
|
|
|
|
| 303 |
progress(0.0, desc=f"Upsampling the prompt on {CONDITIONER_SPACE} ..." if upsample else f"Conditioning on {CONDITIONER_SPACE} ...")
|
| 304 |
conditioned = time.time()
|
| 305 |
prompt_embeds, text_token_tags, metadata, plan = encode_remote(
|
| 306 |
+
prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=upsample,
|
| 307 |
+
ip_token=ip_token_of(request),
|
| 308 |
)
|
| 309 |
condition_seconds = time.time() - conditioned
|
| 310 |
height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames"))
|