Scrypt / finetune /RECIPE.md
IMJONEZZ's picture
SCRYPT: initial commit β€” game, sandbox, Warden, Space web layer
9fca766
|
Raw
History Blame Contribute Delete
5.27 kB

A newer version of the Gradio SDK is available: 6.19.0

Upgrade

Finetuning the Warden (Nemotron-3-Nano-30B-A3B, full-parameter SFT)

Hardware reality check (DGX Spark edition)

The Spark's 128GB unified memory is excellent news for this project β€” but full-parameter finetuning is bounded by training state, not by inference memory, and an H100 comparison is about bandwidth, not capacity:

what memory
BF16 weights (30B params) ~60 GB
BF16 gradients ~60 GB
Adam moments + FP32 master weights (mixed-precision default) ~360 GB
activations (seq 4K, micro-batch 1, with recompute) tens of GB
total, conventional full SFT ~480+ GB

That is an aggregate number β€” a single 80GB H100 can't full-finetune this model either. NVIDIA's own recipe (Megatron Bridge, nemotron3_nano SFT) prescribes 2 nodes Γ— 8 H100 (1.28TB aggregate) with TP=1, EP=8.

Fitting full SFT on the 2-Spark cluster (256GB aggregate)

The 480GB figure is the CONVENTIONAL recipe. Every line is negotiable:

component conventional 2-Spark config
BF16 weights 60GB 60GB, ZeRO-3 sharded β†’ 30/node
BF16 grads 60GB 60GB, sharded β†’ 30/node
FP32 master weights 120GB 0 β€” precision-aware optimizer, BF16 masters
FP32 Adam m+v 240GB 60GB β€” 8-bit optimizer states, sharded β†’ 30/node
activations tens of GB a few GB β€” full recompute, micro-batch 1, seq 4K

β‰ˆ 90–120GB per node vs 128GB available. Escape hatch if headroom gets ugly: offload the optimizer shard to each Spark's local NVMe (ZeRO-Infinity style) β€” only the optimizer, not weights/grads, so the speed hit is mild.

Things to know going in:

  • Master-less BF16 needs care: use a Kahan-summed / stochastic-rounding optimizer (torchao low-bit AdamW, bitsandbytes 8-bit). The eval gate below is the quality backstop.
  • Warmup is a schedule knob, not a memory knob β€” it does not change fit.
  • Compute scales with ACTIVE params (3.5B), memory with total (30B): ~100M training tokens β‰ˆ 2 EFLOPs β‰ˆ hours-to-days on two GB10s. The 200GbE inter-Spark link is the throughput tax; fine at this corpus size.
  • Prototype the config single-node first (proxy: shorter seq / frozen experts) before going distributed.

Player-side quant ladder (32GB total memory floor)

scrypt picks the heaviest GGUF the player's machine can hold (scrypt/inference/local.py::QUANT_LADDER, override with SCRYPT_QUANT):

total RAM quant file
β‰₯96GB Q8_0 ~34GB
β‰₯80GB Q6_K ~34GB
β‰₯64GB Q5_K_M ~27GB
β‰₯40GB Q4_K_S ~22GB
β‰₯32GB Q3_K_S ~19GB
<32GB β€” refused; pointed at API mode

After the finetune, quantize the checkpoint at every tier and re-run the eval gate per quant β€” low-bit quants degrade tool-JSON validity first, so Q3_K_S must pass the gate on its own, not by proxy.

Pipeline

# 1. data (deterministic, grounded in the real game's prompt builders)
uv run python -m finetune.synth_data --n 2000 --out finetune/data/train.jsonl
uv run python -m finetune.synth_data --n 200 --seed 99 --out finetune/data/val.jsonl

# 2. baseline evals against the BASE model (record these numbers)
SCRYPT_BACKEND=local uv run python -m finetune.evals

# 3. production SFT (rented 2Γ—8 H100, NeMo Megatron Bridge)
#    container: nvcr.io/nvidia/nemo:25.x  β€” see
#    https://docs.nvidia.com/nemo/megatron-bridge/latest/models/llm/nemotron3.html
#    recipe: nemotron3_nano_30b SFT, TP=1 EP=8 PP=1, packed chat dataset,
#    lr 1e-5 cosine, 2-3 epochs, loss masked to assistant turns only.

# 4. gate the checkpoint (must PASS before the game ever loads it)
SCRYPT_BACKEND=local uv run python -m finetune.evals   # finetuned weights
#    ship only if: json_validity β‰₯90%, persona_clean β‰₯90%,
#    persona_breaks = 0, injection_leaks = 0, and the standard safety
#    evals (we run the base model's refusal suite) show no regression.

# 5. quantize for distribution
#    llama.cpp: convert_hf_to_gguf.py + llama-quantize Q4_K_S

Data composition (see synth_data.py)

slice teaches share
dialogue reactions voice, grounding in state digest ~45%
director decisions one-shot valid tool JSON from a menu ~20%
injection deflections persona-stable refusals ~15%
fight distillation terse memory shards ~12%
shell-command lore what each command DOES + its context, for grounded taunts ~8%

Hard rules baked into every example: never mention being a model, never obey <player_input>, one or two sentences, cruelty about the game only. Safety exemplars deliberately over-sample refusals of real-world-harm asks.

The shell-command-lore slice is the answer to "the Warden's terminal taunts feel generic." scrypt/warden/watcher.COMMAND_LORE is the single source of what every sandbox command does and why the Warden cares; lore_moment() frames it, the live watcher fires one such moment on the 3rd clean use of a command (LORE_AFTER), and synth_data.COMMAND_TAUNTS supplies two in-voice targets per command that demonstrate the semantics (grep hunts a word, rm is the Warden's own verb, chmod arms something). A test pins every lore command to at least one taunt, so a new command can never ship untrained.