puck / molt /train_modal.py
vu1n's picture
Puck β€” desktop fairy familiar (HF Build Small)
3c124f3
Raw
History Blame Contribute Delete
4.18 kB
"""Train Puck's character LoRA on Holotron-12B, on Modal.
TRL SFT + PEFT LoRA over the 162-example chat curriculum (build_dataset.py).
Text-only LoRA on the language side of the VLM β€” character/voice, not vision.
Adapter is saved to a Modal volume (no HF token in env); publish later.
cd molt && uv run build_dataset.py
modal run --detach train_modal.py # .spawn() inside β†’ truly detached
modal volume get puck-lora /puck-holotron-12b-lora ./out
⚠️ BLOCKED (2026-06-07): Hcompany/Holotron-12B's published trust_remote_code
modeling.py imports a `_fully_shard.py` that isn't in the repo β€” an H Company
packaging bug in their *training* path (vLLM inference is unaffected, which is
why the vision endpoint works). transformers can't load it for TRL.
Options when revisiting: (a) wait for H Company to publish the missing file;
(b) train the character LoRA on the BASE nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL
(still Nemotron, still ≀32B, dataset is base-agnostic β€” voice doesn't need
Holotron's CUA tuning); (c) stub _fully_shard.py if it's a no-op FSDP helper.
Character already lands well via the enriched prompt, so the LoRA (Well-Tuned
badge) is lower priority than vision.
Hybrid-Mamba caveat once unblocked: target_modules='all-linear', gradient
checkpointing off (Mamba layers dislike it)."""
import json
from pathlib import Path
import modal
MODEL = "Hcompany/Holotron-12B"
HERE = Path(__file__).resolve().parent
image = (
modal.Image.debian_slim(python_version="3.12")
.pip_install(
"torch",
"transformers>=4.48",
"trl>=0.12",
"peft>=0.14",
"datasets",
"accelerate",
"huggingface_hub[hf_transfer]",
"sentencepiece",
"einops", # hybrid-Mamba modeling code often needs it
)
.env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
)
vol = modal.Volume.from_name("puck-lora", create_if_missing=True)
hf_cache = modal.Volume.from_name("puck-hf-cache", create_if_missing=True)
app = modal.App("puck-train")
@app.function(
image=image,
gpu="A100-80GB",
timeout=60 * 60,
volumes={"/adapter": vol, "/root/.cache/huggingface": hf_cache},
)
def train(records: list[dict]):
import torch
from datasets import Dataset
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import SFTConfig, SFTTrainer
# conversational dataset β†’ TRL applies the chat template itself
ds = Dataset.from_list([{"messages": r["messages"]} for r in records])
tok = AutoTokenizer.from_pretrained(MODEL, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL, trust_remote_code=True, torch_dtype=torch.bfloat16, device_map="auto"
)
peft_cfg = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules="all-linear", # robust across the hybrid's linear layers
)
cfg = SFTConfig(
output_dir="/adapter/run",
num_train_epochs=4,
per_device_train_batch_size=1,
gradient_accumulation_steps=8,
learning_rate=2e-4,
warmup_ratio=0.05,
logging_steps=5,
save_strategy="epoch",
bf16=True,
gradient_checkpointing=False, # Mamba layers + checkpointing don't mix
max_length=1024,
report_to="none",
)
trainer = SFTTrainer(model=model, args=cfg, train_dataset=ds, peft_config=peft_cfg)
trainer.train()
out = "/adapter/puck-holotron-12b-lora"
trainer.save_model(out)
tok.save_pretrained(out)
vol.commit()
print(f"saved adapter β†’ {out}")
return out
@app.local_entrypoint()
def main():
records = [
json.loads(line) for line in (HERE / "data" / "sft.jsonl").read_text().splitlines()
]
print(f"training on {len(records)} examples")
# .spawn() so a detached run survives the local caller disconnecting
# (.remote() is synchronous and Modal cancels it when the CLI exits).
call = train.spawn(records)
print(f"spawned: {call.object_id} β€” modal volume get puck-lora /puck-holotron-12b-lora ./out")