CreativeEngineer Claude Opus 4.6 commited on
Commit
3313e24
·
1 Parent(s): 2fccde8

refactor: replace unsloth with plain transformers+peft for GRPO training

Browse files

Drop unsloth/vllm dependency chain to eliminate huggingface-hub version
conflicts. Use AutoModelForCausalLM + BitsAndBytesConfig + LoraConfig
directly. Flash attention falls back to SDPA if flash-attn is missing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

training/notebooks/fusion_design_lab_training.ipynb CHANGED
@@ -35,23 +35,13 @@
35
  "id": "9a63283cbaf04dbcab1f6479b197f3a8",
36
  "metadata": {},
37
  "outputs": [],
38
- "source": [
39
- "%%capture\n",
40
- "# Build deps for constellaration (booz-xform compiles from source)\n",
41
- "!apt-get update -qq && apt-get install -y -qq cmake ninja-build g++ gfortran libnetcdf-dev libnetcdff-dev > /dev/null\n",
42
- "\n",
43
- "!pip install unsloth vllm\n",
44
- "!pip install --no-deps trl\n",
45
- "!pip install matplotlib requests"
46
- ]
47
  },
48
  {
49
  "cell_type": "markdown",
50
  "id": "8dd0d8092fe74a7c96281538738b07e2",
51
  "metadata": {},
52
- "source": [
53
- "## 2. Load Model with Unsloth"
54
- ]
55
  },
56
  {
57
  "cell_type": "code",
@@ -59,7 +49,7 @@
59
  "id": "72eea5119410473aa328ad9291626812",
60
  "metadata": {},
61
  "outputs": [],
62
- "source": "from unsloth import FastLanguageModel\n\nMODEL_NAME = \"unsloth/Qwen3.5-4B\"\nMAX_SEQ_LENGTH = 2048\n\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n model_name=MODEL_NAME,\n max_seq_length=MAX_SEQ_LENGTH,\n load_in_4bit=True,\n fast_inference=True,\n)\n\nmodel = FastLanguageModel.get_peft_model(\n model,\n r=32,\n target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\"],\n lora_alpha=32,\n use_gradient_checkpointing=\"unsloth\",\n)\n\nprint(f\"Model loaded: {MODEL_NAME}\")"
63
  },
64
  {
65
  "cell_type": "markdown",
@@ -368,99 +358,7 @@
368
  "id": "3ed186c9a28b402fb0bc4494df01f08d",
369
  "metadata": {},
370
  "outputs": [],
371
- "source": [
372
- "import random\n",
373
- "\n",
374
- "FastLanguageModel.for_inference(model)\n",
375
- "\n",
376
- "\n",
377
- "def reward_term_summary(step_or_obs: object) -> str:\n",
378
- " breakdown_obj = getattr(step_or_obs, \"reward_breakdown\")\n",
379
- " breakdown = (\n",
380
- " breakdown_obj.model_dump() if hasattr(breakdown_obj, \"model_dump\") else breakdown_obj\n",
381
- " )\n",
382
- " terms = []\n",
383
- " for key, value in breakdown.items():\n",
384
- " if key in {\n",
385
- " \"intent\",\n",
386
- " \"total\",\n",
387
- " \"evaluation_failed\",\n",
388
- " \"recovered_from_failure\",\n",
389
- " \"reference_constraints_satisfied\",\n",
390
- " \"reference_score\",\n",
391
- " \"reference_feasibility\",\n",
392
- " \"reference_max_elongation\",\n",
393
- " \"initial_reference_score\",\n",
394
- " \"terminal_score_ratio\",\n",
395
- " }:\n",
396
- " continue\n",
397
- " if isinstance(value, (int, float)) and float(value) != 0.0:\n",
398
- " terms.append(f\"{key}={float(value):+.3f}\")\n",
399
- " return \", \".join(terms) if terms else \"none\"\n",
400
- "\n",
401
- "\n",
402
- "def run_episode_with_model(seed_idx: int) -> tuple[float, list[str]]:\n",
403
- " \"\"\"Run one episode using the trained model.\"\"\"\n",
404
- " env = StellaratorEnvironment()\n",
405
- " obs = env.reset(seed=seed_idx)\n",
406
- " prompt = build_prompt(obs)\n",
407
- " inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n",
408
- " outputs = model.generate(\n",
409
- " **inputs,\n",
410
- " max_new_tokens=MAX_COMPLETION_LENGTH,\n",
411
- " temperature=0.7,\n",
412
- " do_sample=True,\n",
413
- " )\n",
414
- " completion = tokenizer.decode(\n",
415
- " outputs[0][inputs[\"input_ids\"].shape[1] :], skip_special_tokens=True\n",
416
- " )\n",
417
- " actions = parse_action_plan(completion)\n",
418
- " episode = run_episode_with_actions(actions, seed_idx=seed_idx)\n",
419
- " trace = [\n",
420
- " (\n",
421
- " f\"{step.action_label} → reward={step.reward:.3f} \"\n",
422
- " f\"score={step.p1_score:.4f} feasible={step.constraints_satisfied} \"\n",
423
- " f\"terms={reward_term_summary(step)}\"\n",
424
- " )\n",
425
- " for step in episode.steps\n",
426
- " ]\n",
427
- " return episode.total_reward, trace\n",
428
- "\n",
429
- "\n",
430
- "def run_random_episode(seed_idx: int) -> float:\n",
431
- " \"\"\"Run one episode with random actions for comparison.\"\"\"\n",
432
- " actions = [StellaratorAction(**random.choice(RUN_ACTION_SPECS)) for _ in range(BUDGET)]\n",
433
- " return run_episode_with_actions(actions, seed_idx=seed_idx).total_reward\n",
434
- "\n",
435
- "\n",
436
- "# Evaluate\n",
437
- "print(\"=\" * 60)\n",
438
- "print(\"TRAINED MODEL EPISODES\")\n",
439
- "print(\"=\" * 60)\n",
440
- "trained_rewards = []\n",
441
- "for seed in range(len(RESET_SEEDS)):\n",
442
- " reward, trace = run_episode_with_model(seed)\n",
443
- " trained_rewards.append(reward)\n",
444
- " print(f\"\\nSeed {seed} — Total reward: {reward:.3f}\")\n",
445
- " for line in trace:\n",
446
- " print(f\" {line}\")\n",
447
- "\n",
448
- "print(f\"\\nMean trained reward: {sum(trained_rewards) / len(trained_rewards):.3f}\")\n",
449
- "\n",
450
- "print(\"\\n\" + \"=\" * 60)\n",
451
- "print(\"RANDOM BASELINE (10 episodes per seed)\")\n",
452
- "print(\"=\" * 60)\n",
453
- "random_rewards = []\n",
454
- "for seed in range(len(RESET_SEEDS)):\n",
455
- " seed_rewards = [run_random_episode(seed) for _ in range(10)]\n",
456
- " random_rewards.extend(seed_rewards)\n",
457
- " print(\n",
458
- " f\"Seed {seed} — Mean: {sum(seed_rewards) / len(seed_rewards):.3f}, Best: {max(seed_rewards):.3f}\"\n",
459
- " )\n",
460
- "\n",
461
- "print(f\"\\nMean random reward: {sum(random_rewards) / len(random_rewards):.3f}\")\n",
462
- "print(f\"Mean trained reward: {sum(trained_rewards) / len(trained_rewards):.3f}\")"
463
- ]
464
  },
465
  {
466
  "cell_type": "markdown",
@@ -474,59 +372,7 @@
474
  "id": "379cbbc1e968416e875cc15c1202d7eb",
475
  "metadata": {},
476
  "outputs": [],
477
- "source": [
478
- "import requests\n",
479
- "\n",
480
- "from fusion_lab.client import FusionLabClient\n",
481
- "\n",
482
- "HF_SPACE_URL = \"https://creativeengineer-fusion-design-lab.hf.space\"\n",
483
- "\n",
484
- "# Check health\n",
485
- "health = requests.get(f\"{HF_SPACE_URL}/health\").json()\n",
486
- "print(f\"HF Space status: {health['status']}\")\n",
487
- "\n",
488
- "# Get task description\n",
489
- "task = requests.get(f\"{HF_SPACE_URL}/task\").json()\n",
490
- "print(f\"\\nTask: {task['description']}\")\n",
491
- "print(f\"Constraints: {task['constraints']}\")\n",
492
- "print(f\"Budget: {task['budget']}\")\n",
493
- "\n",
494
- "with FusionLabClient(base_url=HF_SPACE_URL) as env:\n",
495
- " reset_result = env.reset(seed=42)\n",
496
- " remote_obs = reset_result.observation\n",
497
- " print(f\"\\nRemote reset — max_elongation: {remote_obs.max_elongation:.4f}\")\n",
498
- " print(f\" aspect_ratio: {remote_obs.aspect_ratio:.4f}\")\n",
499
- " print(f\" constraints_satisfied: {remote_obs.constraints_satisfied}\")\n",
500
- " print(f\" budget_remaining: {remote_obs.budget_remaining}\")\n",
501
- "\n",
502
- " # Generate an action plan from the trained model\n",
503
- " prompt = build_prompt(remote_obs)\n",
504
- " inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n",
505
- " outputs = model.generate(\n",
506
- " **inputs, max_new_tokens=MAX_COMPLETION_LENGTH, temperature=0.7, do_sample=True\n",
507
- " )\n",
508
- " completion = tokenizer.decode(\n",
509
- " outputs[0][inputs[\"input_ids\"].shape[1] :], skip_special_tokens=True\n",
510
- " )\n",
511
- " actions = parse_action_plan(completion)\n",
512
- "\n",
513
- " print(f\"\\nTrained model generated {len(actions)} actions for remote env:\")\n",
514
- " for i, action in enumerate(actions[:BUDGET], start=1):\n",
515
- " if action.intent == \"submit\":\n",
516
- " continue\n",
517
- " result = env.step(action)\n",
518
- " step_obs = result.observation\n",
519
- " reward = float(result.reward) if result.reward is not None else 0.0\n",
520
- " print(\n",
521
- " f\" Step {i}: {action.intent} {action.parameter or ''} \"\n",
522
- " f\"{action.direction or ''} {action.magnitude or ''} \"\n",
523
- " f\"→ reward={reward:.3f}, score={step_obs.p1_score:.4f}, terms={reward_term_summary(step_obs)}\"\n",
524
- " )\n",
525
- " if result.done:\n",
526
- " print(f\" Episode done. Final score: {step_obs.p1_score:.4f}\")\n",
527
- " break\n",
528
- "print(\"\\nEnvironment is live and accessible for training and evaluation.\")"
529
- ]
530
  }
531
  ],
532
  "metadata": {
 
35
  "id": "9a63283cbaf04dbcab1f6479b197f3a8",
36
  "metadata": {},
37
  "outputs": [],
38
+ "source": "%%capture\n# Build deps for constellaration (booz-xform compiles from source)\n!apt-get update -qq && apt-get install -y -qq cmake ninja-build g++ gfortran libnetcdf-dev libnetcdff-dev > /dev/null\n\n!pip install trl peft bitsandbytes datasets matplotlib accelerate\n!pip install \"transformers>=4.51\" \"huggingface-hub<1.0\""
 
 
 
 
 
 
 
 
39
  },
40
  {
41
  "cell_type": "markdown",
42
  "id": "8dd0d8092fe74a7c96281538738b07e2",
43
  "metadata": {},
44
+ "source": "## 2. Load Model with LoRA"
 
 
45
  },
46
  {
47
  "cell_type": "code",
 
49
  "id": "72eea5119410473aa328ad9291626812",
50
  "metadata": {},
51
  "outputs": [],
52
+ "source": "import importlib\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\nfrom peft import LoraConfig, get_peft_model\n\nMODEL_NAME = \"Qwen/Qwen3.5-4B\"\nMAX_SEQ_LENGTH = 2048\n\nbnb_config = BitsAndBytesConfig(\n load_in_4bit=True,\n bnb_4bit_quant_type=\"nf4\",\n bnb_4bit_use_double_quant=True,\n bnb_4bit_compute_dtype=torch.bfloat16,\n)\n\nattn_impl = \"flash_attention_2\" if importlib.util.find_spec(\"flash_attn\") else \"sdpa\"\n\nmodel = AutoModelForCausalLM.from_pretrained(\n MODEL_NAME,\n quantization_config=bnb_config,\n torch_dtype=torch.bfloat16,\n device_map=\"auto\",\n attn_implementation=attn_impl,\n)\n\ntokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)\nif tokenizer.pad_token is None:\n tokenizer.pad_token = tokenizer.eos_token\n\nlora_config = LoraConfig(\n r=32,\n lora_alpha=32,\n target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\"],\n lora_dropout=0.0,\n task_type=\"CAUSAL_LM\",\n)\nmodel = get_peft_model(model, lora_config)\nmodel.gradient_checkpointing_enable()\nmodel.print_trainable_parameters()\nprint(f\"Model loaded: {MODEL_NAME} (attn: {attn_impl})\")"
53
  },
54
  {
55
  "cell_type": "markdown",
 
358
  "id": "3ed186c9a28b402fb0bc4494df01f08d",
359
  "metadata": {},
360
  "outputs": [],
361
+ "source": "import random\n\nmodel.eval()\n\n\ndef reward_term_summary(step_or_obs: object) -> str:\n breakdown_obj = getattr(step_or_obs, \"reward_breakdown\")\n breakdown = (\n breakdown_obj.model_dump() if hasattr(breakdown_obj, \"model_dump\") else breakdown_obj\n )\n terms = []\n for key, value in breakdown.items():\n if key in {\n \"intent\",\n \"total\",\n \"evaluation_failed\",\n \"recovered_from_failure\",\n \"reference_constraints_satisfied\",\n \"reference_score\",\n \"reference_feasibility\",\n \"reference_max_elongation\",\n \"initial_reference_score\",\n \"terminal_score_ratio\",\n }:\n continue\n if isinstance(value, (int, float)) and float(value) != 0.0:\n terms.append(f\"{key}={float(value):+.3f}\")\n return \", \".join(terms) if terms else \"none\"\n\n\ndef run_episode_with_model(seed_idx: int) -> tuple[float, list[str]]:\n \"\"\"Run one episode using the trained model.\"\"\"\n env = StellaratorEnvironment()\n obs = env.reset(seed=seed_idx)\n prompt = build_prompt(obs)\n inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n with torch.no_grad():\n outputs = model.generate(\n **inputs,\n max_new_tokens=MAX_COMPLETION_LENGTH,\n temperature=0.7,\n do_sample=True,\n )\n completion = tokenizer.decode(\n outputs[0][inputs[\"input_ids\"].shape[1] :], skip_special_tokens=True\n )\n actions = parse_action_plan(completion)\n episode = run_episode_with_actions(actions, seed_idx=seed_idx)\n trace = [\n (\n f\"{step.action_label} → reward={step.reward:.3f} \"\n f\"score={step.p1_score:.4f} feasible={step.constraints_satisfied} \"\n f\"terms={reward_term_summary(step)}\"\n )\n for step in episode.steps\n ]\n return episode.total_reward, trace\n\n\ndef run_random_episode(seed_idx: int) -> float:\n \"\"\"Run one episode with random actions for comparison.\"\"\"\n actions = [StellaratorAction(**random.choice(RUN_ACTION_SPECS)) for _ in range(BUDGET)]\n return run_episode_with_actions(actions, seed_idx=seed_idx).total_reward\n\n\n# Evaluate\nprint(\"=\" * 60)\nprint(\"TRAINED MODEL EPISODES\")\nprint(\"=\" * 60)\ntrained_rewards = []\nfor seed in range(len(RESET_SEEDS)):\n reward, trace = run_episode_with_model(seed)\n trained_rewards.append(reward)\n print(f\"\\nSeed {seed} — Total reward: {reward:.3f}\")\n for line in trace:\n print(f\" {line}\")\n\nprint(f\"\\nMean trained reward: {sum(trained_rewards) / len(trained_rewards):.3f}\")\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"RANDOM BASELINE (10 episodes per seed)\")\nprint(\"=\" * 60)\nrandom_rewards = []\nfor seed in range(len(RESET_SEEDS)):\n seed_rewards = [run_random_episode(seed) for _ in range(10)]\n random_rewards.extend(seed_rewards)\n print(\n f\"Seed {seed} — Mean: {sum(seed_rewards) / len(seed_rewards):.3f}, Best: {max(seed_rewards):.3f}\"\n )\n\nprint(f\"\\nMean random reward: {sum(random_rewards) / len(random_rewards):.3f}\")\nprint(f\"Mean trained reward: {sum(trained_rewards) / len(trained_rewards):.3f}\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  },
363
  {
364
  "cell_type": "markdown",
 
372
  "id": "379cbbc1e968416e875cc15c1202d7eb",
373
  "metadata": {},
374
  "outputs": [],
375
+ "source": "import requests\n\nfrom fusion_lab.client import FusionLabClient\n\nHF_SPACE_URL = \"https://creativeengineer-fusion-design-lab.hf.space\"\n\n# Check health\nhealth = requests.get(f\"{HF_SPACE_URL}/health\").json()\nprint(f\"HF Space status: {health['status']}\")\n\n# Get task description\ntask = requests.get(f\"{HF_SPACE_URL}/task\").json()\nprint(f\"\\nTask: {task['description']}\")\nprint(f\"Constraints: {task['constraints']}\")\nprint(f\"Budget: {task['budget']}\")\n\nwith FusionLabClient(base_url=HF_SPACE_URL) as env:\n reset_result = env.reset(seed=42)\n remote_obs = reset_result.observation\n print(f\"\\nRemote reset — max_elongation: {remote_obs.max_elongation:.4f}\")\n print(f\" aspect_ratio: {remote_obs.aspect_ratio:.4f}\")\n print(f\" constraints_satisfied: {remote_obs.constraints_satisfied}\")\n print(f\" budget_remaining: {remote_obs.budget_remaining}\")\n\n # Generate an action plan from the trained model\n prompt = build_prompt(remote_obs)\n inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n with torch.no_grad():\n outputs = model.generate(\n **inputs, max_new_tokens=MAX_COMPLETION_LENGTH, temperature=0.7, do_sample=True\n )\n completion = tokenizer.decode(\n outputs[0][inputs[\"input_ids\"].shape[1] :], skip_special_tokens=True\n )\n actions = parse_action_plan(completion)\n\n print(f\"\\nTrained model generated {len(actions)} actions for remote env:\")\n for i, action in enumerate(actions[:BUDGET], start=1):\n if action.intent == \"submit\":\n continue\n result = env.step(action)\n step_obs = result.observation\n reward = float(result.reward) if result.reward is not None else 0.0\n print(\n f\" Step {i}: {action.intent} {action.parameter or ''} \"\n f\"{action.direction or ''} {action.magnitude or ''} \"\n f\"→ reward={reward:.3f}, score={step_obs.p1_score:.4f}, terms={reward_term_summary(step_obs)}\"\n )\n if result.done:\n print(f\" Episode done. Final score: {step_obs.p1_score:.4f}\")\n break\nprint(\"\\nEnvironment is live and accessible for training and evaluation.\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
  ],
378
  "metadata": {