multimodalart HF Staff commited on
Commit
b2bc645
·
verified ·
1 Parent(s): afea884

Let gradio_client forward the caller's ZeroGPU token itself instead of pinning one by hand

Browse files
Files changed (2) hide show
  1. README.md +13 -16
  2. app.py +16 -30
README.md CHANGED
@@ -183,22 +183,19 @@ on is cold and a cold one pays the lazy 72.16 GiB `PIPE.to("cuda")` inside its f
183
 
184
  ## Whose GPU quota pays
185
 
186
- Two cards are booked per request: this Space's denoise loop and the conditioner's forward, and **both are meant to be
187
- billed to the requesting user**. ZeroGPU attributes a booking to the `X-IP-Token` of the request that triggered it —
188
- its `/schedule` hands that token to the Spaces API together with the duration and the calling pod's IP, and nothing
189
- else, so there is no Space identity in the decision and a valid token minted anywhere is honoured. This Space
190
- therefore forwards the caller's header to the conditioner
191
- (`gradio_client.Client(..., headers={"X-IP-Token": ...})`, off the `gr.Request` gradio injects on
192
- the UI path and the `/generate` API path alike) rather than spending a token of its own.
193
-
194
- A token ZeroGPU refuses it answers `401`, which `spaces` surfaces as `Expired ZeroGPU proxy token` falls back to
195
- calling the conditioner with no token at all, billed to this Space's pod IP off a small shared quota. That is a safety
196
- net rather than the intended path, and the log line `conditioner call paid for by ...` records which identity actually
197
- paid, next to a decoded dump of what the incoming token claimed.
198
-
199
- The conditioner is sized so that even the fallback is legal: an unattributed caller may book at most 120 credits at a
200
- time and an `xlarge` booking costs **twice** its seconds (`_gpu_size_units`), so the conditioner books the encode
201
- (45 s) and a prompt upsample (60 s) as **two separate calls**, where one combined booking would be refused outright.
202
 
203
  ## Secrets
204
 
 
183
 
184
  ## Whose GPU quota pays
185
 
186
+ Two cards are booked per request this Space's denoise loop and the conditioner's forward and both are billed to the
187
+ **requesting user**, with nothing in this repository arranging it. `gradio_client` attaches the caller's own
188
+ `x-ip-token` to every outgoing call by itself, reading it off gradio's `LocalContext` inside the event listener
189
+ (`Client.send_data` -> `add_zero_gpu_headers`), and ZeroGPU's `/schedule` charges the booking to whatever that token
190
+ identifies. Forwarding the header by hand is not needed and is actively worse: a cached `Client` would pin one stale
191
+ token, which ZeroGPU refuses with `Expired ZeroGPU proxy token`.
192
+
193
+ A caller with no token to forward — a `gradio_client` script rather than a browser — leaves the conditioner's booking
194
+ attributed to this Space's pod IP and its small shared quota. That path is why the conditioner books small: an
195
+ unattributed caller may book at most 120 credits at a time and an `xlarge` booking costs **twice** its seconds
196
+ (`_gpu_size_units`), so the conditioner books the encode (45 s) and a prompt upsample (60 s) as two separate calls,
197
+ where one combined booking of the old 300 s would be — and was — refused outright with `The requested GPU duration
198
+ (600s) is larger than the maximum allowed`.
 
 
 
199
 
200
  ## Secrets
201
 
app.py CHANGED
@@ -200,8 +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
- # One `gradio_client.Client` per forwarded token; see `conditioner`.
204
- CLIENTS: dict[str | None, object] = {}
205
 
206
 
207
  def load_models() -> str | None:
@@ -307,33 +306,21 @@ def _arm_decode_hooks(pipe):
307
  setattr(module, method, armed)
308
 
309
 
310
- def conditioner(ip_token: str | None = None):
311
- """The other half, over the gradio API, billed to the requesting user.
312
 
313
- ZeroGPU attributes a booking to the `X-IP-Token` of the request that triggered it: `/schedule` hands that token to
314
- the Spaces API together with the duration and the calling pod's IP and nothing else — there is no Space identity in
315
- the decision so forwarding the caller's header makes the user's own quota pay for both halves of their request,
316
- the way it would if this were a single Space.
317
-
318
- Cached per token: building a `Client` costs a round trip to the Space config, and a token is per user session.
319
  """
320
- from gradio_client import Client
321
-
322
- if ip_token not in CLIENTS:
323
- if len(CLIENTS) >= 32:
324
- CLIENTS.pop(next(iter(CLIENTS)))
325
- CLIENTS[ip_token] = Client(CONDITIONER_SPACE, headers={"X-IP-Token": ip_token} if ip_token else None)
326
- return CLIENTS[ip_token]
327
-
328
 
329
- def ip_token_of(request) -> str | None:
330
- """The requesting user's ZeroGPU identity, as the Spaces router put it on this request.
331
-
332
- The UI path and the `/generate` API path both reach this through the `gr.Request` gradio injects for a parameter
333
- annotated with it.
334
- """
335
- headers = getattr(request, "headers", None)
336
- return None if headers is None else headers.get("x-ip-token")
337
 
338
  def probe(path: str) -> tuple[float | None, float | None]:
339
  """`(video seconds, audio seconds)` of a media file, either being `None` when the stream is absent."""
@@ -431,7 +418,7 @@ def check(prompt: str, references: list[tuple[str, str]]) -> None:
431
  )
432
 
433
 
434
- def encode_remote(prompt, references, canvas, num_frames, rewrite_prompt=False, ip_token=None):
435
  """Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
436
 
437
  The references go over with the request: `ref2va`'s presentation puts a vision block in front of the prompt for
@@ -446,7 +433,7 @@ def encode_remote(prompt, references, canvas, num_frames, rewrite_prompt=False,
446
  from gradio_client import handle_file
447
  from safetensors import safe_open
448
 
449
- path, plan = conditioner(ip_token).predict(
450
  prompt=prompt,
451
  media=[handle_file(path) for _, path in references],
452
  kinds=",".join(kind for kind, _ in references),
@@ -513,7 +500,6 @@ def generate(
513
  seed=42,
514
  upsample=False,
515
  progress=gr.Progress(track_tqdm=True),
516
- request: gr.Request | None = None,
517
  ):
518
  """One request. `upsample` is appended last and defaults off, so an existing API client is untouched by it."""
519
  if LOAD_ERROR:
@@ -536,7 +522,7 @@ def generate(
536
  conditioned = time.time()
537
  try:
538
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
539
- prompt, references, canvas, requested, rewrite_prompt=upsample, ip_token=ip_token_of(request)
540
  )
541
  except gr.Error:
542
  raise
 
200
  PIPE = None
201
  MANAGER = None
202
  LOAD_ERROR: str | None = None
203
+ CLIENT = None
 
204
 
205
 
206
  def load_models() -> str | None:
 
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
 
312
+ No token is passed and none has to be: `gradio_client` attaches the caller's own ZeroGPU token itself, per call,
313
+ by reading the `x-ip-token` of the request being served off gradio's `LocalContext` (`Client.send_data` ->
314
+ `add_zero_gpu_headers`). So calling this from inside an event listener which is the only place it is called
315
+ bills the conditioner's booking to the user who asked for the video, exactly as this Space's own booking is, and
316
+ forwarding the header by hand would only pin a stale token onto a cached client.
 
317
  """
318
+ global CLIENT
319
+ if CLIENT is None:
320
+ from gradio_client import Client
 
 
 
 
 
321
 
322
+ CLIENT = Client(CONDITIONER_SPACE)
323
+ return CLIENT
 
 
 
 
 
 
324
 
325
  def probe(path: str) -> tuple[float | None, float | None]:
326
  """`(video seconds, audio seconds)` of a media file, either being `None` when the stream is absent."""
 
418
  )
419
 
420
 
421
+ def encode_remote(prompt, references, canvas, num_frames, rewrite_prompt=False):
422
  """Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
423
 
424
  The references go over with the request: `ref2va`'s presentation puts a vision block in front of the prompt for
 
433
  from gradio_client import handle_file
434
  from safetensors import safe_open
435
 
436
+ path, plan = conditioner().predict(
437
  prompt=prompt,
438
  media=[handle_file(path) for _, path in references],
439
  kinds=",".join(kind for kind, _ in references),
 
500
  seed=42,
501
  upsample=False,
502
  progress=gr.Progress(track_tqdm=True),
 
503
  ):
504
  """One request. `upsample` is appended last and defaults off, so an existing API client is untouched by it."""
505
  if LOAD_ERROR:
 
522
  conditioned = time.time()
523
  try:
524
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
525
+ prompt, references, canvas, requested, rewrite_prompt=upsample
526
  )
527
  except gr.Error:
528
  raise