multimodalart HF Staff commited on
Commit
e76d22a
·
verified ·
1 Parent(s): 4218806

Forward the caller's ZeroGPU token to the conditioner so one request bills as one request

Browse files
Files changed (2) hide show
  1. README.md +13 -0
  2. app.py +42 -11
README.md CHANGED
@@ -181,6 +181,19 @@ on is cold and a cold one pays the lazy 72.16 GiB `PIPE.to("cuda")` inside its f
181
  | `H3_PLACEMENT_ALLOWANCE` | `90` | Seconds of the reservation set aside for a cold worker's placement. |
182
  | `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  ## Secrets
185
 
186
  Nothing this Space loads is private any more: the weights are the public
 
181
  | `H3_PLACEMENT_ALLOWANCE` | `90` | Seconds of the reservation set aside for a cold worker's placement. |
182
  | `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
183
 
184
+ ## Whose GPU quota pays
185
+
186
+ Two cards are booked per request: this Space's denoise loop and the conditioner's forward. ZeroGPU attributes a
187
+ booking to the `X-IP-Token` header of the request that triggered it, so this Space forwards the caller's token to the
188
+ conditioner (`gradio_client.Client(..., headers={"X-IP-Token": ...})`, from the `gr.Request` gradio injects — the UI
189
+ path and the `/generate` API path alike). One user's request then bills as one request across both halves, the way it
190
+ would if this were a single Space, and no org token is ever spent on it.
191
+
192
+ A caller the router cannot attribute — an unauthenticated API call — falls back to the conditioner's IP-based quota,
193
+ whose ceiling is 120 credits. An `xlarge` booking costs **twice** its seconds there, so the conditioner keeps its
194
+ reservation at 60 s (120 credits) for an encode; asking it to upsample a prompt books 120 s (240 credits) and needs a
195
+ forwarded token.
196
+
197
  ## Secrets
198
 
199
  Nothing this Space loads is private any more: the weights are the public
app.py CHANGED
@@ -200,7 +200,7 @@ def get_duration(prompt_embeds, text_token_tags, references, height, width, num_
200
  PIPE = None
201
  MANAGER = None
202
  LOAD_ERROR: str | None = None
203
- CLIENT = None
204
 
205
 
206
  def load_models() -> str | None:
@@ -306,14 +306,44 @@ def _arm_decode_hooks(pipe):
306
  setattr(module, method, armed)
307
 
308
 
309
- def conditioner():
310
- """The other half, over the gradio API. Cached building a `Client` costs a round trip to the Space config."""
311
- global CLIENT
312
- if CLIENT is None:
313
- from gradio_client import Client
314
 
315
- CLIENT = Client(CONDITIONER_SPACE) # public Space, no org token: the request runs on the caller side quota
316
- return CLIENT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
 
319
  def probe(path: str) -> tuple[float | None, float | None]:
@@ -412,7 +442,7 @@ def check(prompt: str, references: list[tuple[str, str]]) -> None:
412
  )
413
 
414
 
415
- def encode_remote(prompt, references, canvas, num_frames, rewrite_prompt=False):
416
  """Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
417
 
418
  The references go over with the request: `ref2va`'s presentation puts a vision block in front of the prompt for
@@ -427,7 +457,7 @@ def encode_remote(prompt, references, canvas, num_frames, rewrite_prompt=False):
427
  from gradio_client import handle_file
428
  from safetensors import safe_open
429
 
430
- path, plan = conditioner().predict(
431
  prompt=prompt,
432
  media=[handle_file(path) for _, path in references],
433
  kinds=",".join(kind for kind, _ in references),
@@ -494,6 +524,7 @@ def generate(
494
  seed=42,
495
  upsample=False,
496
  progress=gr.Progress(track_tqdm=True),
 
497
  ):
498
  """One request. `upsample` is appended last and defaults off, so an existing API client is untouched by it."""
499
  if LOAD_ERROR:
@@ -516,7 +547,7 @@ def generate(
516
  conditioned = time.time()
517
  try:
518
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
519
- prompt, references, canvas, requested, rewrite_prompt=upsample
520
  )
521
  except gr.Error:
522
  raise
 
200
  PIPE = None
201
  MANAGER = None
202
  LOAD_ERROR: str | None = None
203
+ CLIENTS: dict[str | None, object] = {}
204
 
205
 
206
  def load_models() -> str | None:
 
306
  setattr(module, method, armed)
307
 
308
 
309
+ def conditioner(ip_token: str | None = None):
310
+ """The other half, over the gradio API — booked against *the caller's* ZeroGPU quota, not this org's.
 
 
 
311
 
312
+ ZeroGPU attributes a booking to the `X-IP-Token` header of the request that triggered it
313
+ (`spaces/zero/client.py`), which the Spaces router puts on every browser request. That header is what pays for
314
+ this Space's own `@spaces.GPU` call, and forwarding it to the conditioner makes the same identity pay for the
315
+ conditioner's — the two halves of one user's request then bill as one request, the way they would if this were a
316
+ single Space.
317
+
318
+ Without it the conditioner falls back to an IP-based quota, whose ceiling is low enough that an `xlarge` booking
319
+ is refused outright ("The requested GPU duration (Ns) is larger than the maximum allowed"), so a call that does
320
+ not forward a token only works because the conditioner keeps its own reservation small.
321
+
322
+ Cached per token: building a `Client` costs a round trip to the Space config, and a token is per user session.
323
+ """
324
+ from gradio_client import Client
325
+
326
+ if ip_token in CLIENTS:
327
+ return CLIENTS[ip_token]
328
+ # No org token: the request runs on the caller side quota, which is the point of forwarding theirs.
329
+ client = Client(CONDITIONER_SPACE, headers={"X-IP-Token": ip_token} if ip_token else None)
330
+ if len(CLIENTS) >= 32:
331
+ CLIENTS.pop(next(iter(CLIENTS)))
332
+ CLIENTS[ip_token] = client
333
+ return client
334
+
335
+
336
+ def ip_token_of(request) -> str | None:
337
+ """The caller's ZeroGPU identity, as the Spaces router put it on this request.
338
+
339
+ Present on a browser request and on an API request the router could attribute; absent for a truly anonymous
340
+ caller, which then falls back to the conditioner's IP-based quota. Both the UI path and the `/generate` API path
341
+ reach this through the same `gr.Request` gradio injects for a parameter annotated with it.
342
+ """
343
+ headers = getattr(request, "headers", None)
344
+ token = None if headers is None else headers.get("x-ip-token")
345
+ print(f"[ref2va] conditioner call {'forwards the caller ZeroGPU token' if token else 'is anonymous (IP quota)'}", flush=True)
346
+ return token
347
 
348
 
349
  def probe(path: str) -> tuple[float | None, float | None]:
 
442
  )
443
 
444
 
445
+ def encode_remote(prompt, references, canvas, num_frames, rewrite_prompt=False, ip_token=None):
446
  """Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
447
 
448
  The references go over with the request: `ref2va`'s presentation puts a vision block in front of the prompt for
 
457
  from gradio_client import handle_file
458
  from safetensors import safe_open
459
 
460
+ path, plan = conditioner(ip_token).predict(
461
  prompt=prompt,
462
  media=[handle_file(path) for _, path in references],
463
  kinds=",".join(kind for kind, _ in references),
 
524
  seed=42,
525
  upsample=False,
526
  progress=gr.Progress(track_tqdm=True),
527
+ request: gr.Request | None = None,
528
  ):
529
  """One request. `upsample` is appended last and defaults off, so an existing API client is untouched by it."""
530
  if LOAD_ERROR:
 
547
  conditioned = time.time()
548
  try:
549
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
550
+ prompt, references, canvas, requested, rewrite_prompt=upsample, ip_token=ip_token_of(request)
551
  )
552
  except gr.Error:
553
  raise