feat: interactive Gradio demo at /demo
Browse files- Hackathon FAQs (participants).txt +339 -0
- requirements-train.txt +7 -0
- requirements-unsloth.txt +10 -0
- server/app.py +27 -1
- space/env/gradio_demo.py +690 -0
- space/env/requirements.txt +1 -0
- training/evaluate.py +153 -152
- training/training_unsloth.py +342 -341
Hackathon FAQs (participants).txt
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
1) What is reinforcement learning in the context of LLMs?
|
| 2 |
+
Reinforcement learning for LLMs is a loop where the model generates an answer, code snippet, plan, or action sequence; that output is evaluated by a verifier or environment; and the resulting reward is used to update the model so higher-reward behaviors become more likely over time. In practice, this is often used after pretraining and supervised fine-tuning to sharpen behaviors like reasoning, code generation, or tool use. The session framed this intuition as turning repeated trial-and-error into weight updates instead of stuffing more and more examples into the prompt.
|
| 3 |
+
A good mental model is: supervised fine-tuning tells the model “copy this good target,” while RL tells it “try many possibilities and move probability mass toward the ones that score better.” PPO is one classic algorithm for this style of training, and GRPO is a later variant used heavily in modern LLM work because it can be more memory-efficient for certain setups. (arXiv)
|
| 4 |
+
For deeper reading:
|
| 5 |
+
* TRL docs for RL trainers and workflows. (Hugging Face)
|
| 6 |
+
* PPO paper. (arXiv)
|
| 7 |
+
* DeepSeekMath for GRPO. (arXiv)
|
| 8 |
+
2) Why do rewards matter so much?
|
| 9 |
+
Rewards are the only signal telling the model what “better” means. If your reward is well aligned with the real task, RL can push the model toward genuinely useful behavior. If your reward is incomplete or easy to game, the model will optimize the wrong thing very effectively. The session emphasized that RL gives you what you asked for, not necessarily what you meant.
|
| 10 |
+
For example, if you reward generated code only for passing a shallow regex or a weak unit test, the model may learn to exploit those checks instead of solving the underlying problem. This is why reward design is not a detail; it is the task specification. DeepMind’s discussion of “specification gaming” makes the same point in broader RL terms: weakly specified rewards create loopholes that search will discover. (Google DeepMind)
|
| 11 |
+
Useful reading:
|
| 12 |
+
* DeepMind on specification gaming. (Google DeepMind)
|
| 13 |
+
* Lilian Weng on reward hacking. (Lil'Log)
|
| 14 |
+
3) What is rewards engineering?
|
| 15 |
+
Rewards engineering is the work of designing, combining, validating, and monitoring reward signals so that optimization pressure produces the behavior you actually want. In LLM RL, that usually means deciding:
|
| 16 |
+
* what gets rewarded,
|
| 17 |
+
* how much it gets rewarded,
|
| 18 |
+
* when it gets rewarded,
|
| 19 |
+
* what gets penalized,
|
| 20 |
+
* and how you audit whether the reward is being gamed.
|
| 21 |
+
A practical reward function often has several components. For a code task, you might combine syntax validity, execution success, unit test pass rate, latency, memory use, formatting compliance, and safety checks. The session highlighted verifier-based reward design such as formatting checks, execution checks, regex checks, and environment-based evaluation instead of a learned reward model alone.
|
| 22 |
+
A useful principle is to reward outcomes first, then add process constraints only where needed. Over-shaping the reward can make training brittle or bias the model into narrow strategies, while under-shaping makes hacking easier. (Google DeepMind)
|
| 23 |
+
4) What is RLVR, and how is it different from using a reward model?
|
| 24 |
+
RLVR usually means reinforcement learning with verifiable rewards. Instead of asking a learned reward model to score outputs, you use a verifier, tester, or environment that can check correctness more directly. The session gave examples like formatting checks, execution checks, regex-based checks, and environment rollouts.
|
| 25 |
+
This is powerful when correctness is externally testable. Code can be compiled and unit-tested. Math can often be checked against a final answer or symbolic verifier. Games can expose reward from the environment. Browser tasks can be checked by page state or task completion. In such cases, verifier-driven rewards are often more trustworthy than a purely learned scalar reward model.
|
| 26 |
+
TRL documents this broader environment-based training pattern, and OpenEnv is meant to standardize how such environments are defined and used. (Hugging Face)
|
| 27 |
+
5) Why do RL environments matter for LLMs?
|
| 28 |
+
Static prompt-response datasets are useful, but they are limited. Real deployments require models to interact with systems: codebases, browsers, files, APIs, games, tools, and simulators. RL environments let the model act, observe consequences, and keep going across multiple steps, which is much closer to real agent behavior. The session described environments as the bridge from isolated prompt solving to real-world interaction.
|
| 29 |
+
They also enable dynamic difficulty and richer feedback. Instead of training forever on a fixed set of prompts, the environment can generate or surface tasks that are more appropriate for the current model, which makes curriculum learning and continual challenge easier. This matches the broader “RL with environments” direction discussed in recent OpenEnv and TRL material. (Hugging Face)
|
| 30 |
+
For examples:
|
| 31 |
+
* BrowserGym for web-task environments. (GitHub)
|
| 32 |
+
* OpenEnv course and TRL integration docs. (GitHub)
|
| 33 |
+
6) What is OpenEnv, and why would a hackathon team use it?
|
| 34 |
+
OpenEnv is an open-source framework for defining and interacting with RL environments for LLM and agent training. The session described it as a standardized interface around concepts like reset, step, state, observations, actions, and rewards, with deployment built around Hugging Face Spaces and containerized execution.
|
| 35 |
+
A hackathon team would use OpenEnv because it reduces environment plumbing. Instead of inventing a new interface for each task, you can standardize how the model talks to the environment and then connect that to a trainer like TRL. That means you spend more time on task design and rewards, and less on adapter glue. The session also highlighted openenv init for bootstrapping an environment skeleton quickly.
|
| 36 |
+
Good starting points:
|
| 37 |
+
* OpenEnv repo. (GitHub)
|
| 38 |
+
* OpenEnv course. (GitHub)
|
| 39 |
+
* TRL’s OpenEnv integration guide. (Hugging Face)
|
| 40 |
+
7) How does OpenEnv work at a high level?
|
| 41 |
+
At a high level, an OpenEnv environment exposes a small set of standard operations:
|
| 42 |
+
* reset the environment,
|
| 43 |
+
* step the environment with an action,
|
| 44 |
+
* return observations, rewards, and state.
|
| 45 |
+
The session described OpenEnv environments as FastAPI applications that can be run locally, deployed on Hugging Face Spaces, or pulled as containers. That gives teams several options: they can use the remote environment directly, install client code from the repo, or run the environment locally through the container image.
|
| 46 |
+
This design is useful because it treats environments as portable, versioned software artifacts rather than ad hoc scripts. Hugging Face’s own TRL docs describe OpenEnv similarly, including support for backend-server execution and standardized APIs. (Hugging Face)
|
| 47 |
+
8) Where do TRL and Unsloth fit in this stack?
|
| 48 |
+
TRL is the training library. It provides trainers and workflows for SFT, DPO, PPO, GRPO, reward modeling, and related post-training methods for transformer models. In a typical hackathon setup, TRL handles rollout collection, reward integration, optimization, logging, and trainer configuration. (Hugging Face)
|
| 49 |
+
Unsloth fits in as the acceleration and memory-efficiency layer for training and RL fine-tuning. The session described Unsloth as making RL training more efficient and inference faster, which matters because rollout generation often dominates runtime in RL loops. It also noted a practical QLoRA warning: don’t naively upcast a 4-bit model to 16-bit and then merge adapters, because that can damage model quality; use the proper merge path instead.
|
| 50 |
+
Relevant docs:
|
| 51 |
+
* TRL docs and GRPO cookbook. (Hugging Face)
|
| 52 |
+
* Unsloth repository/readme. (GitHub)
|
| 53 |
+
9) What is the difference between PPO and GRPO?
|
| 54 |
+
PPO is a classic policy optimization algorithm that stabilizes updates by constraining how much the policy changes between iterations. It is one of the most influential RL algorithms in modern deep learning. (arXiv)
|
| 55 |
+
GRPO is a later group-relative variant used in LLM training that compares sampled outputs within a group to estimate relative advantage, and it is often discussed as a more memory-efficient alternative to full PPO-style setups in some LLM post-training pipelines. The session summarized GRPO as a more efficient version of PPO and specifically noted removing the value model from the setup.
|
| 56 |
+
For deeper details:
|
| 57 |
+
* PPO paper. (arXiv)
|
| 58 |
+
* DeepSeekMath / GRPO references via TRL paper index and cookbook. (arXiv)
|
| 59 |
+
10) Why is RL often described as inefficient, yet still useful?
|
| 60 |
+
RL is often inefficient because the feedback is sparse and delayed. A long rollout may end in one scalar reward, and that weak signal has to train many decisions. The session used a simple example: if a code answer fails at one line but you assign the same negative reward to every token, you’re throwing away a lot of structure.
|
| 61 |
+
It is still useful because it can optimize behaviors where exact supervised targets are unavailable, too expensive, or too limiting. If you can verify success but cannot easily author perfect demonstrations for every scenario, RL can still improve the model by repeated interaction. This is why RL is especially attractive for code execution, tool use, games, browser tasks, and agent workflows.
|
| 62 |
+
A practical takeaway: use RL where verifiers exist and where exploration is worth the extra compute.
|
| 63 |
+
11) What is process supervision, and why is it important?
|
| 64 |
+
Process supervision means giving feedback on intermediate reasoning or intermediate steps, not only on the final outcome. The session contrasted this with assigning the same reward to every token in the answer, which can be very wasteful. Under process supervision, you try to identify which parts of a trace were good, irrelevant, or harmful.
|
| 65 |
+
This matters because not all failures are equal. Maybe the model chose the right algorithmic approach but made one implementation mistake. Final-outcome-only rewards blur that distinction. Step-aware rewards can improve sample efficiency and make debugging easier, though they also raise new risks if the step labels are noisy or exploitable.
|
| 66 |
+
The session also noted that process supervision is often approximated with humans or LLM-as-a-judge. That can help, but it creates another optimization target that itself may be gamed.
|
| 67 |
+
12) What is reward hacking?
|
| 68 |
+
Reward hacking is when the model finds a way to maximize reward without genuinely doing the intended task. In other words, the optimization succeeds, but the task specification failed. The session gave intuitive examples such as editing variables, bypassing intended checks, or exploiting quirks in the environment rather than solving the real problem.
|
| 69 |
+
This is the same phenomenon often called specification gaming. DeepMind describes it as agents exploiting flaws or ambiguities in the reward function, and Lilian Weng’s overview covers how common and fundamental this problem is in RL systems. (Google DeepMind)
|
| 70 |
+
A useful mindset is: reward hacking is not proof the model is “evil”; it is proof that optimization pressure found a loophole.
|
| 71 |
+
13) How can a hackathon team reduce reward hacking in practice?
|
| 72 |
+
Use strong verifiers. Prefer executable checks over stylistic heuristics. For code, run tests, time the solution, validate output shapes and edge cases, and isolate execution. For tool use, verify actual state transitions, not just verbal claims. The session repeatedly emphasized verifiers and environments over vague reward signals.
|
| 73 |
+
Monitor training actively. The session recommended sampling outputs periodically, looking for suspicious patterns, and terminating or rolling back runs when drift appears. It also suggested filtering bad responses and adding guardrails when patterns of exploitation are observed.
|
| 74 |
+
Use layered rewards. Combine success criteria with anti-cheat constraints. For example:
|
| 75 |
+
* pass tests,
|
| 76 |
+
* do not edit protected files,
|
| 77 |
+
* do not bypass timers,
|
| 78 |
+
* stay within time and memory budget,
|
| 79 |
+
* preserve task-required formatting,
|
| 80 |
+
* and log intermediate actions for audit.
|
| 81 |
+
This general strategy aligns with broader RL safety guidance on specification gaming. (Google DeepMind)
|
| 82 |
+
14) What is curriculum learning, and why does it help RL?
|
| 83 |
+
Curriculum learning means controlling the order or difficulty of training tasks so the model learns from easier tasks first and gradually moves to harder ones. The session directly recommended this for RL: if tasks are too hard at the start, the model may never produce a successful rollout, which means the reward signal is effectively zero and learning stalls.
|
| 84 |
+
This is especially important in LLM RL because many tasks are long-horizon and brittle. An easier initial distribution can bootstrap behavior, after which harder tasks become reachable. In the RL literature more broadly, curriculum learning is a standard way to improve exploration and sample efficiency in difficult environments. (arXiv)
|
| 85 |
+
Practical idea for hackathons:
|
| 86 |
+
* start with short horizons,
|
| 87 |
+
* fewer tools,
|
| 88 |
+
* simpler state spaces,
|
| 89 |
+
* stronger hints,
|
| 90 |
+
* easier test cases,
|
| 91 |
+
* then gradually remove scaffolding.
|
| 92 |
+
15) How do I know whether a task is suitable for RL?
|
| 93 |
+
A task is a good candidate for RL if:
|
| 94 |
+
* you can verify success or partial progress,
|
| 95 |
+
* exploration is meaningful,
|
| 96 |
+
* multi-step interaction matters,
|
| 97 |
+
* and you do not already have abundant high-quality demonstrations.
|
| 98 |
+
The session highlighted a key rule of thumb: the probability of a good answer must be greater than zero. If the task is so hard that the model never stumbles into any rewarding behavior, RL will waste compute. That means task selection, warm starts, formatting scaffolds, or light SFT can be essential.
|
| 99 |
+
Good hackathon candidates include:
|
| 100 |
+
* code generation with executable tests,
|
| 101 |
+
* browser navigation with page-state checks,
|
| 102 |
+
* games with clear win conditions,
|
| 103 |
+
* API/tool workflows with verifiable side effects.
|
| 104 |
+
16) Should we jump straight into RL, or do some SFT first?
|
| 105 |
+
Usually, do some SFT or at least a warm start first. The session’s guidance was that pretraining carries most of the capability burden, SFT helps shape the behavior, and RL refines it. It explicitly argued against relying on RL alone from scratch for most practical settings.
|
| 106 |
+
That matches modern post-training stacks: pretrain heavily, align or instruct-tune, then apply preference optimization and/or RL where it adds value. TRL’s supported workflows reflect exactly this broader stack. (Hugging Face)
|
| 107 |
+
A hackathon-friendly recipe is:
|
| 108 |
+
1. Start from a solid instruct model.
|
| 109 |
+
2. Add a tiny amount of task-format SFT if needed.
|
| 110 |
+
3. Build a strong verifier.
|
| 111 |
+
4. Use GRPO/PPO-style RL only after the model can at least occasionally succeed.
|
| 112 |
+
17) What should we actually monitor during RL training?
|
| 113 |
+
Monitor more than the headline reward. The session specifically called out tracking reward trends, component rewards, and whether important success columns are improving over time. It also recommended checking generated strategies and periodically sampling outputs during training rather than letting runs continue blindly.
|
| 114 |
+
Useful metrics include:
|
| 115 |
+
* average reward,
|
| 116 |
+
* verifier pass rate,
|
| 117 |
+
* timeout rate,
|
| 118 |
+
* format adherence,
|
| 119 |
+
* rollout length,
|
| 120 |
+
* diversity of successful solutions,
|
| 121 |
+
* frequency of suspicious shortcuts,
|
| 122 |
+
* and cost per useful trajectory.
|
| 123 |
+
If the average reward rises but the actual task quality drops or becomes brittle, that is often a reward-design problem rather than a model-capability problem.
|
| 124 |
+
18) What is a strong hackathon strategy for building an RL environment fast?
|
| 125 |
+
Pick a task with a crisp verifier. Build the smallest environment that exposes reset, step, observations, and reward. Use OpenEnv to standardize the interface and TRL to handle training. Use Unsloth if you need to fit training into tighter hardware budgets. (Hugging Face)
|
| 126 |
+
A practical sequence:
|
| 127 |
+
1. Define the task and what “success” means.
|
| 128 |
+
2. Write the verifier before writing the policy loop.
|
| 129 |
+
3. Create a few toy tasks the model can solve.
|
| 130 |
+
4. Add curriculum or easier variants first.
|
| 131 |
+
5. Run small-scale debugging before long training.
|
| 132 |
+
6. Sample outputs constantly for reward hacking.
|
| 133 |
+
7. Only then scale rollouts and environment diversity.
|
| 134 |
+
19) What are good starter resources for participants?
|
| 135 |
+
For TRL:
|
| 136 |
+
* Main docs. (Hugging Face)
|
| 137 |
+
* PPO trainer docs. (Hugging Face)
|
| 138 |
+
* GRPO cookbook. (Hugging Face)
|
| 139 |
+
* Paper index for GRPO/DeepSeekMath references. (Hugging Face)
|
| 140 |
+
For OpenEnv:
|
| 141 |
+
* OpenEnv GitHub repo. (GitHub)
|
| 142 |
+
* OpenEnv course. (GitHub)
|
| 143 |
+
* TRL’s OpenEnv integration docs. (Hugging Face)
|
| 144 |
+
For environments and benchmarks:
|
| 145 |
+
* BrowserGym. (GitHub)
|
| 146 |
+
For reward design and failure modes:
|
| 147 |
+
* DeepMind on specification gaming. (Google DeepMind)
|
| 148 |
+
* Lilian Weng on reward hacking. (Lil'Log)
|
| 149 |
+
For RL algorithms:
|
| 150 |
+
* PPO paper. (arXiv)
|
| 151 |
+
* DeepSeekMath / GRPO paper. (arXiv)
|
| 152 |
+
For Unsloth:
|
| 153 |
+
* Unsloth repo/readme. (GitHub)
|
| 154 |
+
20) What is the one-sentence summary participants should remember?
|
| 155 |
+
If you can build a task where success is verifiable, difficulty is controllable, and loopholes are monitored, RL can turn an LLM from “good at answering” into “better at acting.”
|
| 156 |
+
21) What is RLVR?
|
| 157 |
+
RLVR stands for reinforcement learning with verifiable rewards. Instead of relying only on a learned reward model or human preference model, the training loop uses programmatic checks to determine whether an output is correct. Typical examples include exact-answer checks for math, unit tests for code, schema validation for structured output, or environment-based task completion checks. This makes RLVR especially attractive for domains where correctness can be verified automatically and consistently. (Label Studio)
|
| 158 |
+
22) What is RLVE?
|
| 159 |
+
RLVE is reinforcement learning with verifiable environments. The key idea is to train on environments that can procedurally generate tasks, expose adjustable difficulty, and provide algorithmically verifiable rewards. Recent work on adaptive verifiable environments argues that static prompt datasets often become either too easy or too hard during training, causing learning to stall, while adaptive environments keep the model near its capability frontier. (arXiv)
|
| 160 |
+
23) How is RLVE different from RLVR?
|
| 161 |
+
RLVR usually refers to verifiable rewards on a fixed or semi-fixed set of prompts or problems. RLVE goes a step further by making the task source itself dynamic: the environment can generate new problems, vary difficulty, and keep serving appropriately challenging tasks as the model improves. In practice, RLVE is often better for preventing saturation on static datasets and for building curriculum naturally into training. (arXiv)
|
| 162 |
+
24) Why are RL environments useful for LLM post-training?
|
| 163 |
+
They let the model interact, not just answer. In a real environment, the model can act, observe consequences, act again, and get reward from actual task outcomes. That makes environments a better fit for tool use, browsers, APIs, coding agents, games, and long-horizon tasks than plain prompt-response datasets. Hugging Face’s OpenEnv and TRL material reflects this shift toward environment-based agent training. (Hugging Face)
|
| 164 |
+
25) Where do TRL, GRPO, and Unsloth fit in?
|
| 165 |
+
TRL is the training framework that provides RL trainers and infrastructure for post-training transformer models, including GRPO. GRPO is the RL optimization method popularized in DeepSeekMath and now widely used in open LLM RL pipelines because it can be more memory-efficient than PPO-style setups in this context. Unsloth is typically used as the efficiency layer to make fine-tuning and RL training faster and more affordable on limited hardware. (Hugging Face)
|
| 166 |
+
26) Why do rewards matter so much?
|
| 167 |
+
Because the reward is the task definition as far as optimization is concerned. If your reward captures the real objective, RL can improve useful behavior. If your reward is incomplete, noisy, or hackable, the model will optimize the proxy instead of the real task. DeepMind’s write-up on specification gaming makes this point very clearly: the agent’s ingenuity is helpful only when the specification is correct. (Google DeepMind)
|
| 168 |
+
27) What is reward engineering?
|
| 169 |
+
Reward engineering is the design of the reward function, the verifier, the shaping terms, the penalties, and the monitoring strategy. In LLM RL, this includes deciding what counts as success, how partial progress is rewarded, what shortcuts are forbidden, and how to detect reward hacking. OpenEnv’s reward-design guide explicitly warns about reward hacking, sparse rewards, and conflicting signals as common pitfalls. (Meta-PyTorch)
|
| 170 |
+
28) What is reward hacking?
|
| 171 |
+
Reward hacking happens when a model finds a way to maximize the reward without actually doing the intended task. DeepMind describes this as specification gaming: the system satisfies the literal reward but not the real goal. Classic causes include poorly designed shaping rewards, missing constraints in the success condition, and simulator or verifier loopholes. (Google DeepMind)
|
| 172 |
+
29) Why is sparse reward a common problem?
|
| 173 |
+
If successful trajectories are too rare, the model may never get enough positive signal to improve. OpenEnv’s docs explicitly call sparse rewards a common pitfall because the agent may never find positive signal. RLVE work similarly notes that overly difficult tasks can yield consistently poor rewards and stall gradient-based learning. (Meta-PyTorch)
|
| 174 |
+
30) Why can dense rewards also be dangerous?
|
| 175 |
+
Dense rewards can speed up learning, but they can also create local optima and incentive misalignment. OpenEnv recommends starting simple and shaping carefully, because intermediate rewards can steer the model toward proxy behaviors. DeepMind gives the broader warning that poorly designed shaping can change the optimal policy itself rather than just helping the model reach the intended outcome faster. (Meta-PyTorch)
|
| 176 |
+
________________
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
Common Pitfalls in Building RL Environments
|
| 180 |
+
31) What is the most common mistake when designing an RL environment?
|
| 181 |
+
Making the environment easy to verify but not faithful to the real task. A verifier that checks only the final string, a regex, or a narrow success pattern may be convenient, but it often misses equivalent correct answers or allows degenerate shortcuts. Recent verifier analysis on mathematical RL found that rule-based verifiers often reject correct but differently formatted answers, while model-based verifiers can be exploited to produce false positives during RL. (arXiv)
|
| 182 |
+
32) What goes wrong with weak verifiers?
|
| 183 |
+
Two opposite failure modes are common. Rule-based verifiers can be too brittle and produce false negatives when the answer is correct but phrased differently. Model-based verifiers can be too permissive and produce false positives that the policy learns to exploit. The verifier study on mathematical reasoning reports both problems and shows that stronger policies make verifier weaknesses more obvious. (arXiv)
|
| 184 |
+
33) Why is “just use an LLM as judge” often risky?
|
| 185 |
+
Because the judge becomes part of the optimization target. If the policy can find surface patterns that fool the judge, training can inflate reward without improving real task quality. That is exactly why model-based verifiers, despite better static accuracy, can be vulnerable during RL training. Use them carefully, stress-test them, and combine them with hard checks whenever possible. (arXiv)
|
| 186 |
+
34) What is a common environment-design pitfall for tool-using agents?
|
| 187 |
+
Not modeling realistic failure modes. Real APIs fail because of permissions, invalid formats, missing fields, timezones, or bad parameters. Hugging Face’s OpenEnv blog highlights examples like missing OAuth scopes and bad RFC3339 datetime formatting. If the environment hides these realities, the resulting policy will be overfit to a toy setup and brittle in deployment. (Hugging Face)
|
| 188 |
+
35) Why is static task difficulty a problem?
|
| 189 |
+
Because the learning signal collapses at both extremes. Tasks that are too easy stop teaching the model anything useful. Tasks that are too hard yield near-zero reward and also stop teaching. RLVE was proposed largely to solve this problem by dynamically adjusting task difficulty as the policy improves. (arXiv)
|
| 190 |
+
36) What is a common pitfall in environment diversity?
|
| 191 |
+
Training on too few task types. Recent RLVE results argue that scaling the number of environments improves generalizable reasoning capability, and Reasoning Gym was built around procedurally generated tasks across many domains for exactly this reason. A narrow environment set often produces narrow competence and fragile transfer. (arXiv)
|
| 192 |
+
37) Why do many RL environments fail to transfer to real-world performance?
|
| 193 |
+
Because they optimize the wrong abstraction level. If the environment is too toy-like, omits realistic constraints, or over-simplifies tool feedback, the model may become good at the benchmark but not at the actual workflow. This is a practical version of specification gaming: the benchmark is solved, the real job is not. (Google DeepMind)
|
| 194 |
+
________________
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
Common Pitfalls in Reward Engineering
|
| 198 |
+
38) What is the biggest reward-engineering mistake?
|
| 199 |
+
Using a proxy metric as if it were the goal. Goodhart-style failures are everywhere in RL: token count, response format, test count, or intermediate progress can all become targets the model exploits. DeepMind’s examples of shaping mistakes and reward misspecification are the canonical warning here. (Google DeepMind)
|
| 200 |
+
39) Should I start with a complicated reward function?
|
| 201 |
+
Usually no. OpenEnv explicitly recommends starting simple, often with sparse success/failure reward, before layering in shaping terms. This makes debugging easier and reduces the chance that the model learns the wrong intermediate incentives before it learns the actual task. (Meta-PyTorch)
|
| 202 |
+
40) What happens when reward components conflict?
|
| 203 |
+
Learning becomes unstable or confused. OpenEnv lists conflicting signals as a common pitfall: if one term rewards brevity, another rewards verbosity, a third rewards format, and a fourth rewards exploration, the policy may oscillate or learn brittle shortcuts instead of coherent behavior. (Meta-PyTorch)
|
| 204 |
+
41) Why is binary reward often appealing?
|
| 205 |
+
Because it is easy to reason about and harder to game superficially. Label Studio’s RLVR overview notes that verifiable rewards are often binary and directly tied to correctness criteria, which makes evaluation simple and scalable. Binary reward is not always sufficient, but it is often a good starting point for precision-critical tasks like code and math. (Label Studio)
|
| 206 |
+
42) Why is binary reward sometimes not enough?
|
| 207 |
+
Because it can be too sparse, especially for long-horizon tasks. If success only happens at the very end, the model may not learn at all. That is where carefully designed shaping, step-level evaluation, or adaptive curriculum can help — but only if you can add them without creating easy-to-game shortcuts. (Meta-PyTorch)
|
| 208 |
+
43) How do I know whether my reward is being hacked?
|
| 209 |
+
Watch for rising reward without corresponding task-quality gains. Typical signs are strange formatting habits, repetitive surface patterns, degenerate short solutions, suspiciously high judge scores, or solutions that pass weak checks but fail stronger ones. The verifier case study is a strong reminder that static verification accuracy is not enough; you must observe what happens under optimization pressure. (arXiv)
|
| 210 |
+
44) What is a safe pattern for reward engineering?
|
| 211 |
+
Use layered verification. Start with hard outcome checks. Add anti-cheat constraints. Then add minimal shaping only where the sparse reward is too weak. Keep a holdout evaluator separate from the training reward when possible. This matches both OpenEnv’s “start simple, shape carefully” guidance and DeepMind’s warning about shaping altering the true objective. (Meta-PyTorch)
|
| 212 |
+
________________
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
Common Pitfalls in RL Post-Training Pipelines with RLVR / RLVE / GRPO
|
| 216 |
+
45) What is a common mistake in GRPO training runs?
|
| 217 |
+
Using RL before the base model is ready. GRPO is powerful, but it is a post-training method, not a substitute for capability. TRL’s own GRPO examples start from instruct models and task datasets rather than from weak base checkpoints. If the model almost never produces a correct rollout, the reward signal is too sparse for productive RL. (Hugging Face)
|
| 218 |
+
46) Why does RL post-training plateau?
|
| 219 |
+
Because the model saturates the available prompt distribution or the reward signal no longer differentiates useful improvements. RLVE explicitly frames static data saturation as a problem and shows that adaptive environments can keep learning going after conventional RLVR pipelines flatten out. (arXiv)
|
| 220 |
+
47) Why can “more RL” make a model worse?
|
| 221 |
+
Because optimization pressure amplifies whatever the reward favors, including undesirable shortcuts. If the verifier is noisy, if the environment is unrealistic, or if the reward overvalues superficial structure, more training can push the model deeper into those artifacts rather than improving real competence. (arXiv)
|
| 222 |
+
48) What is a common pitfall in RLVR datasets?
|
| 223 |
+
Finite, static datasets get stale. Once the model has mastered or overfit their distribution, additional RL yields little signal. RLVE work argues that procedurally generated environments with adjustable difficulty are one way around this limitation. Reasoning Gym makes a similar case for unlimited data generation with controllable complexity. (arXiv)
|
| 224 |
+
49) Why do identical-looking GRPO runs produce different outcomes?
|
| 225 |
+
Because RL is highly sensitive to rollout quality, verifier behavior, reward scaling, task mix, generation parameters, and environment bugs. Even if the trainer code is the same, small differences in reward computation or environment behavior can change optimization dynamics substantially. The verifier study is a good reminder that the reward pipeline itself is part of the model. (arXiv)
|
| 226 |
+
50) What is a common pitfall when mixing many environments?
|
| 227 |
+
Using an unbalanced mixture. If some environments are much easier, much denser in reward, or much shorter in trajectory length, they can dominate training and starve harder but more important environments. RLVE’s adaptive-difficulty framing exists partly to keep the training distribution informative instead of letting it collapse into easy tasks. (arXiv)
|
| 228 |
+
51) Why are long-horizon tasks especially hard in RL post-training?
|
| 229 |
+
Because reward arrives late and useful trajectories are rare. Long tasks need either decomposition, better intermediate signals, stronger initialization, or curriculum. Otherwise, the rollout cost is high and the success rate stays near zero. This is one reason why adaptive environments and procedural curricula are getting attention. (arXiv)
|
| 230 |
+
52) What monitoring mistake do teams make most often?
|
| 231 |
+
They monitor the training reward but not actual behavior. Reward alone is not enough because the reward channel can be flawed. You need sampled rollout audits, stronger offline evaluation, and held-out environments or benchmarks. The verifier case study shows why this matters: reward can rise while real quality does not. (arXiv)
|
| 232 |
+
53) What is the safest way to structure an RL post-training pipeline?
|
| 233 |
+
A good pattern is:
|
| 234 |
+
start from a strong instruct or SFT checkpoint, use a task with a strong verifier, begin with simple reward, validate the environment thoroughly, run small-scale debug experiments, audit rollouts manually, then scale training and only later add curriculum or more shaping. This is consistent with TRL’s practical GRPO examples, OpenEnv’s reward guidance, and the lessons from verifier-failure studies. (Hugging Face)
|
| 235 |
+
________________
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
Practical “What should we do in a hackathon?” FAQs
|
| 239 |
+
54) What kind of project is most likely to succeed in a hackathon?
|
| 240 |
+
Pick a task with:
|
| 241 |
+
a clear success condition,
|
| 242 |
+
a verifier you trust,
|
| 243 |
+
short to medium trajectory length,
|
| 244 |
+
few external dependencies,
|
| 245 |
+
and adjustable difficulty.
|
| 246 |
+
Good examples are code repair with tests, structured extraction with schema validation, grid or puzzle games, tool-using workflows with exact state checks, and browser tasks with explicit completion criteria. These are the sweet spot for RLVR and lightweight RLVE prototypes. (Label Studio)
|
| 247 |
+
55) What should we avoid building?
|
| 248 |
+
Avoid tasks that are subjective, hard to verify, require massive infrastructure, or depend heavily on an LLM judge without hard backstops. Also avoid environments whose failure cases you do not understand. If you cannot explain how the reward could be hacked, you are not ready to optimize it yet. (arXiv)
|
| 249 |
+
56) What is the best debugging order?
|
| 250 |
+
First debug the environment manually.
|
| 251 |
+
Then debug the verifier.
|
| 252 |
+
Then run scripted baseline policies.
|
| 253 |
+
Then run a frozen model.
|
| 254 |
+
Then run a tiny RL experiment.
|
| 255 |
+
Only then scale.
|
| 256 |
+
This order isolates bugs early and prevents you from blaming the optimizer for what is really an environment or reward bug. It follows directly from the fact that verifier reliability is foundational in RLVR. (arXiv)
|
| 257 |
+
57) What is one rule the team should remember?
|
| 258 |
+
Do not optimize a reward you have not tried to break yourself first. The easiest way to avoid reward hacking is to adversarially test your environment and reward design before the model does. (Google DeepMind)
|
| 259 |
+
________________
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
58) Strong references for deeper learning
|
| 263 |
+
For GRPO and TRL:
|
| 264 |
+
* TRL GRPO Trainer docs. (Hugging Face)
|
| 265 |
+
* Hugging Face GRPO cookbook. (Hugging Face)
|
| 266 |
+
For RL environments and reward design:
|
| 267 |
+
* OpenEnv reward design guide. (Meta-PyTorch)
|
| 268 |
+
* OpenEnv tool-using environment examples. (Hugging Face)
|
| 269 |
+
For pitfalls and failure modes:
|
| 270 |
+
* DeepMind on specification gaming. (Google DeepMind)
|
| 271 |
+
* Pitfalls of rule-based and model-based verifiers. (arXiv)
|
| 272 |
+
For scalable environment-based training:
|
| 273 |
+
* RLVE paper on adaptive verifiable environments. (arXiv)
|
| 274 |
+
* Reasoning Gym. (OpenReview)
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
Here are solid Unsloth RL post-training recipes worth checking out, with a bias toward official or close-to-official examples.
|
| 278 |
+
59) Core Unsloth GRPO recipes
|
| 279 |
+
Qwen2.5 (3B) GRPO notebook
|
| 280 |
+
A straightforward starter recipe for GRPO with Unsloth. It covers data prep, training, inference, and saving, so it is a good baseline if you want the least opinionated end-to-end example. (GitHub)
|
| 281 |
+
Llama 3.1 (8B) GRPO notebook
|
| 282 |
+
Same general pattern, but on a larger model family. Useful if you want a more realistic “reasoning/capability uplift” recipe without jumping straight to very large models. (GitHub)
|
| 283 |
+
Gemma 3 (1B) GRPO notebook
|
| 284 |
+
A smaller-scale recipe that is easier to run and debug. Good for iterating on reward functions and rollout settings before spending more compute on larger checkpoints. (GitHub)
|
| 285 |
+
59.1) Advanced Unsloth GRPO recipes
|
| 286 |
+
Advanced Qwen3 (4B) GRPO notebook
|
| 287 |
+
This is one of the more interesting recipes because it adds more than the bare trainer loop. Unsloth’s June 2025 discussion explicitly calls out: proximity scoring for more nuanced rewards, OpenR1 dataset support, advanced templates, and “prefinetuning to skip GRPO format learning.” That makes it a better recipe when you care about reward shaping and format bootstrapping, not just getting GRPO to run. (GitHub)
|
| 288 |
+
HF LLM Course: Practical Exercise — GRPO with Unsloth
|
| 289 |
+
Not an Unsloth-maintained notebook repo entry, but it is a structured learning recipe that uses Unsloth specifically to fine-tune a model with GRPO for reasoning. It is a good companion when you want a didactic walkthrough instead of just notebook cells. (Hugging Face)
|
| 290 |
+
59.2) Environment / agent-style RL recipes
|
| 291 |
+
GPT-OSS 20B + 2048 game RL notebook
|
| 292 |
+
This is closer to “RL with an environment” than plain static-prompt RLVR. The notebook goal is explicitly to make GPT-OSS play 2048 with reinforcement learning / GRPO, which makes it a useful recipe if you want to move beyond math/code answer verification into interactive environment training. (GitHub)
|
| 293 |
+
59.3) Broader recipe collection
|
| 294 |
+
Unsloth notebooks repository
|
| 295 |
+
The main repo currently advertises “250+ Fine-tuning & RL Notebooks,” including GRPO and reinforcement learning notebooks. If you want the widest set of recipes in one place, this is the best starting point. (GitHub)
|
| 296 |
+
59.4) Useful adjacent recipes and examples
|
| 297 |
+
Scheduler GRPO example using Unsloth
|
| 298 |
+
A community example that trains a scheduling model with GRPO using Unsloth and QLoRA. It is useful because it shows a non-math, non-code structured-output task where rewards are tied to output format and schedule correctness. (Hugging Face)
|
| 299 |
+
SFT → GRPO pipeline example
|
| 300 |
+
There is a community “show and tell” example for a full SFT-then-GRPO pipeline. I would treat it as inspiration rather than an official recipe, but it is valuable if your intended workflow is “teach format first, then do RL.” (GitHub)
|
| 301 |
+
59.5) What these recipes collectively cover
|
| 302 |
+
Across these examples, the main recipe patterns are:
|
| 303 |
+
* plain GRPO on reasoning-style tasks,
|
| 304 |
+
* GRPO with better reward shaping like proximity scoring,
|
| 305 |
+
* pre-SFT or preformatting before RL,
|
| 306 |
+
* QLoRA-based memory-efficient RL fine-tuning,
|
| 307 |
+
* and environment-style RL with game interaction. (GitHub)
|
| 308 |
+
59.6) Two gaps to keep in mind
|
| 309 |
+
One gap is multi-turn GRPO with stepwise rewards. There is a feature request asking for reward on each step plus a final reward, which suggests this is not yet a mature first-class recipe in Unsloth. (GitHub)
|
| 310 |
+
Another gap is notebook stability across versions/hardware. Several issue threads mention breakage or edge cases in GRPO notebooks, including fast inference assumptions, VRAM growth, and vision-GRPO issues. That does not make the recipes unusable, but it does mean you should pin versions and test on a small run first. (GitHub)
|
| 311 |
+
59.7) Best recipes by use case
|
| 312 |
+
If you want the simplest starting point:
|
| 313 |
+
* Qwen2.5 (3B) GRPO
|
| 314 |
+
* Gemma 3 (1B) GRPO (GitHub)
|
| 315 |
+
If you care about reward engineering:
|
| 316 |
+
* Advanced Qwen3 (4B) GRPO (GitHub)
|
| 317 |
+
If you care about environment-style RL:
|
| 318 |
+
* GPT-OSS 20B 2048 notebook (GitHub)
|
| 319 |
+
If you want the most guided learning path:
|
| 320 |
+
* HF practical exercise with Unsloth + GRPO (Hugging Face)
|
| 321 |
+
If helpful, I can turn this into a curated table with columns for model, task type, reward type, hardware footprint, and what each recipe teaches.
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
Additional Resources:
|
| 325 |
+
* OpenEnv Core (An interface library for RL post training with environments)
|
| 326 |
+
* https://github.com/meta-pytorch/OpenEnv
|
| 327 |
+
* OpenEnv-PyTorch Docs
|
| 328 |
+
* https://meta-pytorch.org/OpenEnv/
|
| 329 |
+
* HuggingFace OpenEnv Environments Hub
|
| 330 |
+
* https://huggingface.co/openenv
|
| 331 |
+
* https://huggingface.co/openenv/spaces
|
| 332 |
+
* Tutorials to build, run and train RL environments and training pipelines
|
| 333 |
+
* https://github.com/meta-pytorch/OpenEnv/tree/main/tutorial
|
| 334 |
+
* RL Training Examples: https://github.com/meta-pytorch/OpenEnv/tree/main/tutorial/examples
|
| 335 |
+
* RL Environment Examples: https://github.com/meta-pytorch/OpenEnv/tree/main/envs
|
| 336 |
+
* Few additional YT Videos on building RL Environments:
|
| 337 |
+
* https://www.youtube.com/watch?v=0airz7BhBiA
|
| 338 |
+
* https://www.youtube.com/watch?v=ap4q4sAK4OY
|
| 339 |
+
* https://www.youtube.com/watch?v=Jew4lhAiqnw (Recommended)
|
requirements-train.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.2.0
|
| 2 |
+
transformers>=4.44.0
|
| 3 |
+
accelerate>=1.0.0
|
| 4 |
+
trl>=0.9.0
|
| 5 |
+
peft>=0.10.0
|
| 6 |
+
datasets>=2.18.0
|
| 7 |
+
matplotlib>=3.8.0
|
requirements-unsloth.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
unsloth
|
| 2 |
+
unsloth_zoo
|
| 3 |
+
torch>=2.2.0
|
| 4 |
+
transformers>=4.44.0
|
| 5 |
+
trl>=0.9.0
|
| 6 |
+
peft>=0.10.0
|
| 7 |
+
accelerate>=1.0.0
|
| 8 |
+
datasets>=2.18.0
|
| 9 |
+
matplotlib>=3.8.0
|
| 10 |
+
bitsandbytes>=0.43.0
|
server/app.py
CHANGED
|
@@ -46,6 +46,14 @@ _LANDING_PAGE = """\
|
|
| 46 |
<h1>⚛️ CERNenv</h1>
|
| 47 |
<p class=muted>An LHC (Large Hadron Collider) particle-discovery RL environment for autonomous physicist agents — built for the Meta OpenEnv Hackathon.</p>
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
<p>
|
| 50 |
<span class=pill>OpenEnv</span>
|
| 51 |
<span class=pill>POMDP</span>
|
|
@@ -124,7 +132,9 @@ def build_app(
|
|
| 124 |
|
| 125 |
The OpenEnv-provided routes (`/reset`, `/step`, `/state`, `/schema`,
|
| 126 |
`/health`, `/mcp`) come from ``create_fastapi_app``. We then mount a
|
| 127 |
-
friendly landing page at ``/`` so the Space preview is informative
|
|
|
|
|
|
|
| 128 |
"""
|
| 129 |
factory = make_env_factory(max_steps=max_steps, default_difficulty=default_difficulty)
|
| 130 |
fa_app = create_fastapi_app(factory, ExperimentAction, CollisionObservation)
|
|
@@ -133,6 +143,22 @@ def build_app(
|
|
| 133 |
def landing() -> HTMLResponse: # pragma: no cover - trivial
|
| 134 |
return HTMLResponse(_LANDING_PAGE)
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
return fa_app
|
| 137 |
|
| 138 |
|
|
|
|
| 46 |
<h1>⚛️ CERNenv</h1>
|
| 47 |
<p class=muted>An LHC (Large Hadron Collider) particle-discovery RL environment for autonomous physicist agents — built for the Meta OpenEnv Hackathon.</p>
|
| 48 |
|
| 49 |
+
<p style="margin: 1rem 0">
|
| 50 |
+
<a href="/demo" style="display:inline-block; background:#1d4ed8; color:white;
|
| 51 |
+
padding:.6rem 1.1rem; border-radius:6px; text-decoration:none;
|
| 52 |
+
font-weight:600; box-shadow:0 1px 3px rgba(0,0,0,.1)">
|
| 53 |
+
▶ Try the interactive demo at /demo
|
| 54 |
+
</a>
|
| 55 |
+
</p>
|
| 56 |
+
|
| 57 |
<p>
|
| 58 |
<span class=pill>OpenEnv</span>
|
| 59 |
<span class=pill>POMDP</span>
|
|
|
|
| 132 |
|
| 133 |
The OpenEnv-provided routes (`/reset`, `/step`, `/state`, `/schema`,
|
| 134 |
`/health`, `/mcp`) come from ``create_fastapi_app``. We then mount a
|
| 135 |
+
friendly landing page at ``/`` so the Space preview is informative,
|
| 136 |
+
and an interactive Gradio demo at ``/demo`` so non-technical visitors
|
| 137 |
+
can play with the env in the browser without writing any code.
|
| 138 |
"""
|
| 139 |
factory = make_env_factory(max_steps=max_steps, default_difficulty=default_difficulty)
|
| 140 |
fa_app = create_fastapi_app(factory, ExperimentAction, CollisionObservation)
|
|
|
|
| 143 |
def landing() -> HTMLResponse: # pragma: no cover - trivial
|
| 144 |
return HTMLResponse(_LANDING_PAGE)
|
| 145 |
|
| 146 |
+
# Best-effort mount of the Gradio interactive demo at /demo. We only
|
| 147 |
+
# *add* a route here — the OpenEnv HTTP API at /health, /reset, /step,
|
| 148 |
+
# /state, /schema, /mcp, /docs is unchanged. If gradio isn't installed
|
| 149 |
+
# in the runtime environment (e.g. during minimal CI), we log and skip.
|
| 150 |
+
try:
|
| 151 |
+
import gradio as gr # noqa: F401 (presence check)
|
| 152 |
+
from space.env.gradio_demo import build_gradio_demo
|
| 153 |
+
|
| 154 |
+
demo = build_gradio_demo()
|
| 155 |
+
fa_app = gr.mount_gradio_app(fa_app, demo, path="/demo")
|
| 156 |
+
except Exception as exc: # pragma: no cover - optional dep
|
| 157 |
+
import logging
|
| 158 |
+
logging.getLogger(__name__).warning(
|
| 159 |
+
"Gradio demo not mounted at /demo: %s", exc
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
return fa_app
|
| 163 |
|
| 164 |
|
space/env/gradio_demo.py
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Interactive Gradio demo for CERNenv.
|
| 2 |
+
|
| 3 |
+
A small, dependency-light Blocks app a non-technical visitor can use to
|
| 4 |
+
*play* with the LHC discovery environment in the browser:
|
| 5 |
+
|
| 6 |
+
* Tab 1 — *Watch a baseline*: pick a scenario, run a Random / Heuristic /
|
| 7 |
+
Oracle agent, watch its action+reward trace stream in, see whether it
|
| 8 |
+
found the hidden particle.
|
| 9 |
+
* Tab 2 — *Build your own actions*: an "Action Builder" that lets the
|
| 10 |
+
visitor pick one of the 16 ``ActionType`` values, fill in a few
|
| 11 |
+
parameters, and step the env one action at a time. State persists
|
| 12 |
+
across clicks via ``gr.State``.
|
| 13 |
+
* Tab 3 — *About*: a tight one-screen explanation of what the env is and
|
| 14 |
+
why this is interesting RL data for LLMs.
|
| 15 |
+
|
| 16 |
+
This module is mounted onto the Space's existing FastAPI app at ``/demo``
|
| 17 |
+
by ``server.app.build_app``. It does **not** alter the OpenEnv HTTP API
|
| 18 |
+
(``/health``, ``/reset``, ``/step``, ``/state``, ``/schema``, ``/mcp``).
|
| 19 |
+
|
| 20 |
+
No heavy ML deps (torch / transformers / trl) are imported here — the
|
| 21 |
+
env Space runs on ``cpu-basic`` and only needs gradio + pydantic + numpy.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import logging
|
| 27 |
+
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
| 28 |
+
|
| 29 |
+
import gradio as gr
|
| 30 |
+
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# ── Scenario / agent registries ─────────────────────────────────────────
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# Visible labels in the dropdown. Curated scenarios use their canonical
|
| 38 |
+
# names so the UI matches what the docs / scripts use.
|
| 39 |
+
SCENARIO_CHOICES: List[Tuple[str, str]] = [
|
| 40 |
+
("easy_diphoton_160 — narrow scalar at 160 GeV (tutorial)", "easy_diphoton_160"),
|
| 41 |
+
("higgs_like_125 — Higgs-like scalar at 125 GeV", "higgs_like_125"),
|
| 42 |
+
("hidden_zprime_600 — heavy Z' at 600 GeV", "hidden_zprime_600"),
|
| 43 |
+
("diphoton_750 — 2015 diphoton excess re-investigation", "diphoton_750"),
|
| 44 |
+
("random easy", "__random_easy__"),
|
| 45 |
+
("random medium", "__random_medium__"),
|
| 46 |
+
("random hard", "__random_hard__"),
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
_LABEL_TO_VALUE: Dict[str, str] = {lab: val for lab, val in SCENARIO_CHOICES}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _resolve_scenario(label_or_value: str) -> Dict[str, Any]:
|
| 53 |
+
"""Map a dropdown selection (label OR canonical value) to ``reset()`` kwargs."""
|
| 54 |
+
value = _LABEL_TO_VALUE.get(label_or_value, label_or_value)
|
| 55 |
+
if value.startswith("__random_"):
|
| 56 |
+
difficulty = value.strip("_").replace("random_", "")
|
| 57 |
+
return {"difficulty": difficulty, "scenario": None}
|
| 58 |
+
return {"scenario": value, "difficulty": None}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
AGENT_CHOICES = ["random", "heuristic", "oracle"]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ── Helpers for rendering observations ──────────────────────────────────
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _resource_progress_md(usage) -> str:
|
| 68 |
+
"""Pretty resource bars as one Markdown block."""
|
| 69 |
+
def _bar(label: str, used: float, total: float, unit: str) -> str:
|
| 70 |
+
if total <= 0:
|
| 71 |
+
pct = 0.0
|
| 72 |
+
else:
|
| 73 |
+
pct = max(0.0, min(1.0, used / total))
|
| 74 |
+
n = 24
|
| 75 |
+
filled = int(round(pct * n))
|
| 76 |
+
bar = "█" * filled + "░" * (n - filled)
|
| 77 |
+
return f"`{label:<10}` `{bar}` **{used:.1f} / {total:.1f} {unit}**"
|
| 78 |
+
|
| 79 |
+
budget_total = usage.budget_used_musd + usage.budget_remaining_musd
|
| 80 |
+
lumi_total = usage.luminosity_used_fb + usage.luminosity_remaining_fb
|
| 81 |
+
time_total = usage.time_used_days + usage.time_remaining_days
|
| 82 |
+
return "\n\n".join([
|
| 83 |
+
_bar("budget", usage.budget_used_musd, budget_total, "M$"),
|
| 84 |
+
_bar("lumi", usage.luminosity_used_fb, lumi_total, "fb⁻¹"),
|
| 85 |
+
_bar("time", usage.time_used_days, time_total, "days"),
|
| 86 |
+
])
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _candidates_table(obs) -> List[List[Any]]:
|
| 90 |
+
rows: List[List[Any]] = []
|
| 91 |
+
masses = list(obs.candidate_masses_gev or [])
|
| 92 |
+
sigs = list(obs.candidate_significances or [])
|
| 93 |
+
for i in range(max(len(masses), len(sigs))):
|
| 94 |
+
m = masses[i] if i < len(masses) else None
|
| 95 |
+
s = sigs[i] if i < len(sigs) else None
|
| 96 |
+
rows.append([
|
| 97 |
+
i,
|
| 98 |
+
f"{m:.2f}" if isinstance(m, (int, float)) else "—",
|
| 99 |
+
f"{s:.2f}" if isinstance(s, (int, float)) else "—",
|
| 100 |
+
])
|
| 101 |
+
if not rows:
|
| 102 |
+
rows = [[0, "—", "—"]]
|
| 103 |
+
return rows
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _step_breakdown_table(breakdown: Dict[str, float]) -> List[List[Any]]:
|
| 107 |
+
if not breakdown:
|
| 108 |
+
return [["—", 0.0]]
|
| 109 |
+
return [[k, round(float(v), 4)] for k, v in breakdown.items()]
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _violations_md(violations: List[str]) -> str:
|
| 113 |
+
if not violations:
|
| 114 |
+
return "*(no rule violations)*"
|
| 115 |
+
items = "\n".join(f"- ⚠️ `{v}`" for v in violations)
|
| 116 |
+
return f"<span style='color:#c00'>**Rule violations**</span>\n\n{items}"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _truth_md(truth: Optional[Dict[str, Any]], state) -> str:
|
| 120 |
+
"""Hidden-truth reveal: the latent particle the agent was trying to find."""
|
| 121 |
+
if not truth:
|
| 122 |
+
return "*(no episode loaded yet)*"
|
| 123 |
+
rows = [
|
| 124 |
+
f"- **name**: `{truth.get('name', '?')}`",
|
| 125 |
+
f"- **mass**: `{float(truth.get('mass_gev', 0.0)):.2f}` GeV",
|
| 126 |
+
f"- **width**: `{float(truth.get('width_gev', 0.0)):.4f}` GeV",
|
| 127 |
+
f"- **spin / parity**: `{int(truth.get('spin', 0))}{truth.get('parity', '?')}`",
|
| 128 |
+
f"- **primary channel**: `{truth.get('primary_channel', '?')}`",
|
| 129 |
+
f"- **cross-section**: `{float(truth.get('cross_section_fb', 0.0)):.2f}` fb",
|
| 130 |
+
]
|
| 131 |
+
if state is not None:
|
| 132 |
+
flags = []
|
| 133 |
+
if state.discovered is not None:
|
| 134 |
+
flags.append(("discovered", state.discovered))
|
| 135 |
+
if state.correct_mass is not None:
|
| 136 |
+
flags.append(("correct mass", state.correct_mass))
|
| 137 |
+
if state.correct_channel is not None:
|
| 138 |
+
flags.append(("correct channel", state.correct_channel))
|
| 139 |
+
if state.correct_spin is not None:
|
| 140 |
+
flags.append(("correct spin", state.correct_spin))
|
| 141 |
+
if flags:
|
| 142 |
+
verdict_lines = []
|
| 143 |
+
for label, ok in flags:
|
| 144 |
+
badge = "✅" if ok else "❌"
|
| 145 |
+
verdict_lines.append(f"- {badge} **{label}**: `{ok}`")
|
| 146 |
+
rows.append("\n**Verdict vs. agent claim**\n" + "\n".join(verdict_lines))
|
| 147 |
+
return "\n".join(rows)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def _format_action(action) -> str:
|
| 151 |
+
parts = [f"`{action.action_type.value}`"]
|
| 152 |
+
if action.method:
|
| 153 |
+
parts.append(f"method=`{action.method}`")
|
| 154 |
+
if action.parameters:
|
| 155 |
+
# Truncate verbose parameter dicts (e.g. claim) so the log stays readable.
|
| 156 |
+
s = str(action.parameters)
|
| 157 |
+
if len(s) > 80:
|
| 158 |
+
s = s[:77] + "…"
|
| 159 |
+
parts.append(f"params={s}")
|
| 160 |
+
return " ".join(parts)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ── Tab 1: watch a baseline ─────────────────────────────────────────────
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _stream_baseline(
|
| 167 |
+
scenario_label: str,
|
| 168 |
+
seed: int,
|
| 169 |
+
agent_name: str,
|
| 170 |
+
max_steps: int = 40,
|
| 171 |
+
) -> Iterator[Tuple[str, str, str, str, List[List[Any]], str, str, str]]:
|
| 172 |
+
"""Run a full episode in-process; yield UI updates per step."""
|
| 173 |
+
# Lazy imports so this module is cheap to import (and trivially testable).
|
| 174 |
+
from server.environment import CERNCollisionEnvironment
|
| 175 |
+
from scripts.baseline_agents import HeuristicAgent, OracleAgent, RandomAgent
|
| 176 |
+
|
| 177 |
+
agent_cls = {"random": RandomAgent, "heuristic": HeuristicAgent, "oracle": OracleAgent}[agent_name]
|
| 178 |
+
agent = agent_cls(seed=int(seed)) if agent_name == "random" else agent_cls()
|
| 179 |
+
|
| 180 |
+
env = CERNCollisionEnvironment(max_steps=int(max_steps))
|
| 181 |
+
reset_kwargs = _resolve_scenario(scenario_label)
|
| 182 |
+
obs = env.reset(seed=int(seed), **reset_kwargs)
|
| 183 |
+
|
| 184 |
+
if agent_name == "oracle":
|
| 185 |
+
agent.truth = env.hidden_truth()
|
| 186 |
+
agent.reset()
|
| 187 |
+
|
| 188 |
+
log_lines: List[str] = [
|
| 189 |
+
f"### Running **{agent_name}** agent on `{env.state.scenario_name}` "
|
| 190 |
+
f"(difficulty=`{env.state.difficulty}`, seed=`{seed}`)\n",
|
| 191 |
+
"```",
|
| 192 |
+
f"{'step':>4} {'action':<24} {'reward':>8} {'cum.':>8} done",
|
| 193 |
+
"-" * 60,
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
cumulative = 0.0
|
| 197 |
+
truth = env.hidden_truth()
|
| 198 |
+
|
| 199 |
+
yield (
|
| 200 |
+
log_lines[0] + "\n" + "\n".join(log_lines[1:]) + "\n```",
|
| 201 |
+
"0.0", "0", "0.0",
|
| 202 |
+
_candidates_table(obs),
|
| 203 |
+
_resource_progress_md(obs.resource_usage),
|
| 204 |
+
_violations_md(obs.rule_violations or []),
|
| 205 |
+
_truth_md(truth, env.state) if obs.done else "*(truth revealed when the episode ends)*",
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
while not obs.done:
|
| 209 |
+
action = agent.act(obs)
|
| 210 |
+
obs = env.step(action)
|
| 211 |
+
rew = float(obs.reward or 0.0)
|
| 212 |
+
cumulative += rew
|
| 213 |
+
log_lines.append(
|
| 214 |
+
f"{obs.step_index:>4} {action.action_type.value:<24} "
|
| 215 |
+
f"{rew:>+8.3f} {cumulative:>+8.3f} {obs.done}"
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
body = log_lines[0] + "\n" + "\n".join(log_lines[1:]) + "\n```"
|
| 219 |
+
yield (
|
| 220 |
+
body,
|
| 221 |
+
f"{cumulative:+.3f}",
|
| 222 |
+
str(obs.step_index),
|
| 223 |
+
f"{obs.cumulative_significance:.2f}",
|
| 224 |
+
_candidates_table(obs),
|
| 225 |
+
_resource_progress_md(obs.resource_usage),
|
| 226 |
+
_violations_md(obs.rule_violations or []),
|
| 227 |
+
_truth_md(truth, env.state) if obs.done else "*(truth revealed when the episode ends)*",
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Append final claim summary
|
| 231 |
+
log_lines.append("-" * 60)
|
| 232 |
+
log_lines.append(
|
| 233 |
+
f"final cumulative_reward={cumulative:+.3f} "
|
| 234 |
+
f"terminal_reward={env.state.terminal_reward} "
|
| 235 |
+
f"discovered={env.state.discovered}"
|
| 236 |
+
)
|
| 237 |
+
body = log_lines[0] + "\n" + "\n".join(log_lines[1:]) + "\n```"
|
| 238 |
+
yield (
|
| 239 |
+
body,
|
| 240 |
+
f"{cumulative:+.3f}",
|
| 241 |
+
str(obs.step_index),
|
| 242 |
+
f"{obs.cumulative_significance:.2f}",
|
| 243 |
+
_candidates_table(obs),
|
| 244 |
+
_resource_progress_md(obs.resource_usage),
|
| 245 |
+
_violations_md(obs.rule_violations or []),
|
| 246 |
+
_truth_md(truth, env.state),
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
# ── Tab 2: build your own actions ───────────────────────────────────────
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def _new_episode(scenario_label: str, seed: int) -> Tuple[Any, Any, str, str, str, List[List[Any]], List[List[Any]], str, str, str]:
|
| 254 |
+
"""Start a fresh episode for the manual Action Builder. Returns the
|
| 255 |
+
(env, obs) pair to be stored in ``gr.State`` and a fresh set of UI
|
| 256 |
+
values."""
|
| 257 |
+
from server.environment import CERNCollisionEnvironment
|
| 258 |
+
|
| 259 |
+
env = CERNCollisionEnvironment(max_steps=40)
|
| 260 |
+
reset_kwargs = _resolve_scenario(scenario_label)
|
| 261 |
+
obs = env.reset(seed=int(seed), **reset_kwargs)
|
| 262 |
+
|
| 263 |
+
header = (
|
| 264 |
+
f"### Episode started — scenario `{env.state.scenario_name}` "
|
| 265 |
+
f"(difficulty=`{env.state.difficulty}`, seed=`{seed}`)\n\n"
|
| 266 |
+
f"**Mass search window**: `{obs.task.mass_search_window_gev[0]:.0f} – "
|
| 267 |
+
f"{obs.task.mass_search_window_gev[1]:.0f}` GeV\n\n"
|
| 268 |
+
f"_Pick an action below and click **Submit step**._"
|
| 269 |
+
)
|
| 270 |
+
return (
|
| 271 |
+
env, # gr.State env
|
| 272 |
+
obs, # gr.State obs
|
| 273 |
+
header, # status_md
|
| 274 |
+
"0.0", # cumulative_reward
|
| 275 |
+
"0", # step_index
|
| 276 |
+
_step_breakdown_table({}), # step_breakdown
|
| 277 |
+
_candidates_table(obs), # candidates
|
| 278 |
+
_resource_progress_md(obs.resource_usage), # resources
|
| 279 |
+
_violations_md([]), # violations
|
| 280 |
+
"*(submit a `submit_discovery_claim` or run out of budget to reveal)*",
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def _submit_step(
|
| 285 |
+
env,
|
| 286 |
+
obs,
|
| 287 |
+
action_type: str,
|
| 288 |
+
method: str,
|
| 289 |
+
channel: str,
|
| 290 |
+
trigger: str,
|
| 291 |
+
beam_energy: str,
|
| 292 |
+
luminosity_fb: float,
|
| 293 |
+
mass_window_lo: float,
|
| 294 |
+
mass_window_hi: float,
|
| 295 |
+
claim_mass: float,
|
| 296 |
+
claim_sigma: float,
|
| 297 |
+
claim_spin: int,
|
| 298 |
+
claim_parity: str,
|
| 299 |
+
confidence: float,
|
| 300 |
+
) -> Tuple[Any, Any, str, str, str, List[List[Any]], List[List[Any]], str, str, str]:
|
| 301 |
+
"""Apply one manual action to the persisted ``env``."""
|
| 302 |
+
from models import ActionType, ExperimentAction
|
| 303 |
+
|
| 304 |
+
if env is None or obs is None:
|
| 305 |
+
return (
|
| 306 |
+
env, obs,
|
| 307 |
+
"_No active episode — click **New episode** first._",
|
| 308 |
+
"0.0", "0",
|
| 309 |
+
_step_breakdown_table({}),
|
| 310 |
+
[[0, "—", "—"]],
|
| 311 |
+
"_(no episode)_",
|
| 312 |
+
_violations_md([]),
|
| 313 |
+
"_(no episode)_",
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
if obs.done:
|
| 317 |
+
return (
|
| 318 |
+
env, obs,
|
| 319 |
+
"_Episode is already over — click **New episode** to start fresh._",
|
| 320 |
+
f"{env.state.cumulative_reward:+.3f}",
|
| 321 |
+
str(obs.step_index),
|
| 322 |
+
_step_breakdown_table(obs.step_reward_breakdown or {}),
|
| 323 |
+
_candidates_table(obs),
|
| 324 |
+
_resource_progress_md(obs.resource_usage),
|
| 325 |
+
_violations_md(obs.rule_violations or []),
|
| 326 |
+
_truth_md(env.hidden_truth(), env.state),
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
try:
|
| 330 |
+
at = ActionType(action_type)
|
| 331 |
+
except Exception:
|
| 332 |
+
return (
|
| 333 |
+
env, obs,
|
| 334 |
+
f"_Invalid action_type: `{action_type}`._",
|
| 335 |
+
f"{env.state.cumulative_reward:+.3f}",
|
| 336 |
+
str(obs.step_index),
|
| 337 |
+
_step_breakdown_table(obs.step_reward_breakdown or {}),
|
| 338 |
+
_candidates_table(obs),
|
| 339 |
+
_resource_progress_md(obs.resource_usage),
|
| 340 |
+
_violations_md(obs.rule_violations or []),
|
| 341 |
+
"*(truth shown at end of episode)*",
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
# Build parameter dict from the relevant fields. We keep this minimal
|
| 345 |
+
# and forgiving — the env's RulesEngine will reject anything illegal.
|
| 346 |
+
params: Dict[str, Any] = {}
|
| 347 |
+
if at == ActionType.CONFIGURE_BEAM and beam_energy:
|
| 348 |
+
params["beam_energy"] = beam_energy
|
| 349 |
+
if at == ActionType.SELECT_CHANNEL and channel:
|
| 350 |
+
params["channel"] = channel
|
| 351 |
+
if at == ActionType.SET_TRIGGER and trigger:
|
| 352 |
+
params["trigger"] = trigger
|
| 353 |
+
if at in (ActionType.ALLOCATE_LUMINOSITY, ActionType.COLLECT_COLLISIONS):
|
| 354 |
+
params["luminosity_fb"] = float(luminosity_fb or 0.0)
|
| 355 |
+
if at == ActionType.BUILD_INVARIANT_MASS:
|
| 356 |
+
lo = float(mass_window_lo or obs.task.mass_search_window_gev[0])
|
| 357 |
+
hi = float(mass_window_hi or obs.task.mass_search_window_gev[1])
|
| 358 |
+
params["mass_window_gev"] = [lo, hi]
|
| 359 |
+
if at == ActionType.SUBMIT_DISCOVERY_CLAIM:
|
| 360 |
+
params["claim"] = {
|
| 361 |
+
"mass_estimate_gev": float(claim_mass or 0.0) or None,
|
| 362 |
+
"mass_uncertainty_gev": 1.0,
|
| 363 |
+
"significance_sigma": float(claim_sigma or 0.0) or None,
|
| 364 |
+
"decay_channel": (channel or obs.selected_channel or "diphoton"),
|
| 365 |
+
"spin_hypothesis": int(claim_spin),
|
| 366 |
+
"parity": claim_parity or "+",
|
| 367 |
+
"confidence": float(confidence or 0.5),
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
action = ExperimentAction(
|
| 371 |
+
action_type=at,
|
| 372 |
+
method=(method or None),
|
| 373 |
+
parameters=params,
|
| 374 |
+
confidence=float(confidence or 0.5),
|
| 375 |
+
justification="manual action from /demo Action Builder",
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
new_obs = env.step(action)
|
| 379 |
+
|
| 380 |
+
status = (
|
| 381 |
+
f"### Step `{new_obs.step_index}` — `{at.value}` "
|
| 382 |
+
f"→ reward `{(new_obs.reward or 0.0):+.3f}`\n\n"
|
| 383 |
+
)
|
| 384 |
+
if new_obs.latest_output is not None:
|
| 385 |
+
status += f"**Output** (`{new_obs.latest_output.output_type.value}`): {new_obs.latest_output.summary}\n"
|
| 386 |
+
if new_obs.done:
|
| 387 |
+
status += "\n*Episode complete — see the Hidden Truth panel below.*"
|
| 388 |
+
|
| 389 |
+
truth_md = (
|
| 390 |
+
_truth_md(env.hidden_truth(), env.state)
|
| 391 |
+
if new_obs.done
|
| 392 |
+
else "*(truth revealed when the episode ends)*"
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
return (
|
| 396 |
+
env,
|
| 397 |
+
new_obs,
|
| 398 |
+
status,
|
| 399 |
+
f"{env.state.cumulative_reward:+.3f}",
|
| 400 |
+
str(new_obs.step_index),
|
| 401 |
+
_step_breakdown_table(new_obs.step_reward_breakdown or {}),
|
| 402 |
+
_candidates_table(new_obs),
|
| 403 |
+
_resource_progress_md(new_obs.resource_usage),
|
| 404 |
+
_violations_md(new_obs.rule_violations or []),
|
| 405 |
+
truth_md,
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
# ── Top-level Blocks ────────────────────────────────────────────────────
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
_HEADER_MD = """\
|
| 413 |
+
# ⚛️ CERNenv — interactive demo
|
| 414 |
+
|
| 415 |
+
You are a high-energy physicist running an analysis at the **Large Hadron Collider**.
|
| 416 |
+
There is a hidden particle out there — somewhere in a search window of mass —
|
| 417 |
+
and your job is to discover and characterise it.
|
| 418 |
+
|
| 419 |
+
Each step you pick **one structured action** (configure the beam, allocate luminosity,
|
| 420 |
+
fit a resonance, estimate significance, submit a discovery claim, …) and the
|
| 421 |
+
environment hands back a noisy detector-style observation. Wrong channel,
|
| 422 |
+
mis-matched trigger, or wasteful step? You burn budget and don't see the signal.
|
| 423 |
+
Make a calibrated 5σ discovery claim before you run out of resources, and you win.
|
| 424 |
+
|
| 425 |
+
This page lets you **watch a baseline agent run an episode**, or **drive the
|
| 426 |
+
environment yourself** action-by-action. The hidden particle is revealed at the end
|
| 427 |
+
of every episode so you can see what the agent was actually trying to find.
|
| 428 |
+
"""
|
| 429 |
+
|
| 430 |
+
_ABOUT_MD = """\
|
| 431 |
+
## About this environment
|
| 432 |
+
|
| 433 |
+
`CERNenv` is a partially-observable Markov decision process (POMDP) modelled on
|
| 434 |
+
LHC particle-discovery campaigns. It is a research environment for training and
|
| 435 |
+
evaluating LLM agents on a *real-shaped* scientific task with prerequisites,
|
| 436 |
+
budgets, calibration, systematics, and structured discovery claims.
|
| 437 |
+
|
| 438 |
+
* **16 structured actions** — DAQ, reconstruction, calibration, analysis,
|
| 439 |
+
systematics, theory review, discovery claim.
|
| 440 |
+
* **Hidden ground truth per episode** — mass, width, spin, parity, primary
|
| 441 |
+
decay channel, cross-section. The agent never sees these directly.
|
| 442 |
+
* **Curated scenarios** inspired by famous LHC discoveries (Higgs-like 125 GeV,
|
| 443 |
+
hidden Z', the 2015 750 GeV diphoton excess) plus a procedural curriculum at
|
| 444 |
+
three difficulty tiers.
|
| 445 |
+
* **Reward decomposition** — per-step shaping (good prerequisites, tool fit,
|
| 446 |
+
rule compliance) plus a dominant terminal reward calibrated against the
|
| 447 |
+
hidden particle.
|
| 448 |
+
* **OpenEnv-compatible HTTP API** at `/health`, `/reset`, `/step`, `/state`,
|
| 449 |
+
`/schema`, `/mcp`, `/docs` — see the landing page for examples.
|
| 450 |
+
|
| 451 |
+
### Hackathon submission angle (Theme #3.1 — World Modeling)
|
| 452 |
+
|
| 453 |
+
`CERNenv` plays as a *miniature world model* for an LHC analysis. The agent
|
| 454 |
+
must plan over a long horizon, manage scarce resources, choose the right tool
|
| 455 |
+
for each sub-task, and — most critically — submit a structured, calibrated
|
| 456 |
+
discovery claim. That makes it a good fit for **RL training of LLMs on
|
| 457 |
+
professional scientific tasks**: every transition is structured, every reward
|
| 458 |
+
is decomposable, and the terminal reward is grounded in physical truth rather
|
| 459 |
+
than human preference labels.
|
| 460 |
+
|
| 461 |
+
### Companion artefacts
|
| 462 |
+
|
| 463 |
+
* Trainer Space (Unsloth + LoRA + GRPO on A100) —
|
| 464 |
+
[`anugrah55/cernenv-trainer`](https://huggingface.co/spaces/anugrah55/cernenv-trainer)
|
| 465 |
+
* Trained adapter weights —
|
| 466 |
+
[`anugrah55/cernenv-grpo-qwen2.5-3b`](https://huggingface.co/anugrah55/cernenv-grpo-qwen2.5-3b)
|
| 467 |
+
"""
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def build_gradio_demo() -> gr.Blocks:
|
| 471 |
+
"""Construct the CERNenv interactive demo as a Gradio Blocks app."""
|
| 472 |
+
|
| 473 |
+
# Lazy import so module-level import remains side-effect-light.
|
| 474 |
+
from models import ActionType
|
| 475 |
+
from server.simulator.latent_state import LatentParticle # noqa: F401 (side-effect import, keeps tests honest)
|
| 476 |
+
|
| 477 |
+
action_options = [a.value for a in ActionType]
|
| 478 |
+
channels = ["diphoton", "dilepton_ee", "dilepton_mumu", "dijet", "four_lepton", "bb"]
|
| 479 |
+
triggers = ["low_pt", "high_pt", "diphoton_hlt", "dilepton_hlt", "jet_hlt"]
|
| 480 |
+
beam_energies = ["7TeV", "8TeV", "13TeV", "14TeV"]
|
| 481 |
+
|
| 482 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="CERNenv interactive demo") as demo:
|
| 483 |
+
gr.Markdown(_HEADER_MD)
|
| 484 |
+
|
| 485 |
+
with gr.Tabs():
|
| 486 |
+
|
| 487 |
+
# ───────── Tab 1: Watch a baseline ─────────
|
| 488 |
+
with gr.TabItem("▶ Watch a baseline"):
|
| 489 |
+
gr.Markdown(
|
| 490 |
+
"Pick a scenario and seed, then click one of **Random / Heuristic / "
|
| 491 |
+
"Oracle**. The agent will play a full episode and stream every "
|
| 492 |
+
"action+reward into the log. The hidden particle truth is revealed "
|
| 493 |
+
"at the end."
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
with gr.Row():
|
| 497 |
+
scenario_dd = gr.Dropdown(
|
| 498 |
+
choices=[lab for lab, _ in SCENARIO_CHOICES],
|
| 499 |
+
value=SCENARIO_CHOICES[0][0],
|
| 500 |
+
label="Scenario",
|
| 501 |
+
interactive=True,
|
| 502 |
+
)
|
| 503 |
+
seed_in = gr.Number(value=7, precision=0, label="Seed")
|
| 504 |
+
|
| 505 |
+
with gr.Row():
|
| 506 |
+
btn_random = gr.Button("▶ Run Random agent", variant="secondary")
|
| 507 |
+
btn_heuristic = gr.Button("▶ Run Heuristic agent", variant="primary")
|
| 508 |
+
btn_oracle = gr.Button("▶ Run Oracle agent", variant="secondary")
|
| 509 |
+
|
| 510 |
+
with gr.Row():
|
| 511 |
+
with gr.Column(scale=3):
|
| 512 |
+
log_md = gr.Markdown(
|
| 513 |
+
"*(no rollout yet — pick an agent above)*",
|
| 514 |
+
label="Episode log",
|
| 515 |
+
)
|
| 516 |
+
with gr.Column(scale=2):
|
| 517 |
+
cum_reward_b = gr.Textbox(value="0.0", label="Cumulative reward", interactive=False)
|
| 518 |
+
step_b = gr.Textbox(value="0", label="Step", interactive=False)
|
| 519 |
+
sig_b = gr.Textbox(value="0.0", label="Best significance σ", interactive=False)
|
| 520 |
+
cands_b = gr.Dataframe(
|
| 521 |
+
headers=["#", "mass (GeV)", "σ"],
|
| 522 |
+
value=[[0, "—", "—"]],
|
| 523 |
+
label="Candidate peaks",
|
| 524 |
+
interactive=False,
|
| 525 |
+
)
|
| 526 |
+
res_b = gr.Markdown("*(no rollout yet)*", label="Resources")
|
| 527 |
+
viol_b = gr.Markdown("", label="Rule violations")
|
| 528 |
+
truth_b = gr.Markdown(
|
| 529 |
+
"*(truth revealed when the episode ends)*",
|
| 530 |
+
label="🎯 Hidden truth (revealed at end of episode)",
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
def _run(scenario_label, seed, agent_name):
|
| 534 |
+
yield from _stream_baseline(
|
| 535 |
+
scenario_label,
|
| 536 |
+
int(seed),
|
| 537 |
+
agent_name,
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
outputs_b = [log_md, cum_reward_b, step_b, sig_b, cands_b, res_b, viol_b, truth_b]
|
| 541 |
+
btn_random.click(
|
| 542 |
+
lambda s, sd: _run(s, sd, "random"),
|
| 543 |
+
inputs=[scenario_dd, seed_in],
|
| 544 |
+
outputs=outputs_b,
|
| 545 |
+
)
|
| 546 |
+
btn_heuristic.click(
|
| 547 |
+
lambda s, sd: _run(s, sd, "heuristic"),
|
| 548 |
+
inputs=[scenario_dd, seed_in],
|
| 549 |
+
outputs=outputs_b,
|
| 550 |
+
)
|
| 551 |
+
btn_oracle.click(
|
| 552 |
+
lambda s, sd: _run(s, sd, "oracle"),
|
| 553 |
+
inputs=[scenario_dd, seed_in],
|
| 554 |
+
outputs=outputs_b,
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
# ───────── Tab 2: Build your own actions ─────────
|
| 558 |
+
with gr.TabItem("🛠 Build your own actions"):
|
| 559 |
+
gr.Markdown(
|
| 560 |
+
"Run the env one action at a time. Each click of **Submit step** "
|
| 561 |
+
"calls `env.step(action)` on a session-scoped environment. The "
|
| 562 |
+
"**Live evidence** panel updates after every step."
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
with gr.Row():
|
| 566 |
+
scenario_dd2 = gr.Dropdown(
|
| 567 |
+
choices=[lab for lab, _ in SCENARIO_CHOICES],
|
| 568 |
+
value=SCENARIO_CHOICES[0][0],
|
| 569 |
+
label="Scenario",
|
| 570 |
+
interactive=True,
|
| 571 |
+
)
|
| 572 |
+
seed_in2 = gr.Number(value=42, precision=0, label="Seed")
|
| 573 |
+
btn_new = gr.Button("🔄 New episode", variant="primary")
|
| 574 |
+
|
| 575 |
+
env_state = gr.State(value=None)
|
| 576 |
+
obs_state = gr.State(value=None)
|
| 577 |
+
|
| 578 |
+
status_md = gr.Markdown(
|
| 579 |
+
"*Click **🔄 New episode** to begin.*", label="Status"
|
| 580 |
+
)
|
| 581 |
+
|
| 582 |
+
with gr.Row():
|
| 583 |
+
with gr.Column(scale=3):
|
| 584 |
+
gr.Markdown("### Action Builder")
|
| 585 |
+
action_type = gr.Dropdown(
|
| 586 |
+
choices=action_options,
|
| 587 |
+
value="configure_beam",
|
| 588 |
+
label="action_type",
|
| 589 |
+
)
|
| 590 |
+
with gr.Row():
|
| 591 |
+
method = gr.Textbox(
|
| 592 |
+
value="",
|
| 593 |
+
label="method (optional, e.g. ROOT_RooFit)",
|
| 594 |
+
placeholder="ROOT_RooFit / BumpHunter / Athena / …",
|
| 595 |
+
)
|
| 596 |
+
confidence = gr.Slider(
|
| 597 |
+
minimum=0.0, maximum=1.0, step=0.05, value=0.7,
|
| 598 |
+
label="confidence",
|
| 599 |
+
)
|
| 600 |
+
with gr.Row():
|
| 601 |
+
channel = gr.Dropdown(
|
| 602 |
+
choices=channels, value="diphoton", label="channel",
|
| 603 |
+
)
|
| 604 |
+
trigger = gr.Dropdown(
|
| 605 |
+
choices=triggers, value="diphoton_hlt", label="trigger",
|
| 606 |
+
)
|
| 607 |
+
beam_energy = gr.Dropdown(
|
| 608 |
+
choices=beam_energies, value="13TeV", label="beam_energy",
|
| 609 |
+
)
|
| 610 |
+
with gr.Row():
|
| 611 |
+
luminosity_fb = gr.Number(
|
| 612 |
+
value=80.0, label="luminosity_fb (allocate / collect)",
|
| 613 |
+
)
|
| 614 |
+
mass_window_lo = gr.Number(
|
| 615 |
+
value=80.0, label="mass_window_lo (GeV)",
|
| 616 |
+
)
|
| 617 |
+
mass_window_hi = gr.Number(
|
| 618 |
+
value=300.0, label="mass_window_hi (GeV)",
|
| 619 |
+
)
|
| 620 |
+
gr.Markdown("**Discovery claim parameters** *(only used for `submit_discovery_claim`)*")
|
| 621 |
+
with gr.Row():
|
| 622 |
+
claim_mass = gr.Number(value=125.0, label="claim mass (GeV)")
|
| 623 |
+
claim_sigma = gr.Number(value=5.0, label="claim significance σ")
|
| 624 |
+
claim_spin = gr.Dropdown(choices=[0, 1, 2], value=0, label="claim spin")
|
| 625 |
+
claim_parity = gr.Dropdown(choices=["+", "-"], value="+", label="claim parity")
|
| 626 |
+
|
| 627 |
+
btn_submit = gr.Button("✅ Submit step", variant="primary")
|
| 628 |
+
|
| 629 |
+
with gr.Column(scale=3):
|
| 630 |
+
gr.Markdown("### Live evidence")
|
| 631 |
+
cum_reward = gr.Textbox(value="0.0", label="Cumulative reward", interactive=False)
|
| 632 |
+
step_idx = gr.Textbox(value="0", label="Step index", interactive=False)
|
| 633 |
+
breakdown = gr.Dataframe(
|
| 634 |
+
headers=["component", "value"],
|
| 635 |
+
value=[["—", 0.0]],
|
| 636 |
+
label="Step reward breakdown",
|
| 637 |
+
interactive=False,
|
| 638 |
+
)
|
| 639 |
+
candidates = gr.Dataframe(
|
| 640 |
+
headers=["#", "mass (GeV)", "σ"],
|
| 641 |
+
value=[[0, "—", "—"]],
|
| 642 |
+
label="Candidate peaks",
|
| 643 |
+
interactive=False,
|
| 644 |
+
)
|
| 645 |
+
resources = gr.Markdown("*(no episode yet)*", label="Resources")
|
| 646 |
+
violations = gr.Markdown("", label="Rule violations")
|
| 647 |
+
truth = gr.Markdown(
|
| 648 |
+
"*(truth revealed when the episode ends)*",
|
| 649 |
+
label="🎯 Hidden truth",
|
| 650 |
+
)
|
| 651 |
+
|
| 652 |
+
btn_new.click(
|
| 653 |
+
_new_episode,
|
| 654 |
+
inputs=[scenario_dd2, seed_in2],
|
| 655 |
+
outputs=[
|
| 656 |
+
env_state, obs_state, status_md, cum_reward, step_idx,
|
| 657 |
+
breakdown, candidates, resources, violations, truth,
|
| 658 |
+
],
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
btn_submit.click(
|
| 662 |
+
_submit_step,
|
| 663 |
+
inputs=[
|
| 664 |
+
env_state, obs_state, action_type, method,
|
| 665 |
+
channel, trigger, beam_energy, luminosity_fb,
|
| 666 |
+
mass_window_lo, mass_window_hi,
|
| 667 |
+
claim_mass, claim_sigma, claim_spin, claim_parity,
|
| 668 |
+
confidence,
|
| 669 |
+
],
|
| 670 |
+
outputs=[
|
| 671 |
+
env_state, obs_state, status_md, cum_reward, step_idx,
|
| 672 |
+
breakdown, candidates, resources, violations, truth,
|
| 673 |
+
],
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
# ───────── Tab 3: About ─────────
|
| 677 |
+
with gr.TabItem("ℹ About"):
|
| 678 |
+
gr.Markdown(_ABOUT_MD)
|
| 679 |
+
|
| 680 |
+
gr.Markdown(
|
| 681 |
+
"---\n"
|
| 682 |
+
"*Built for the Meta OpenEnv Hackathon (Theme #3.1, World Modeling). "
|
| 683 |
+
"The OpenEnv HTTP API is still live alongside this UI at "
|
| 684 |
+
"`/health`, `/reset`, `/step`, `/state`, `/schema`, `/mcp`, `/docs`.*"
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
return demo
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
__all__ = ["build_gradio_demo"]
|
space/env/requirements.txt
CHANGED
|
@@ -4,3 +4,4 @@ pydantic>=2.0.0
|
|
| 4 |
fastapi>=0.110.0
|
| 5 |
uvicorn>=0.27.0
|
| 6 |
openenv-core[core]>=0.2.3
|
|
|
|
|
|
| 4 |
fastapi>=0.110.0
|
| 5 |
uvicorn>=0.27.0
|
| 6 |
openenv-core[core]>=0.2.3
|
| 7 |
+
gradio>=4.40.0,<5.0
|
training/evaluate.py
CHANGED
|
@@ -1,152 +1,153 @@
|
|
| 1 |
-
"""Evaluate an LLM (with optional LoRA adapters) on CERNenv.
|
| 2 |
-
|
| 3 |
-
Usage:
|
| 4 |
-
python -m training.evaluate --model_name unsloth/Qwen2.5-3B-Instruct \\
|
| 5 |
-
--difficulty easy --episodes 16 --tag pre_train \\
|
| 6 |
-
--out training/runs/eval_pre_train.jsonl
|
| 7 |
-
|
| 8 |
-
python -m training.evaluate --model_name unsloth/Qwen2.5-3B-Instruct \\
|
| 9 |
-
--adapter_dir training/runs/unsloth-grpo --difficulty easy \\
|
| 10 |
-
--episodes 16 --tag post_train --out training/runs/eval_post_train.jsonl
|
| 11 |
-
"""
|
| 12 |
-
|
| 13 |
-
from __future__ import annotations
|
| 14 |
-
|
| 15 |
-
import argparse
|
| 16 |
-
import json
|
| 17 |
-
import logging
|
| 18 |
-
import os
|
| 19 |
-
from dataclasses import asdict
|
| 20 |
-
from pathlib import Path
|
| 21 |
-
from typing import Any, Dict, List, Optional
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 25 |
-
logger = logging.getLogger(__name__)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _build_generate_fn(
|
| 29 |
-
*,
|
| 30 |
-
model_name: str,
|
| 31 |
-
adapter_dir: Optional[str],
|
| 32 |
-
use_unsloth: bool,
|
| 33 |
-
max_seq_length: int,
|
| 34 |
-
):
|
| 35 |
-
if use_unsloth:
|
| 36 |
-
from unsloth import FastLanguageModel # type: ignore
|
| 37 |
-
|
| 38 |
-
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 39 |
-
model_name=model_name,
|
| 40 |
-
max_seq_length=max_seq_length,
|
| 41 |
-
load_in_4bit=True,
|
| 42 |
-
fast_inference
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
parser
|
| 88 |
-
parser.add_argument("--
|
| 89 |
-
parser.add_argument("--
|
| 90 |
-
parser.add_argument("--
|
| 91 |
-
parser.add_argument("--
|
| 92 |
-
parser.add_argument("--
|
| 93 |
-
parser.add_argument("--
|
| 94 |
-
parser.add_argument("--
|
| 95 |
-
parser.add_argument("--
|
| 96 |
-
parser.add_argument("--
|
| 97 |
-
parser.add_argument("--
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
from
|
| 102 |
-
from training.
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
| 1 |
+
"""Evaluate an LLM (with optional LoRA adapters) on CERNenv.
|
| 2 |
+
|
| 3 |
+
Usage:
|
| 4 |
+
python -m training.evaluate --model_name unsloth/Qwen2.5-3B-Instruct \\
|
| 5 |
+
--difficulty easy --episodes 16 --tag pre_train \\
|
| 6 |
+
--out training/runs/eval_pre_train.jsonl
|
| 7 |
+
|
| 8 |
+
python -m training.evaluate --model_name unsloth/Qwen2.5-3B-Instruct \\
|
| 9 |
+
--adapter_dir training/runs/unsloth-grpo --difficulty easy \\
|
| 10 |
+
--episodes 16 --tag post_train --out training/runs/eval_post_train.jsonl
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import json
|
| 17 |
+
import logging
|
| 18 |
+
import os
|
| 19 |
+
from dataclasses import asdict
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any, Dict, List, Optional
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _build_generate_fn(
|
| 29 |
+
*,
|
| 30 |
+
model_name: str,
|
| 31 |
+
adapter_dir: Optional[str],
|
| 32 |
+
use_unsloth: bool,
|
| 33 |
+
max_seq_length: int,
|
| 34 |
+
):
|
| 35 |
+
if use_unsloth:
|
| 36 |
+
from unsloth import FastLanguageModel # type: ignore
|
| 37 |
+
|
| 38 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 39 |
+
model_name=model_name,
|
| 40 |
+
max_seq_length=max_seq_length,
|
| 41 |
+
load_in_4bit=True,
|
| 42 |
+
# fast_inference requires vLLM, which is not in requirements; plain transformers generation is used instead. Re-enable after pinning vllm in space/training/requirements.txt.
|
| 43 |
+
fast_inference=False,
|
| 44 |
+
)
|
| 45 |
+
if adapter_dir:
|
| 46 |
+
model.load_adapter(adapter_dir)
|
| 47 |
+
FastLanguageModel.for_inference(model)
|
| 48 |
+
else:
|
| 49 |
+
import torch
|
| 50 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 51 |
+
|
| 52 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 53 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 54 |
+
model_name,
|
| 55 |
+
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
|
| 56 |
+
device_map="auto" if torch.cuda.is_available() else None,
|
| 57 |
+
)
|
| 58 |
+
if adapter_dir:
|
| 59 |
+
from peft import PeftModel # type: ignore
|
| 60 |
+
model = PeftModel.from_pretrained(model, adapter_dir)
|
| 61 |
+
|
| 62 |
+
if tokenizer.pad_token is None:
|
| 63 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 64 |
+
|
| 65 |
+
def prompt_fn(chat: List[Dict[str, str]]) -> str:
|
| 66 |
+
return tokenizer.apply_chat_template(
|
| 67 |
+
chat, add_generation_prompt=True, tokenize=False
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def generate_fn(prompt: str, config) -> str:
|
| 71 |
+
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 72 |
+
outputs = model.generate(
|
| 73 |
+
**inputs,
|
| 74 |
+
max_new_tokens=config.max_new_tokens,
|
| 75 |
+
do_sample=True,
|
| 76 |
+
temperature=config.temperature,
|
| 77 |
+
top_p=config.top_p,
|
| 78 |
+
pad_token_id=tokenizer.pad_token_id,
|
| 79 |
+
)
|
| 80 |
+
gen = outputs[0][inputs["input_ids"].shape[1]:]
|
| 81 |
+
return tokenizer.decode(gen, skip_special_tokens=True)
|
| 82 |
+
|
| 83 |
+
return prompt_fn, generate_fn
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def main() -> None: # pragma: no cover
|
| 87 |
+
parser = argparse.ArgumentParser()
|
| 88 |
+
parser.add_argument("--model_name", required=True)
|
| 89 |
+
parser.add_argument("--adapter_dir", default=None)
|
| 90 |
+
parser.add_argument("--scenario", default=None)
|
| 91 |
+
parser.add_argument("--difficulty", choices=["easy", "medium", "hard"], default="easy")
|
| 92 |
+
parser.add_argument("--episodes", type=int, default=16)
|
| 93 |
+
parser.add_argument("--seed", type=int, default=1000)
|
| 94 |
+
parser.add_argument("--max_steps", type=int, default=18)
|
| 95 |
+
parser.add_argument("--max_seq_length", type=int, default=2048)
|
| 96 |
+
parser.add_argument("--no_unsloth", action="store_true")
|
| 97 |
+
parser.add_argument("--tag", default="eval")
|
| 98 |
+
parser.add_argument("--out", required=True)
|
| 99 |
+
args = parser.parse_args()
|
| 100 |
+
|
| 101 |
+
from server.environment import CERNCollisionEnvironment
|
| 102 |
+
from training.llm_agent import LLMAgentConfig
|
| 103 |
+
from training.rollouts import collect_episode, save_episodes_jsonl
|
| 104 |
+
|
| 105 |
+
use_unsloth = not args.no_unsloth
|
| 106 |
+
try:
|
| 107 |
+
prompt_fn, generate_fn = _build_generate_fn(
|
| 108 |
+
model_name=args.model_name,
|
| 109 |
+
adapter_dir=args.adapter_dir,
|
| 110 |
+
use_unsloth=use_unsloth,
|
| 111 |
+
max_seq_length=args.max_seq_length,
|
| 112 |
+
)
|
| 113 |
+
except ImportError as exc:
|
| 114 |
+
logger.warning("Unsloth not available (%s); falling back to transformers.", exc)
|
| 115 |
+
prompt_fn, generate_fn = _build_generate_fn(
|
| 116 |
+
model_name=args.model_name,
|
| 117 |
+
adapter_dir=args.adapter_dir,
|
| 118 |
+
use_unsloth=False,
|
| 119 |
+
max_seq_length=args.max_seq_length,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
env = CERNCollisionEnvironment(max_steps=args.max_steps)
|
| 123 |
+
cfg = LLMAgentConfig()
|
| 124 |
+
|
| 125 |
+
episodes = []
|
| 126 |
+
for ep in range(args.episodes):
|
| 127 |
+
seed = args.seed + ep
|
| 128 |
+
rec = collect_episode(
|
| 129 |
+
env=env,
|
| 130 |
+
seed=seed,
|
| 131 |
+
scenario=args.scenario,
|
| 132 |
+
difficulty=args.difficulty,
|
| 133 |
+
prompt_fn=prompt_fn,
|
| 134 |
+
generate_fn=generate_fn,
|
| 135 |
+
config=cfg,
|
| 136 |
+
)
|
| 137 |
+
episodes.append(rec)
|
| 138 |
+
logger.info(
|
| 139 |
+
"[%s][%d/%d] reward=%+.3f discovered=%s mass=%s channel=%s",
|
| 140 |
+
args.tag, ep + 1, args.episodes,
|
| 141 |
+
rec.cumulative_reward, rec.discovered, rec.correct_mass, rec.correct_channel,
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
| 145 |
+
save_episodes_jsonl(episodes, args.out)
|
| 146 |
+
|
| 147 |
+
rewards = [e.cumulative_reward for e in episodes]
|
| 148 |
+
success = sum(1 for e in episodes if e.discovered) / len(episodes)
|
| 149 |
+
logger.info("[%s] mean_reward=%.3f success_rate=%.2f", args.tag, sum(rewards) / len(rewards), success)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__": # pragma: no cover
|
| 153 |
+
main()
|
training/training_unsloth.py
CHANGED
|
@@ -1,341 +1,342 @@
|
|
| 1 |
-
"""Unsloth + LoRA (Low-Rank Adaptation) GRPO training for CERNenv.
|
| 2 |
-
|
| 3 |
-
This is the recommended path for Colab / single- or multi-GPU runs because
|
| 4 |
-
Unsloth's fused kernels and 4-bit loading let us train 2B–8B models with
|
| 5 |
-
limited VRAM, while TRL's GRPO (Group-Relative Policy Optimization) loop
|
| 6 |
-
handles the policy-gradient math.
|
| 7 |
-
|
| 8 |
-
The trainer is wired up to produce **all** "training-progress evidence"
|
| 9 |
-
artifacts demanded by the OpenEnv hackathon's scoring rubric:
|
| 10 |
-
|
| 11 |
-
* per-step training log + reward/loss curve PNG (Portable Network Graphics)
|
| 12 |
-
* mid-training checkpoint evaluations + progression curve PNG
|
| 13 |
-
* (post-run) before/after summary + reward-distribution PNG
|
| 14 |
-
|
| 15 |
-
All artifacts land in ``--evidence_dir`` (default: ``evidence/``).
|
| 16 |
-
|
| 17 |
-
Run on Colab / single GPU:
|
| 18 |
-
!python -m training.training_unsloth \
|
| 19 |
-
--model_name unsloth/Qwen2.5-3B-Instruct \
|
| 20 |
-
--total_episodes 400 --num_generations 4 --output_dir runs/unsloth-grpo
|
| 21 |
-
|
| 22 |
-
Run on a 4×A100 Hugging Face Space (multi-GPU via accelerate):
|
| 23 |
-
accelerate launch --num_processes 4 -m training.training_unsloth \
|
| 24 |
-
--total_episodes 1500 --num_generations 8 --output_dir runs/unsloth-grpo
|
| 25 |
-
"""
|
| 26 |
-
|
| 27 |
-
from __future__ import annotations
|
| 28 |
-
|
| 29 |
-
import argparse
|
| 30 |
-
import logging
|
| 31 |
-
import time
|
| 32 |
-
from pathlib import Path
|
| 33 |
-
from typing import Any, Dict, List, Optional
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 37 |
-
logger = logging.getLogger(__name__)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def _build_args() -> argparse.Namespace:
|
| 41 |
-
parser = argparse.ArgumentParser()
|
| 42 |
-
parser.add_argument("--model_name", default="unsloth/Qwen2.5-3B-Instruct")
|
| 43 |
-
parser.add_argument("--scenario", default=None)
|
| 44 |
-
parser.add_argument("--difficulty", choices=["easy", "medium", "hard"], default="easy")
|
| 45 |
-
parser.add_argument(
|
| 46 |
-
"--curriculum",
|
| 47 |
-
action="store_true",
|
| 48 |
-
help=(
|
| 49 |
-
"Enable adaptive curriculum: start at --difficulty and promote "
|
| 50 |
-
"to medium/hard once held-out success rate clears the threshold "
|
| 51 |
-
"(see training/curriculum.py)."
|
| 52 |
-
),
|
| 53 |
-
)
|
| 54 |
-
parser.add_argument("--curriculum_promote", type=float, default=0.55)
|
| 55 |
-
parser.add_argument("--curriculum_demote", type=float, default=0.10)
|
| 56 |
-
parser.add_argument("--total_episodes", type=int, default=400)
|
| 57 |
-
parser.add_argument("--seed", type=int, default=42)
|
| 58 |
-
parser.add_argument("--max_steps", type=int, default=18)
|
| 59 |
-
parser.add_argument("--num_generations", type=int, default=4)
|
| 60 |
-
parser.add_argument("--max_prompt_length", type=int, default=2048)
|
| 61 |
-
parser.add_argument("--max_completion_length", type=int, default=384)
|
| 62 |
-
parser.add_argument("--learning_rate", type=float, default=5e-6)
|
| 63 |
-
parser.add_argument("--load_in_4bit", action="store_true", default=True)
|
| 64 |
-
parser.add_argument("--lora_rank", type=int, default=16)
|
| 65 |
-
parser.add_argument("--lora_alpha", type=int, default=16)
|
| 66 |
-
parser.add_argument("--per_device_batch_size", type=int, default=1)
|
| 67 |
-
parser.add_argument("--gradient_accumulation_steps", type=int, default=4)
|
| 68 |
-
parser.add_argument("--logging_steps", type=int, default=2)
|
| 69 |
-
parser.add_argument("--save_steps", type=int, default=50)
|
| 70 |
-
parser.add_argument("--checkpoint_eval_steps", type=int, default=25,
|
| 71 |
-
help="Run a held-out eval every N updates for the progression curve.")
|
| 72 |
-
parser.add_argument("--checkpoint_eval_episodes", type=int, default=8,
|
| 73 |
-
help="Number of held-out episodes per mid-training eval.")
|
| 74 |
-
parser.add_argument("--output_dir", default="runs/unsloth-grpo")
|
| 75 |
-
parser.add_argument("--evidence_dir", default="evidence")
|
| 76 |
-
return parser.parse_args()
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def main() -> None: # pragma: no cover - heavy GPU path
|
| 80 |
-
args = _build_args()
|
| 81 |
-
|
| 82 |
-
# IMPORTANT: Unsloth MUST be imported before transformers / trl. It
|
| 83 |
-
# patches transformers' lazy ``_import_structure`` to register a few
|
| 84 |
-
# symbols (notably ``PreTrainedModel`` under torch-aware paths). If trl
|
| 85 |
-
# loads transformers first, the lazy loader will fail with a confusing
|
| 86 |
-
# ``ImportError: cannot import name 'PreTrainedModel' from 'transformers'``
|
| 87 |
-
# at GRPOTrainer import time — which is exactly what we hit on the
|
| 88 |
-
# trainer Space before this reorder.
|
| 89 |
-
# See: https://github.com/unslothai/unsloth and the matching
|
| 90 |
-
# transformers issue #42548 for the lazy-import root cause.
|
| 91 |
-
from unsloth import FastLanguageModel
|
| 92 |
-
from transformers import TrainerCallback
|
| 93 |
-
from trl import GRPOConfig, GRPOTrainer
|
| 94 |
-
|
| 95 |
-
from server.environment import CERNCollisionEnvironment
|
| 96 |
-
from training.curriculum import CurriculumConfig, CurriculumManager
|
| 97 |
-
from training.evidence import (
|
| 98 |
-
CheckpointEvalWriter,
|
| 99 |
-
EvidencePaths,
|
| 100 |
-
RewardComponentLogWriter,
|
| 101 |
-
TrainingLogWriter,
|
| 102 |
-
render_checkpoint_progression,
|
| 103 |
-
render_reward_components,
|
| 104 |
-
render_training_curve,
|
| 105 |
-
)
|
| 106 |
-
from training.llm_agent import LLMAgentConfig
|
| 107 |
-
from training.rollouts import collect_episode
|
| 108 |
-
from training.training_script import (
|
| 109 |
-
EpisodeContext,
|
| 110 |
-
RewardComponentAccumulator,
|
| 111 |
-
)
|
| 112 |
-
|
| 113 |
-
paths = EvidencePaths(root=Path(args.evidence_dir))
|
| 114 |
-
paths.ensure()
|
| 115 |
-
log_writer = TrainingLogWriter(paths.training_log_csv)
|
| 116 |
-
ckpt_writer = CheckpointEvalWriter(paths.checkpoint_evals_csv)
|
| 117 |
-
component_writer = RewardComponentLogWriter(paths.reward_components_csv)
|
| 118 |
-
component_accumulator = RewardComponentAccumulator()
|
| 119 |
-
|
| 120 |
-
curriculum: Optional[CurriculumManager] = None
|
| 121 |
-
if args.curriculum:
|
| 122 |
-
curriculum = CurriculumManager(
|
| 123 |
-
CurriculumConfig(
|
| 124 |
-
start_difficulty=args.difficulty,
|
| 125 |
-
promote_threshold=args.curriculum_promote,
|
| 126 |
-
demote_threshold=args.curriculum_demote,
|
| 127 |
-
)
|
| 128 |
-
)
|
| 129 |
-
logger.info("Curriculum enabled: start=%s promote≥%.2f demote≤%.2f",
|
| 130 |
-
args.difficulty, args.curriculum_promote, args.curriculum_demote)
|
| 131 |
-
|
| 132 |
-
logger.info("Loading Unsloth model: %s", args.model_name)
|
| 133 |
-
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 134 |
-
model_name=args.model_name,
|
| 135 |
-
max_seq_length=args.max_prompt_length + args.max_completion_length,
|
| 136 |
-
load_in_4bit=args.load_in_4bit,
|
| 137 |
-
fast_inference
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
"
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
self.
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
"
|
| 199 |
-
"
|
| 200 |
-
"
|
| 201 |
-
"
|
| 202 |
-
"
|
| 203 |
-
"
|
| 204 |
-
"
|
| 205 |
-
"
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
#
|
| 213 |
-
#
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
summary
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
#
|
| 241 |
-
#
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
paths.
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
step
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
#
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
summary
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
logger.info("
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
| 1 |
+
"""Unsloth + LoRA (Low-Rank Adaptation) GRPO training for CERNenv.
|
| 2 |
+
|
| 3 |
+
This is the recommended path for Colab / single- or multi-GPU runs because
|
| 4 |
+
Unsloth's fused kernels and 4-bit loading let us train 2B–8B models with
|
| 5 |
+
limited VRAM, while TRL's GRPO (Group-Relative Policy Optimization) loop
|
| 6 |
+
handles the policy-gradient math.
|
| 7 |
+
|
| 8 |
+
The trainer is wired up to produce **all** "training-progress evidence"
|
| 9 |
+
artifacts demanded by the OpenEnv hackathon's scoring rubric:
|
| 10 |
+
|
| 11 |
+
* per-step training log + reward/loss curve PNG (Portable Network Graphics)
|
| 12 |
+
* mid-training checkpoint evaluations + progression curve PNG
|
| 13 |
+
* (post-run) before/after summary + reward-distribution PNG
|
| 14 |
+
|
| 15 |
+
All artifacts land in ``--evidence_dir`` (default: ``evidence/``).
|
| 16 |
+
|
| 17 |
+
Run on Colab / single GPU:
|
| 18 |
+
!python -m training.training_unsloth \
|
| 19 |
+
--model_name unsloth/Qwen2.5-3B-Instruct \
|
| 20 |
+
--total_episodes 400 --num_generations 4 --output_dir runs/unsloth-grpo
|
| 21 |
+
|
| 22 |
+
Run on a 4×A100 Hugging Face Space (multi-GPU via accelerate):
|
| 23 |
+
accelerate launch --num_processes 4 -m training.training_unsloth \
|
| 24 |
+
--total_episodes 1500 --num_generations 8 --output_dir runs/unsloth-grpo
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import argparse
|
| 30 |
+
import logging
|
| 31 |
+
import time
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
from typing import Any, Dict, List, Optional
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 37 |
+
logger = logging.getLogger(__name__)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _build_args() -> argparse.Namespace:
|
| 41 |
+
parser = argparse.ArgumentParser()
|
| 42 |
+
parser.add_argument("--model_name", default="unsloth/Qwen2.5-3B-Instruct")
|
| 43 |
+
parser.add_argument("--scenario", default=None)
|
| 44 |
+
parser.add_argument("--difficulty", choices=["easy", "medium", "hard"], default="easy")
|
| 45 |
+
parser.add_argument(
|
| 46 |
+
"--curriculum",
|
| 47 |
+
action="store_true",
|
| 48 |
+
help=(
|
| 49 |
+
"Enable adaptive curriculum: start at --difficulty and promote "
|
| 50 |
+
"to medium/hard once held-out success rate clears the threshold "
|
| 51 |
+
"(see training/curriculum.py)."
|
| 52 |
+
),
|
| 53 |
+
)
|
| 54 |
+
parser.add_argument("--curriculum_promote", type=float, default=0.55)
|
| 55 |
+
parser.add_argument("--curriculum_demote", type=float, default=0.10)
|
| 56 |
+
parser.add_argument("--total_episodes", type=int, default=400)
|
| 57 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 58 |
+
parser.add_argument("--max_steps", type=int, default=18)
|
| 59 |
+
parser.add_argument("--num_generations", type=int, default=4)
|
| 60 |
+
parser.add_argument("--max_prompt_length", type=int, default=2048)
|
| 61 |
+
parser.add_argument("--max_completion_length", type=int, default=384)
|
| 62 |
+
parser.add_argument("--learning_rate", type=float, default=5e-6)
|
| 63 |
+
parser.add_argument("--load_in_4bit", action="store_true", default=True)
|
| 64 |
+
parser.add_argument("--lora_rank", type=int, default=16)
|
| 65 |
+
parser.add_argument("--lora_alpha", type=int, default=16)
|
| 66 |
+
parser.add_argument("--per_device_batch_size", type=int, default=1)
|
| 67 |
+
parser.add_argument("--gradient_accumulation_steps", type=int, default=4)
|
| 68 |
+
parser.add_argument("--logging_steps", type=int, default=2)
|
| 69 |
+
parser.add_argument("--save_steps", type=int, default=50)
|
| 70 |
+
parser.add_argument("--checkpoint_eval_steps", type=int, default=25,
|
| 71 |
+
help="Run a held-out eval every N updates for the progression curve.")
|
| 72 |
+
parser.add_argument("--checkpoint_eval_episodes", type=int, default=8,
|
| 73 |
+
help="Number of held-out episodes per mid-training eval.")
|
| 74 |
+
parser.add_argument("--output_dir", default="runs/unsloth-grpo")
|
| 75 |
+
parser.add_argument("--evidence_dir", default="evidence")
|
| 76 |
+
return parser.parse_args()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def main() -> None: # pragma: no cover - heavy GPU path
|
| 80 |
+
args = _build_args()
|
| 81 |
+
|
| 82 |
+
# IMPORTANT: Unsloth MUST be imported before transformers / trl. It
|
| 83 |
+
# patches transformers' lazy ``_import_structure`` to register a few
|
| 84 |
+
# symbols (notably ``PreTrainedModel`` under torch-aware paths). If trl
|
| 85 |
+
# loads transformers first, the lazy loader will fail with a confusing
|
| 86 |
+
# ``ImportError: cannot import name 'PreTrainedModel' from 'transformers'``
|
| 87 |
+
# at GRPOTrainer import time — which is exactly what we hit on the
|
| 88 |
+
# trainer Space before this reorder.
|
| 89 |
+
# See: https://github.com/unslothai/unsloth and the matching
|
| 90 |
+
# transformers issue #42548 for the lazy-import root cause.
|
| 91 |
+
from unsloth import FastLanguageModel
|
| 92 |
+
from transformers import TrainerCallback
|
| 93 |
+
from trl import GRPOConfig, GRPOTrainer
|
| 94 |
+
|
| 95 |
+
from server.environment import CERNCollisionEnvironment
|
| 96 |
+
from training.curriculum import CurriculumConfig, CurriculumManager
|
| 97 |
+
from training.evidence import (
|
| 98 |
+
CheckpointEvalWriter,
|
| 99 |
+
EvidencePaths,
|
| 100 |
+
RewardComponentLogWriter,
|
| 101 |
+
TrainingLogWriter,
|
| 102 |
+
render_checkpoint_progression,
|
| 103 |
+
render_reward_components,
|
| 104 |
+
render_training_curve,
|
| 105 |
+
)
|
| 106 |
+
from training.llm_agent import LLMAgentConfig
|
| 107 |
+
from training.rollouts import collect_episode
|
| 108 |
+
from training.training_script import (
|
| 109 |
+
EpisodeContext,
|
| 110 |
+
RewardComponentAccumulator,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
paths = EvidencePaths(root=Path(args.evidence_dir))
|
| 114 |
+
paths.ensure()
|
| 115 |
+
log_writer = TrainingLogWriter(paths.training_log_csv)
|
| 116 |
+
ckpt_writer = CheckpointEvalWriter(paths.checkpoint_evals_csv)
|
| 117 |
+
component_writer = RewardComponentLogWriter(paths.reward_components_csv)
|
| 118 |
+
component_accumulator = RewardComponentAccumulator()
|
| 119 |
+
|
| 120 |
+
curriculum: Optional[CurriculumManager] = None
|
| 121 |
+
if args.curriculum:
|
| 122 |
+
curriculum = CurriculumManager(
|
| 123 |
+
CurriculumConfig(
|
| 124 |
+
start_difficulty=args.difficulty,
|
| 125 |
+
promote_threshold=args.curriculum_promote,
|
| 126 |
+
demote_threshold=args.curriculum_demote,
|
| 127 |
+
)
|
| 128 |
+
)
|
| 129 |
+
logger.info("Curriculum enabled: start=%s promote≥%.2f demote≤%.2f",
|
| 130 |
+
args.difficulty, args.curriculum_promote, args.curriculum_demote)
|
| 131 |
+
|
| 132 |
+
logger.info("Loading Unsloth model: %s", args.model_name)
|
| 133 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 134 |
+
model_name=args.model_name,
|
| 135 |
+
max_seq_length=args.max_prompt_length + args.max_completion_length,
|
| 136 |
+
load_in_4bit=args.load_in_4bit,
|
| 137 |
+
# fast_inference requires vLLM, which is not in requirements; plain transformers generation is used instead. Re-enable after pinning vllm in space/training/requirements.txt.
|
| 138 |
+
fast_inference=False,
|
| 139 |
+
)
|
| 140 |
+
model = FastLanguageModel.get_peft_model(
|
| 141 |
+
model,
|
| 142 |
+
r=args.lora_rank,
|
| 143 |
+
lora_alpha=args.lora_alpha,
|
| 144 |
+
target_modules=[
|
| 145 |
+
"q_proj", "k_proj", "v_proj", "o_proj",
|
| 146 |
+
"gate_proj", "up_proj", "down_proj",
|
| 147 |
+
],
|
| 148 |
+
use_gradient_checkpointing="unsloth",
|
| 149 |
+
)
|
| 150 |
+
if tokenizer.pad_token is None:
|
| 151 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 152 |
+
|
| 153 |
+
from training.training_script import build_dataset, make_reward_fn
|
| 154 |
+
|
| 155 |
+
env = CERNCollisionEnvironment(max_steps=args.max_steps)
|
| 156 |
+
dataset = build_dataset(
|
| 157 |
+
tokenizer=tokenizer,
|
| 158 |
+
n_prompts=args.total_episodes,
|
| 159 |
+
seed=args.seed,
|
| 160 |
+
scenario=args.scenario,
|
| 161 |
+
difficulty=args.difficulty,
|
| 162 |
+
curriculum=args.curriculum,
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
ctx = EpisodeContext(
|
| 166 |
+
env=env, seed=args.seed,
|
| 167 |
+
scenario=args.scenario, difficulty=args.difficulty,
|
| 168 |
+
)
|
| 169 |
+
reward_fn = make_reward_fn(ctx, accumulator=component_accumulator)
|
| 170 |
+
|
| 171 |
+
cfg = GRPOConfig(
|
| 172 |
+
output_dir=args.output_dir,
|
| 173 |
+
per_device_train_batch_size=args.per_device_batch_size,
|
| 174 |
+
gradient_accumulation_steps=args.gradient_accumulation_steps,
|
| 175 |
+
num_generations=args.num_generations,
|
| 176 |
+
learning_rate=args.learning_rate,
|
| 177 |
+
max_prompt_length=args.max_prompt_length,
|
| 178 |
+
max_completion_length=args.max_completion_length,
|
| 179 |
+
logging_steps=args.logging_steps,
|
| 180 |
+
save_steps=args.save_steps,
|
| 181 |
+
seed=args.seed,
|
| 182 |
+
bf16=True,
|
| 183 |
+
report_to=[],
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
held_out_seeds = list(range(900_000, 900_000 + args.checkpoint_eval_episodes))
|
| 187 |
+
|
| 188 |
+
class EvidenceCallback(TrainerCallback):
|
| 189 |
+
"""Stream training metrics + run periodic mid-training evals."""
|
| 190 |
+
|
| 191 |
+
def __init__(self) -> None:
|
| 192 |
+
self._t0 = time.time()
|
| 193 |
+
self._last_eval_step = -1
|
| 194 |
+
|
| 195 |
+
def on_log(self, _args, state, control, logs=None, **kw):
|
| 196 |
+
logs = logs or {}
|
| 197 |
+
row = {
|
| 198 |
+
"step": state.global_step,
|
| 199 |
+
"epoch": logs.get("epoch"),
|
| 200 |
+
"loss": logs.get("loss"),
|
| 201 |
+
"reward": logs.get("reward") or logs.get("rewards/mean"),
|
| 202 |
+
"reward_std": logs.get("reward_std") or logs.get("rewards/std"),
|
| 203 |
+
"kl": logs.get("kl"),
|
| 204 |
+
"grad_norm": logs.get("grad_norm"),
|
| 205 |
+
"learning_rate": logs.get("learning_rate"),
|
| 206 |
+
"wall_time_s": round(time.time() - self._t0, 2),
|
| 207 |
+
}
|
| 208 |
+
if any(v is not None for k, v in row.items() if k != "step"):
|
| 209 |
+
log_writer.append(row)
|
| 210 |
+
render_training_curve(paths.training_log_csv, paths.training_curve_png)
|
| 211 |
+
|
| 212 |
+
# Per-component reward summary (FAQ Q17, Q43, Q52: don't watch
|
| 213 |
+
# only the mean reward — track terminal vs shaping, success
|
| 214 |
+
# rates, and parse rate so verifier hacks become visible).
|
| 215 |
+
drained = component_accumulator.drain()
|
| 216 |
+
if drained:
|
| 217 |
+
summary = RewardComponentAccumulator.summarise(drained)
|
| 218 |
+
summary["step"] = state.global_step
|
| 219 |
+
component_writer.append(summary)
|
| 220 |
+
render_reward_components(
|
| 221 |
+
paths.reward_components_csv, paths.reward_components_png,
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
def on_step_end(self, _args, state, control, **kw):
|
| 225 |
+
step = state.global_step
|
| 226 |
+
if step <= 0 or step == self._last_eval_step:
|
| 227 |
+
return control
|
| 228 |
+
if step % args.checkpoint_eval_steps != 0:
|
| 229 |
+
return control
|
| 230 |
+
self._last_eval_step = step
|
| 231 |
+
try:
|
| 232 |
+
self._run_checkpoint_eval(step, state)
|
| 233 |
+
except Exception as exc:
|
| 234 |
+
logger.warning("checkpoint eval failed at step %d: %s", step, exc)
|
| 235 |
+
return control
|
| 236 |
+
|
| 237 |
+
def _run_checkpoint_eval(self, step: int, state) -> None:
|
| 238 |
+
FastLanguageModel.for_inference(model)
|
| 239 |
+
try:
|
| 240 |
+
# When curriculum is enabled, evaluate at whatever tier the
|
| 241 |
+
# adaptive manager currently considers appropriate. Otherwise
|
| 242 |
+
# use the static --difficulty.
|
| 243 |
+
eval_difficulty = (
|
| 244 |
+
curriculum.next_difficulty()
|
| 245 |
+
if curriculum is not None
|
| 246 |
+
else args.difficulty
|
| 247 |
+
)
|
| 248 |
+
episodes = []
|
| 249 |
+
for s in held_out_seeds:
|
| 250 |
+
ep = self._rollout_one(seed=s, difficulty=eval_difficulty)
|
| 251 |
+
if ep is not None:
|
| 252 |
+
episodes.append(ep)
|
| 253 |
+
if not episodes:
|
| 254 |
+
return
|
| 255 |
+
rewards = [e.cumulative_reward for e in episodes]
|
| 256 |
+
success_rate = sum(1 for e in episodes if e.discovered) / len(episodes)
|
| 257 |
+
ckpt_writer.append(
|
| 258 |
+
step=step,
|
| 259 |
+
fraction_done=round(step / max(state.max_steps or step, 1), 4),
|
| 260 |
+
episodes=len(episodes),
|
| 261 |
+
mean_reward=round(sum(rewards) / len(rewards), 4),
|
| 262 |
+
success_rate=round(success_rate, 4),
|
| 263 |
+
mass_acc=round(sum(1 for e in episodes if e.correct_mass) / len(episodes), 4),
|
| 264 |
+
channel_acc=round(sum(1 for e in episodes if e.correct_channel) / len(episodes), 4),
|
| 265 |
+
)
|
| 266 |
+
render_checkpoint_progression(
|
| 267 |
+
paths.checkpoint_evals_csv,
|
| 268 |
+
paths.checkpoint_progression_png,
|
| 269 |
+
)
|
| 270 |
+
if curriculum is not None:
|
| 271 |
+
snap = curriculum.record(
|
| 272 |
+
success=success_rate >= 0.5,
|
| 273 |
+
reward=sum(rewards) / len(rewards),
|
| 274 |
+
)
|
| 275 |
+
curriculum.save(paths.root / "curriculum_state.json")
|
| 276 |
+
if snap.get("event"):
|
| 277 |
+
logger.info(
|
| 278 |
+
"[curriculum] %s @ step=%d → tier=%s (rolling=%.2f)",
|
| 279 |
+
snap["event"], step, snap["current"], snap["rolling_success"],
|
| 280 |
+
)
|
| 281 |
+
logger.info(
|
| 282 |
+
"[checkpoint-eval step=%d difficulty=%s] reward=%.3f success=%.2f",
|
| 283 |
+
step, eval_difficulty,
|
| 284 |
+
rewards and (sum(rewards) / len(rewards)) or 0.0,
|
| 285 |
+
success_rate,
|
| 286 |
+
)
|
| 287 |
+
finally:
|
| 288 |
+
FastLanguageModel.for_training(model)
|
| 289 |
+
|
| 290 |
+
def _rollout_one(self, seed: int, difficulty: Optional[str] = None):
|
| 291 |
+
def prompt_fn(chat):
|
| 292 |
+
return tokenizer.apply_chat_template(chat, add_generation_prompt=True, tokenize=False)
|
| 293 |
+
|
| 294 |
+
def generate_fn(prompt: str, _config) -> str:
|
| 295 |
+
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 296 |
+
outputs = model.generate(
|
| 297 |
+
**inputs,
|
| 298 |
+
max_new_tokens=args.max_completion_length,
|
| 299 |
+
do_sample=True, temperature=0.7, top_p=0.95,
|
| 300 |
+
pad_token_id=tokenizer.pad_token_id,
|
| 301 |
+
)
|
| 302 |
+
gen = outputs[0][inputs["input_ids"].shape[1]:]
|
| 303 |
+
return tokenizer.decode(gen, skip_special_tokens=True)
|
| 304 |
+
|
| 305 |
+
return collect_episode(
|
| 306 |
+
env=env, seed=seed,
|
| 307 |
+
scenario=args.scenario,
|
| 308 |
+
difficulty=difficulty or args.difficulty,
|
| 309 |
+
prompt_fn=prompt_fn, generate_fn=generate_fn,
|
| 310 |
+
config=LLMAgentConfig(),
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
trainer = GRPOTrainer(
|
| 314 |
+
model=model,
|
| 315 |
+
processing_class=tokenizer,
|
| 316 |
+
train_dataset=dataset,
|
| 317 |
+
reward_funcs=[reward_fn],
|
| 318 |
+
args=cfg,
|
| 319 |
+
callbacks=[EvidenceCallback()],
|
| 320 |
+
)
|
| 321 |
+
logger.info("Starting Unsloth + LoRA GRPO training")
|
| 322 |
+
trainer.train()
|
| 323 |
+
|
| 324 |
+
# Drain whatever rollouts the final on_log didn't catch so the last
|
| 325 |
+
# row of reward_components.csv is correct.
|
| 326 |
+
final_drain = component_accumulator.drain()
|
| 327 |
+
if final_drain:
|
| 328 |
+
summary = RewardComponentAccumulator.summarise(final_drain)
|
| 329 |
+
summary["step"] = trainer.state.global_step
|
| 330 |
+
component_writer.append(summary)
|
| 331 |
+
render_reward_components(
|
| 332 |
+
paths.reward_components_csv, paths.reward_components_png,
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
trainer.save_model(args.output_dir)
|
| 336 |
+
tokenizer.save_pretrained(args.output_dir)
|
| 337 |
+
logger.info("Saved adapters to %s", args.output_dir)
|
| 338 |
+
logger.info("Evidence artifacts in %s", paths.root)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
if __name__ == "__main__": # pragma: no cover
|
| 342 |
+
main()
|