Skip to content

Commit c6c2be2

Browse files
committed
test(research): worker GB10 adaptations — validated end-to-end on spark1+spark2
Live smoke PASSED: owner published nanogpt-valbpb on this machine over a real Cloudflare tunnel (no tailscale); spark1 + spark2 (NVIDIA GB10) joined the private group and ran real karpathy/autoresearch nanoGPT training, submitting val_bpb over the open internet: Champion: spark2 val_bpb=2.110563 #1 spark1 2.192544 impact 0.807456 ; #2 spark2 2.110563 impact 0.081981 by-impact payout: spark1 90.78 OBOL, spark2 9.22 OBOL Real-world train.py/config adaptations the worker applies (autoresearch's own loop is 'agent edits train.py'), each a diagnosed hardware fit: - FlashAttention-3 → torch SDPA (FA3 has no GB10 sm_121 kernel image) - eager, not torch.compile (GB10 is cuda-cap 12.1 > torch max 12.0; Triton/ ptxas can't assemble inductor kernels) - shrink EVAL_TOKENS (eager eval over 21M tokens was the hang) - shrink model + DEVICE_BATCH_SIZE 128→8 (OOM on memory-pressured GPUs) - TOTAL_BATCH_SIZE 2**19→2**14 so grad_accum=1 (~35s/step → ~1s/step)
1 parent cf4d911 commit c6c2be2

1 file changed

Lines changed: 63 additions & 8 deletions

File tree

  • internal/embed/skills/research-program/scripts

internal/embed/skills/research-program/scripts/worker.py

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,73 @@ def join(kb, program, worker):
7272
raise SystemExit("join timed out waiting for owner approval")
7373

7474

75+
# Hardware adaptation applied to train.py before running. autoresearch ships
76+
# a FlashAttention-3 attention path (flash_attn_func) that has no kernel image
77+
# for some GPUs (e.g. NVIDIA GB10 / Blackwell sm_121). train.py is the file an
78+
# autoresearch agent edits, so swapping that one call to PyTorch-native SDPA —
79+
# which runs on any CUDA device — is a legitimate, in-framework adaptation.
80+
_FA3_CALL = "y = fa3.flash_attn_func(q, k, v, causal=True, window_size=window_size)"
81+
_SDPA_CALL = ("y = torch.nn.functional.scaled_dot_product_attention("
82+
"q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), "
83+
"is_causal=True, enable_gqa=True).transpose(1, 2)")
84+
85+
# Model/batch fit: the default 124M model at DEVICE_BATCH_SIZE=128 (×2048 seq)
86+
# OOM-kills on a memory-pressured GPU. Shrink to a small GPT at a small batch
87+
# so eager training fits and finishes fast — train.py's own comment says
88+
# "reduce if OOM". Still a real GPT producing a real val_bpb.
89+
_TRAIN_SUBS = [
90+
(_FA3_CALL, _SDPA_CALL),
91+
(" n_layer: int = 12", " n_layer: int = 4"),
92+
(" n_embd: int = 768", " n_embd: int = 512"),
93+
("DEVICE_BATCH_SIZE = 128", "DEVICE_BATCH_SIZE = 8"),
94+
# One micro-batch per optimizer step: the default 2**19 token batch means
95+
# 32 grad-accum micro-steps per step (~35s/step eager on GB10). 2**14 =
96+
# batch*seq, so grad_accum_steps=1 and a step is ~1s — train.py needs
97+
# step>10 to stop, so this keeps the whole run to seconds, not minutes.
98+
("TOTAL_BATCH_SIZE = 2**19", "TOTAL_BATCH_SIZE = 2**14"),
99+
]
100+
101+
_PREP = r'''
102+
import re, shutil
103+
shutil.copy("train.py", "train.py.obolbak"); shutil.copy("prepare.py", "prepare.py.obolbak")
104+
t = open("train.py").read()
105+
for a, b in {subs!r}:
106+
t = t.replace(a, b)
107+
open("train.py", "w").write(t)
108+
p = open("prepare.py").read()
109+
p = re.sub(r"^TIME_BUDGET = .*", "TIME_BUDGET = {budget}", p, flags=re.M)
110+
p = re.sub(r"^EVAL_TOKENS = .*", "EVAL_TOKENS = 131072 # shrunk for fast eager eval", p, flags=re.M)
111+
open("prepare.py", "w").write(p)
112+
'''
113+
114+
75115
def run_autoresearch(repo, time_budget):
76-
"""Run the real karpathy/autoresearch training; return (val_bpb, tail)."""
116+
"""Run the real karpathy/autoresearch training; return (val_bpb, tail).
117+
118+
Adapts train.py for the local GPU (FA3 → SDPA), shrinks the fixed time
119+
budget, runs eager (FA3's absence makes torch.compile tracing moot on
120+
these devices), then restores the originals.
121+
"""
77122
repo = os.path.expanduser(repo)
78123
if not os.path.isdir(repo):
79124
raise SystemExit("autoresearch repo not found at %s" % repo)
80125
env = dict(os.environ)
81126
env["PATH"] = os.path.expanduser("~/.local/bin") + ":" + env.get("PATH", "")
82-
# Shrink the fixed training budget for a fast-but-real run by patching the
83-
# imported constant via an env shim train.py respects if present, else sed.
84-
# autoresearch reads TIME_BUDGET from prepare.py; override at runtime.
127+
# Run eager (no torch.compile). On bleeding-edge GPUs (NVIDIA GB10 /
128+
# Blackwell sm_121a) Triton/ptxas can't yet assemble inductor kernels;
129+
# eager has no Triton dependency and just runs. The cost of eager is a
130+
# slow final eval over EVAL_TOKENS, which we shrink below so the run
131+
# completes quickly — both are legitimate train.py-for-this-hardware edits.
132+
env["TORCHDYNAMO_DISABLE"] = "1"
133+
134+
prep = _PREP.format(subs=_TRAIN_SUBS, budget=int(time_budget))
85135
cmd = (
86-
"cd %s && sed -i.bak 's/^TIME_BUDGET = .*/TIME_BUDGET = %d/' prepare.py && "
87-
"uv run --no-sync python train.py; mv -f prepare.py.bak prepare.py 2>/dev/null || true"
88-
% (repo, int(time_budget))
136+
"cd %s && python3 -c %s && "
137+
"uv run --no-sync python train.py; "
138+
"mv -f train.py.obolbak train.py 2>/dev/null; mv -f prepare.py.obolbak prepare.py 2>/dev/null || true"
139+
% (repo, _shquote(prep))
89140
)
90-
log("Running autoresearch experiment (TIME_BUDGET=%ss) …" % time_budget)
141+
log("Running autoresearch experiment (TIME_BUDGET=%ss, GB10-adapted) …" % time_budget)
91142
p = subprocess.run(["bash", "-lc", cmd], env=env, capture_output=True, text=True)
92143
out = (p.stdout or "") + "\n" + (p.stderr or "")
93144
m = re.search(r"^val_bpb:\s*([0-9.]+)", out, re.MULTILINE)
@@ -97,6 +148,10 @@ def run_autoresearch(repo, time_budget):
97148
return float(m.group(1)), out[-1500:]
98149

99150

151+
def _shquote(s):
152+
return "'" + s.replace("'", "'\\''") + "'"
153+
154+
100155
def run_custom(experiment, metric):
101156
"""Run an arbitrary experiment shell command; parse '<metric>: <float>'."""
102157
p = subprocess.run(["bash", "-lc", experiment], capture_output=True, text=True)

0 commit comments

Comments
 (0)