Hog GPU once acquired: duration=120, up to 12 sub-drains, 100s deadline
Browse files- app.py +48 -10
- docs/blog.md +12 -7
app.py
CHANGED
|
@@ -207,15 +207,49 @@ def world_snapshot() -> dict:
|
|
| 207 |
return result
|
| 208 |
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
"""
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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
|
| 22 |
|
| 23 |
-
With different
|
| 24 |
|
| 25 |
-
Each
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|