flozi00 commited on
Commit
de2dd32
·
verified ·
1 Parent(s): a186636

zero-gpu: size the token budget to the measured decode rate

Browse files
BUILD.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
- "built_at": "2026-10-29T17:07:28",
3
- "commit": "1be6abe",
4
  "files": 53,
5
  "with_store": false,
6
  "with_card": true
 
1
  {
2
+ "built_at": "2026-10-29T17:14:25",
3
+ "commit": "b187ff2",
4
  "files": 53,
5
  "with_store": false,
6
  "with_card": true
traceforge/space/local_llm.py CHANGED
@@ -255,9 +255,10 @@ def _fused_moe_forward(self, hidden_states, top_k_index, top_k_weights):
255
  transformers' Lfm2MoeExperts.forward loops over the experts that fired and
256
  calls .nonzero(), torch.where() and parameter indexing inside that loop.
257
  Every one of those is a device-to-host sync, so a single decoded token
258
- costs ~6 syncs per expert per layer -- ~480 for this 40-layer top-2 model.
259
- On a ZeroGPU container, whose CPU share is small, that is the decode
260
- ceiling, not the GPU: 16 tok/s measured on a full RTX PRO 6000 Blackwell.
 
261
 
262
  For a handful of tokens we gather the selected experts' weights and run two
263
  batched GEMMs instead, which needs no sync at all. Prefill keeps the
@@ -386,6 +387,12 @@ def generate(messages: list[dict], *, max_tokens: int = 512,
386
  enc = tok(prompt, return_tensors="pt", truncation=True,
387
  max_length=LOCAL_MAX_INPUT_TOKENS).to(LOCAL_DEVICE)
388
  n = max(1, min(int(max_tokens or 512), LOCAL_MAX_NEW_TOKENS))
 
 
 
 
 
 
389
  t0 = time.time()
390
  with torch.inference_mode():
391
  out = model.generate(
 
255
  transformers' Lfm2MoeExperts.forward loops over the experts that fired and
256
  calls .nonzero(), torch.where() and parameter indexing inside that loop.
257
  Every one of those is a device-to-host sync, so a single decoded token
258
+ costs ~6 syncs per expert per layer -- ~960 for this 40-layer top-4 model
259
+ (64 experts, 4 per token). At the ~60 us a sync costs on a ZeroGPU
260
+ container that is 58 ms per token, i.e. the 16 tok/s we measured on a full
261
+ RTX PRO 6000 Blackwell, where the weights alone allow hundreds.
262
 
263
  For a handful of tokens we gather the selected experts' weights and run two
264
  batched GEMMs instead, which needs no sync at all. Prefill keeps the
 
387
  enc = tok(prompt, return_tensors="pt", truncation=True,
388
  max_length=LOCAL_MAX_INPUT_TOKENS).to(LOCAL_DEVICE)
389
  n = max(1, min(int(max_tokens or 512), LOCAL_MAX_NEW_TOKENS))
390
+ # Size the budget to what this slice can actually decode. The first call
391
+ # measures tok/s; later calls trim the ceiling to ~60 s of generation so a
392
+ # slow slice fails fast instead of burning the visitor's whole quota.
393
+ measured = float(LAST_GEN.get("tok_s") or 0)
394
+ if measured > 0:
395
+ n = max(64, min(n, int(measured * 60)))
396
  t0 = time.time()
397
  with torch.inference_mode():
398
  out = model.generate(
traceforge/space/pipeline.py CHANGED
@@ -88,7 +88,9 @@ def space_config(data_dir: Optional[str] = None, **overrides: Any) -> Config:
88
  cfg.triage_max_chars = cfg.splice_max_chars
89
  # Labels are compact JSON, but LFM2.5 always thinks first and ignores
90
  # a pre-closed think block: 600 tokens only ever bought reasoning.
91
- cfg.triage_max_tokens = _env_int("TRACEFORGE_LOCAL_TRIAGE_TOKENS", 1200)
 
 
92
  cfg.rewrite_max_tokens = _env_int("TRACEFORGE_LOCAL_REWRITE_TOKENS",
93
  1000)
94
  cfg.judge_max_tokens = _env_int("TRACEFORGE_LOCAL_JUDGE_TOKENS", 1200)
 
88
  cfg.triage_max_chars = cfg.splice_max_chars
89
  # Labels are compact JSON, but LFM2.5 always thinks first and ignores
90
  # a pre-closed think block: 600 tokens only ever bought reasoning.
91
+ # The ceiling is generous because generate() trims it to what the
92
+ # measured decode rate can actually deliver inside one slice.
93
+ cfg.triage_max_tokens = _env_int("TRACEFORGE_LOCAL_TRIAGE_TOKENS", 4000)
94
  cfg.rewrite_max_tokens = _env_int("TRACEFORGE_LOCAL_REWRITE_TOKENS",
95
  1000)
96
  cfg.judge_max_tokens = _env_int("TRACEFORGE_LOCAL_JUDGE_TOKENS", 1200)