multimodalart HF Staff commited on
Commit
bf1199a
·
verified ·
1 Parent(s): 9ac597f

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 +17 -31
README.md CHANGED
@@ -160,22 +160,19 @@ one-time `PIPE.to("cuda")` is inside the first row's 339 s and does not reappear
160
 
161
  ## Whose GPU quota pays
162
 
163
- Two cards are booked per request: this Space's denoise loop and the conditioner's forward, and **both are meant to be
164
- billed to the requesting user**. ZeroGPU attributes a booking to the `X-IP-Token` of the request that triggered it —
165
- its `/schedule` hands that token to the Spaces API together with the duration and the calling pod's IP, and nothing
166
- else, so there is no Space identity in the decision and a valid token minted anywhere is honoured. This Space
167
- therefore forwards the caller's header to the conditioner
168
- (`gradio_client.Client(..., headers={"X-IP-Token": ...})`, off the `gr.Request` gradio injects on
169
- the UI path and the `/generate` API path alike) rather than spending a token of its own.
170
-
171
- A token ZeroGPU refuses it answers `401`, which `spaces` surfaces as `Expired ZeroGPU proxy token` falls back to
172
- calling the conditioner with no token at all, billed to this Space's pod IP off a small shared quota. That is a safety
173
- net rather than the intended path, and the log line `conditioner call paid for by ...` records which identity actually
174
- paid, next to a decoded dump of what the incoming token claimed.
175
-
176
- The conditioner is sized so that even the fallback is legal: an unattributed caller may book at most 120 credits at a
177
- time and an `xlarge` booking costs **twice** its seconds (`_gpu_size_units`), so the conditioner books the encode
178
- (45 s) and a prompt upsample (60 s) as **two separate calls**, where one combined booking would be refused outright.
179
 
180
  ## Secrets
181
 
 
160
 
161
  ## Whose GPU quota pays
162
 
163
+ Two cards are booked per request this Space's denoise loop and the conditioner's forward and both are billed to the
164
+ **requesting user**, with nothing in this repository arranging it. `gradio_client` attaches the caller's own
165
+ `x-ip-token` to every outgoing call by itself, reading it off gradio's `LocalContext` inside the event listener
166
+ (`Client.send_data` -> `add_zero_gpu_headers`), and ZeroGPU's `/schedule` charges the booking to whatever that token
167
+ identifies. Forwarding the header by hand is not needed and is actively worse: a cached `Client` would pin one stale
168
+ token, which ZeroGPU refuses with `Expired ZeroGPU proxy token`.
169
+
170
+ A caller with no token to forward — a `gradio_client` script rather than a browser — leaves the conditioner's booking
171
+ attributed to this Space's pod IP and its small shared quota. That path is why the conditioner books small: an
172
+ unattributed caller may book at most 120 credits at a time and an `xlarge` booking costs **twice** its seconds
173
+ (`_gpu_size_units`), so the conditioner books the encode (45 s) and a prompt upsample (60 s) as two separate calls,
174
+ where one combined booking of the old 300 s would be — and was — refused outright with `The requested GPU duration
175
+ (600s) is larger than the maximum allowed`.
 
 
 
176
 
177
  ## Secrets
178
 
app.py CHANGED
@@ -65,8 +65,7 @@ PIPE = None
65
  MANAGER = None
66
  LOAD_ERROR: str | None = None
67
  LOADED_IN: float | None = None
68
- # One `gradio_client.Client` per forwarded token; see `conditioner`.
69
- CLIENTS: dict[str | None, object] = {}
70
 
71
 
72
  def status() -> str:
@@ -171,35 +170,23 @@ def _arm_decode_hooks(pipe):
171
  module.decode = armed
172
 
173
 
174
- def conditioner(ip_token: str | None = None):
175
- """The other half, over the gradio API, billed to the requesting user.
176
 
177
- ZeroGPU attributes a booking to the `X-IP-Token` of the request that triggered it: `/schedule` hands that token to
178
- the Spaces API together with the duration and the calling pod's IP and nothing else — there is no Space identity in
179
- the decision so forwarding the caller's header makes the user's own quota pay for both halves of their request,
180
- the way it would if this were a single Space.
181
-
182
- Cached per token: building a `Client` costs a round trip to the Space config, and a token is per user session.
183
  """
184
- from gradio_client import Client
185
-
186
- if ip_token not in CLIENTS:
187
- if len(CLIENTS) >= 32:
188
- CLIENTS.pop(next(iter(CLIENTS)))
189
- CLIENTS[ip_token] = Client(CONDITIONER_SPACE, headers={"X-IP-Token": ip_token} if ip_token else None)
190
- return CLIENTS[ip_token]
191
-
192
 
193
- def ip_token_of(request) -> str | None:
194
- """The requesting user's ZeroGPU identity, as the Spaces router put it on this request.
195
-
196
- The UI path and the `/generate` API path both reach this through the `gr.Request` gradio injects for a parameter
197
- annotated with it.
198
- """
199
- headers = getattr(request, "headers", None)
200
- return None if headers is None else headers.get("x-ip-token")
201
 
202
- def encode_remote(prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=False, ip_token=None):
203
  """Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
204
 
205
  `rewrite_prompt` is the conditioner's prompt upsampling: it rewrites the request into MiniMax-H3's trained format
@@ -210,7 +197,7 @@ def encode_remote(prompt, image_path, last_image_path, canvas, num_frames, rewri
210
  from gradio_client import handle_file
211
  from safetensors import safe_open
212
 
213
- path, plan = conditioner(ip_token).predict(
214
  prompt=prompt,
215
  image_path=handle_file(image_path) if image_path else None,
216
  last_image_path=handle_file(last_image_path) if last_image_path else None,
@@ -274,7 +261,7 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
274
  return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
275
 
276
 
277
- 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):
278
  """One request. `upsample` is appended last and defaults off, so an existing API client is untouched by it."""
279
  if LOAD_ERROR:
280
  raise gr.Error(LOAD_ERROR)
@@ -292,8 +279,7 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
292
  progress(0.0, desc=f"Upsampling the prompt on {CONDITIONER_SPACE} ..." if upsample else f"Conditioning on {CONDITIONER_SPACE} ...")
293
  conditioned = time.time()
294
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
295
- prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=upsample,
296
- ip_token=ip_token_of(request),
297
  )
298
  condition_seconds = time.time() - conditioned
299
  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
+ CLIENT = None
 
69
 
70
 
71
  def status() -> str:
 
170
  module.decode = armed
171
 
172
 
173
+ def conditioner():
174
+ """The other half, over the gradio API. Cached — building a `Client` costs a round trip to the Space config.
175
 
176
+ No token is passed and none has to be: `gradio_client` attaches the caller's own ZeroGPU token itself, per call,
177
+ by reading the `x-ip-token` of the request being served off gradio's `LocalContext` (`Client.send_data` ->
178
+ `add_zero_gpu_headers`). So calling this from inside an event listener which is the only place it is called
179
+ bills the conditioner's booking to the user who asked for the video, exactly as this Space's own booking is, and
180
+ forwarding the header by hand would only pin a stale token onto a cached client.
 
181
  """
182
+ global CLIENT
183
+ if CLIENT is None:
184
+ from gradio_client import Client
 
 
 
 
 
185
 
186
+ CLIENT = Client(CONDITIONER_SPACE)
187
+ return CLIENT
 
 
 
 
 
 
188
 
189
+ def encode_remote(prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=False):
190
  """Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
191
 
192
  `rewrite_prompt` is the conditioner's prompt upsampling: it rewrites the request into MiniMax-H3's trained format
 
197
  from gradio_client import handle_file
198
  from safetensors import safe_open
199
 
200
+ path, plan = conditioner().predict(
201
  prompt=prompt,
202
  image_path=handle_file(image_path) if image_path else None,
203
  last_image_path=handle_file(last_image_path) if last_image_path else None,
 
261
  return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
262
 
263
 
264
+ 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)):
265
  """One request. `upsample` is appended last and defaults off, so an existing API client is untouched by it."""
266
  if LOAD_ERROR:
267
  raise gr.Error(LOAD_ERROR)
 
279
  progress(0.0, desc=f"Upsampling the prompt on {CONDITIONER_SPACE} ..." if upsample else f"Conditioning on {CONDITIONER_SPACE} ...")
280
  conditioned = time.time()
281
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
282
+ prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=upsample
 
283
  )
284
  condition_seconds = time.time() - conditioned
285
  height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames"))