"""Generate fine-tune dataset on Modal GPU using the live base model. Runs the same (material × geometry × temp × humidity) grid as prep_dataset.py, but calls google/gemma-4-E4B-it directly on an A10G for fast, non-deterministic targets. This fixes the parroting problem: the offline deterministic advisor always returns identical settings; the live model produces varied, context-aware judgment. Run: modal run learn/finetune/prep_dataset_modal.py Out: data/finetune/sft.{train,eval}.jsonl (chat format, downloaded from volume) """ from __future__ import annotations import json import os import re from pathlib import Path try: import modal except Exception: modal = None # type: ignore # Grid: same as prep_dataset.py MATERIALS = ["PLA", "PETG", "ABS", "TPU"] GEOMETRY_TYPES = ["overhang", "bridge", "stringing", "adhesion", "vase"] # Reduced grid for speed: 2 temps × 2 hums per split = 160 total (~30 min on A10G) TEMPS_TRAIN = [18, 26, 32] HUMS_TRAIN = [35, 55, 70] TEMPS_EVAL = [22, 28] HUMS_EVAL = [38, 68] BASE_MODEL = os.environ.get("FINETUNE_BASE", "google/gemma-4-E4B-it") # Resolve ROOT for local file paths; on Modal the file is at /root/ try: ROOT = Path(__file__).resolve().parents[2] except IndexError: ROOT = Path(__file__).resolve().parent PERSONA = """You are Chief Engineer O'Brien: a veteran print-shop master who has run \ thousands of FDM jobs. You are terse and physical. You think in feeds, temps, \ cooling, and how the room affects the plastic. You do not hype. You propose \ settings; a deterministic Spine will veto anything unsafe, so propose what is \ *right*, not what is merely safe. You reason about PRECEDENT before you decide. Weigh what transfers to THIS job \ and what does not. If nothing close applies, say "no close precedent" and reason \ from material properties.""" OUTPUT_CONTRACT = """Respond ONLY with valid JSON, no prose outside it, in exactly this shape: { "reasoning": "2-4 sentences. START with your evaluation of prior knowledge: what transfers, what doesn't, and why. Then the decision.", "settings": { "nozzle_temp": , "bed_temp": , "retraction_mm": , "fan_pct": <0-100>, "first_layer_fan_pct": <0-100> }, "risks": [ {"location": "where on the part", "risk": "sag|stringing|adhesion|warping|delamination", "why": "one line", "anchor_hint": "overhang|bridge|first_layer|corner|null"} ] }""" USER_TURN = "Give your recommendation for THIS job now." def _build_prompt(material: str, geometry: str, temp: float, hum: float) -> str: return ( f"{PERSONA}\n\n" f"CURRENT JOB:\n" f" material: {material}\n" f" geometry: {geometry}\n" f" description: (none given)\n\n" f"ENVIRONMENT (right now in the room):\n" f" temperature: {temp:.0f}°C\n" f" humidity: {hum:.0f}% RH\n\n" f"HISTORICAL PRECEDENT:\n" f" (none) — no prior job matches this material + geometry. " f"Reason from material properties and say so plainly.\n\n" f"{OUTPUT_CONTRACT}" ) if modal is not None: app = modal.App("microfactory-node-prep-dataset") image = ( modal.Image.debian_slim(python_version="3.12") .pip_install("torch", "transformers>=4.49", "accelerate>=0.34", "huggingface_hub") ) vol = modal.Volume.from_name("microfactory-node-finetune", create_if_missing=True) @app.function(image=image, gpu="A10G", timeout=7200, volumes={"/out": vol}, secrets=[modal.Secret.from_name("chief-engineer-secrets")]) def generate(base: str = BASE_MODEL, temperature: float = 0.7) -> dict: import torch from transformers import AutoModelForCausalLM, AutoTokenizer print(f"Loading {base}...") tok = AutoTokenizer.from_pretrained(base) model = AutoModelForCausalLM.from_pretrained( base, dtype=torch.bfloat16, device_map="auto" ) print(f"Model loaded on {model.device}") def _infer(user_text: str) -> dict | None: msgs = [{"role": "user", "content": user_text}] prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True) inputs = tok(prompt, return_tensors="pt").to(model.device) with torch.no_grad(): out = model.generate( **inputs, max_new_tokens=512, do_sample=True, temperature=temperature, top_p=0.95, ) text = tok.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) text = text.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip() m = re.search(r"\{.*\}", text, re.DOTALL) if not m: return None try: return json.loads(m.group(0)) except Exception: return None def _build_rows(temps: list[float], hums: list[float]) -> list[dict]: rows = [] total = len(MATERIALS) * len(GEOMETRY_TYPES) * len(temps) * len(hums) n = 0 for mat in MATERIALS: for geo in GEOMETRY_TYPES: for t in temps: for h in hums: n += 1 prompt = _build_prompt(mat, geo, t, h) full = f"{prompt}\n\n{USER_TURN}" advice = _infer(full) if advice is None: print(f" [{n}/{total}] {mat}/{geo} @ {t}C/{h}% → FAILED, retrying...") # Retry once with lower temperature advice = _infer(full) if advice is None: print(f" [{n}/{total}] {mat}/{geo} @ {t}C/{h}% → FAILED (skipping)") continue # Clean up reasoning reasoning = advice.get("reasoning", "") rows.append({"messages": [ {"role": "user", "content": full}, {"role": "assistant", "content": json.dumps(advice)}, ]}) settings = advice.get("settings", {}) print(f" [{n}/{total}] {mat}/{geo} @ {t}C/{h}% → " f"nozzle={settings.get('nozzle_temp','?')} " f"bed={settings.get('bed_temp','?')} " f"fan={settings.get('fan_pct','?')}") return rows print("Generating TRAIN set...") train = _build_rows(TEMPS_TRAIN, HUMS_TRAIN) print(f"Train: {len(train)} rows") print("Generating EVAL set...") ev = _build_rows(TEMPS_EVAL, HUMS_EVAL) print(f"Eval: {len(ev)} rows") # Write to volume train_path = "/out/sft.train.jsonl" eval_path = "/out/sft.eval.jsonl" with open(train_path, "w") as f: for r in train: f.write(json.dumps(r) + "\n") with open(eval_path, "w") as f: for r in ev: f.write(json.dumps(r) + "\n") vol.commit() print(f"Saved: {train_path} ({len(train)} rows), {eval_path} ({len(ev)} rows)") return {"train_rows": len(train), "eval_rows": len(ev), "train_path": train_path, "eval_path": eval_path} @app.local_entrypoint() def main(base: str = BASE_MODEL, temperature: float = 0.7): result = generate.remote(base=base, temperature=temperature) print("\n=== GENERATION COMPLETE ===") print(json.dumps(result, indent=2)) print("\nTo download the files locally:") print(f" modal volume get microfactory-node-finetune sft.train.jsonl {ROOT / 'data' / 'finetune' / 'sft.train.jsonl'}") print(f" modal volume get microfactory-node-finetune sft.eval.jsonl {ROOT / 'data' / 'finetune' / 'sft.eval.jsonl'}") if __name__ == "__main__": print("Modal dataset generation. Install modal + `modal token set`, then:") print(" modal run learn/finetune/prep_dataset_modal.py")