Budlee commited on
Commit
e4e5d2c
·
verified ·
1 Parent(s): 233354f

Hog GPU once acquired: duration=120, up to 12 sub-drains, 100s deadline

Browse files
Files changed (2) hide show
  1. app.py +48 -10
  2. docs/blog.md +12 -7
app.py CHANGED
@@ -207,15 +207,49 @@ def world_snapshot() -> dict:
207
  return result
208
 
209
 
210
- @GPU(duration=120)
211
- def _drain_gpu() -> int:
212
- """The hot path: drain inside a ZeroGPU fork. RealLLMBackend loads each
213
- fork's Llama with n_gpu_layers=-1. Reservation is 120s to give headroom
214
- over the observed ~100s worst-case drain (3 chars × ~30s + per-fork
215
- cold-load). Smaller batches (DECISIONS_PER_DRAIN=2) keep individual
216
- drains under this comfortably.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  """
218
- return TOWNLET.drain_decisions()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
 
221
  def _drain_cpu() -> int:
@@ -242,15 +276,19 @@ def tick_decisions_api() -> dict:
242
  falls through to on that path).
243
  """
244
  _log_req("tick_decisions")
 
245
  try:
246
- n = _drain_gpu()
 
 
247
  mode = "gpu"
 
248
  except BaseException as e:
249
  msg = f"{type(e).__name__}: {e}"
250
  tlog(f"[townlet] GPU drain failed ({msg[:120]}); CPU fallback engaged")
251
  n = _drain_cpu()
252
  mode = "cpu"
253
- result = {"decisions_run": n, "mode": mode}
254
  _log_resp("tick_decisions", result)
255
  return result
256
 
 
207
  return result
208
 
209
 
210
+ # GPU "hog mode": once we've been granted a slot, the cold-load cost
211
+ # (~15-20s for Llama.from_pretrained inside the fork) is sunk — every
212
+ # subsequent drain in the same fork reuses the already-loaded Llama via
213
+ # backend.factory._llms. So instead of running ONE drain per @spaces.GPU
214
+ # call and giving the slot back, we run multiple drains back-to-back until
215
+ # we approach the time budget or run out of pending decisions.
216
+ #
217
+ # duration=120 chosen to maximise work-per-acquisition. Per-call quota
218
+ # burn is the same whether the fork does 2 drains or 12 drains (we use
219
+ # real wall time, not the declared duration). What we want is high
220
+ # decisions-per-fork so that even when the allocator denies us 1 in N
221
+ # calls, the calls that ARE granted produce a lot of visible action.
222
+ # We are NOT fanning out concurrent forks — that multiplies cold-load
223
+ # and quota burn linearly while making us a "noisy neighbour" to the
224
+ # allocator. One fat fork strictly beats N thin forks.
225
+ GPU_DURATION = 120
226
+ _GPU_DEADLINE_S = 100.0 # stop spawning new sub-drains after this elapses
227
+ _MAX_SUB_DRAINS = 12 # safety cap in case sub-drains run very fast
228
+
229
+
230
+ @GPU(duration=GPU_DURATION)
231
+ def _drain_gpu() -> dict:
232
+ """Hog the GPU once granted. Runs drain_decisions() in a loop inside
233
+ the same fork until time is up or no pending characters remain. Returns
234
+ a dict with total decisions_run + number of sub-drains performed so the
235
+ caller can log it.
236
  """
237
+ import time
238
+ started = time.monotonic()
239
+ total = 0
240
+ sub_drains = 0
241
+ while sub_drains < _MAX_SUB_DRAINS:
242
+ elapsed = time.monotonic() - started
243
+ if elapsed > _GPU_DEADLINE_S:
244
+ break
245
+ n = TOWNLET.drain_decisions()
246
+ sub_drains += 1
247
+ total += n
248
+ if n == 0:
249
+ # No pending decisions queued. tick_world inside drain_decisions
250
+ # already advanced the clock; idling further would just spin.
251
+ break
252
+ return {"decisions_run": total, "sub_drains": sub_drains}
253
 
254
 
255
  def _drain_cpu() -> int:
 
276
  falls through to on that path).
277
  """
278
  _log_req("tick_decisions")
279
+ sub_drains = 1 # CPU path is always one sub-drain
280
  try:
281
+ gpu_result = _drain_gpu()
282
+ n = gpu_result["decisions_run"]
283
+ sub_drains = gpu_result["sub_drains"]
284
  mode = "gpu"
285
+ tlog(f"[townlet] GPU drain hog: {sub_drains} sub-drain(s), {n} decisions")
286
  except BaseException as e:
287
  msg = f"{type(e).__name__}: {e}"
288
  tlog(f"[townlet] GPU drain failed ({msg[:120]}); CPU fallback engaged")
289
  n = _drain_cpu()
290
  mode = "cpu"
291
+ result = {"decisions_run": n, "mode": mode, "sub_drains": sub_drains}
292
  _log_resp("tick_decisions", result)
293
  return result
294
 
docs/blog.md CHANGED
@@ -1,9 +1,9 @@
1
  # LinkedIn
2
 
3
  I have been taking part in the @HuggingFace hackathon for Build Small.
4
- It has been a while since I have used smaller models, I just select Opus and let it rip.
5
 
6
- However, this has properly taken me back to the earlier days of AI when I was using smaller models with Langchain to try and get a weak Llama 3:8b to do tool calls.
7
 
8
  In this hackathon there were 2 tracks, something useful and something fun.
9
  I chose something fun and its a bit different to what others are doing.
@@ -11,17 +11,22 @@ I chose something fun and its a bit different to what others are doing.
11
  I have created a little town of people 'Townlet' and townlet has a bunch of characters, they draw water, draw electricity and then have access to two items.
12
 
13
  * A message board for broadcasting messages to all players
14
- * A python shell
15
 
16
- Each character is given a personality and generates a stream of conciousness of there actions to reach their goal.
17
 
18
  I gave a lock on the Python shell so only one character at a time could access it.
19
 
20
  The idea is that all the files related to a character (their state files) are available in the shell.
21
- So instead of minning a character could just update their state file, or delete another characters state file, removing them from the game.
22
 
23
- With different archytypes I wanted to see what would happen, would there be contention and how would they behave.
24
 
25
- Each chacter I hoped would use the shell to make something interesting or potentially be destructive.
 
 
 
 
 
26
 
27
  THe
 
1
  # LinkedIn
2
 
3
  I have been taking part in the @HuggingFace hackathon for Build Small.
4
+ It has been a while since I have used smaller models, I always just select Opus and let it rip.
5
 
6
+ However, this has properly taken me back to the earlier days of AI when I was using smaller models locally with Langchain to try and get a weak Llama 3:8b to do tool calls, suffice to say it was not fruitful.
7
 
8
  In this hackathon there were 2 tracks, something useful and something fun.
9
  I chose something fun and its a bit different to what others are doing.
 
11
  I have created a little town of people 'Townlet' and townlet has a bunch of characters, they draw water, draw electricity and then have access to two items.
12
 
13
  * A message board for broadcasting messages to all players
14
+ * A python shell, which costs one electricity and one water to use
15
 
16
+ Each character is given a personality and generates a stream of conciousness of actions they should perform to reach their goal.
17
 
18
  I gave a lock on the Python shell so only one character at a time could access it.
19
 
20
  The idea is that all the files related to a character (their state files) are available in the shell.
21
+ So instead of mining a character could just update their state file, or delete another characters state file, removing them from the game.
22
 
23
+ With different archetypes I wanted to see what would happen, would there be contention and how would they behave.
24
 
25
+ Each character I hoped would use the shell to make something interesting or potentially be destructive.
26
+ The end result, I think showed two things.
27
+
28
+ - The characters and limits did not give me the results, visually I had hoped for, they did use the shell and message board which was interesting to see.
29
+ - The charactes did not evolve as I had hoped. I did include a dream feature that periodically should have shiffted behaviour, but it did not do enough.
30
+ -
31
 
32
  THe