RL Crystal Generation

Lessons from Designing Rewards for Crystal Generation

What months of failed training runs actually taught me about reward shaping.

By jmedina9 • 10 min read • Reward Design RL Materials Science

It has long been known that constructing a reward function on top of an environment, such that a desired objective is learned, is a non-trivial task. Among the many problems that arise, reward hacking stands out the most due to its catastrophic results and its tendency to appear where you least expect it.

Why is it so hard? In my experience, the root cause is that you start with an objective that is inherently difficult to define precisely: "What is a clean room? What is a good poem? What is a stable crystal?" Beyond that, the limitations of your design are impossible to predict in advance; they only surface after training has already revealed the gaps. This is compounded by the model itself: when a model is not capable enough to reliably satisfy a well-defined reward, the reward designer is forced to make strategic tradeoffs between training longer, running faster iterations with tweaked rewards, or restructuring the reward entirely.

What follows are five concrete lessons I learned while designing the reward function for crystal generation using GRPO on 0.5–8B parameter models. Some of these lessons emerged quickly; others only became clear after watching hundreds of failed rollouts. All of them are grounded in specific failures from the crystal generation task.

Five Lessons at a Glance

1️⃣
Rubric-style rewards > gated rewards. Each sub-goal should be scored independently; never let failure on one silently zero out the rest.
2️⃣
Avoid negative rewards. Punishing a model for failure teaches avoidance, not correction, and can collapse training into a local optimum immediately.
3️⃣
More reward terms → harder to control training. Exponential growth in distinct reward signals creates unexpected emergent policies.
4️⃣
Continuous rewards improve training over binary ones. When the metric is continuous, preserve that signal; dense gradients improve learning efficiency.
5️⃣
Always ask: what is the path of least resistance? Before you finalize a reward, ask how a perfectly rational, lazily optimal agent would game it.

Why This Matters for Crystal Generation Specifically

This is really an issue of model capability. If the models I was using were sufficiently powerful, a simple binary reward (is the crystal stable or not?) would be enough. But at the scales tested (0.5–8B parameters), they simply are not. Even the original DeepSeek paper included a lightweight format reward term rather than discarding any response that lacked proper formatting, because a model that can't yet produce valid outputs still needs a gradient signal to learn from.

Crystal structure generation adds further difficulty: the problem is highly underdetermined (many valid CIFs exist for a given composition), the evaluation oracle (CHGNet) has its own noise floor, and there is no natural language description of what a "slightly better" crystal looks like. Every lesson below was hard-won from the specific constraints of this task.

Lesson 1: Rubric-Style Rewards Beat Gated Rewards

A gated reward, where failing one check returns 0 for everything, throws away the most valuable training signal: partially correct outputs that carry real information about what the model is getting right.

My first reward function was sequential: it checked format, then composition, then bond lengths, then forces. If any check failed, the function returned 0 immediately. The logic seemed defensible: why reward a crystal that doesn't even have the right composition? But the consequence was that a large fraction of the most valuable training examples were silently discarded.

Here is a concrete example. Early in training, the model occasionally generated a CIF file with excellent bond geometry and low forces, a structurally coherent proposal, but with a composition that was slightly off (e.g., NaCl₁.₅ instead of NaCl). Under the gated reward, this rollout earned exactly 0. Under a rubric reward, it would have earned partial credit for bond lengths and forces, providing a real gradient signal on two of the four sub-goals.

Gated Reward

def reward(cif, target):
    if not parse(cif):
        return 0.0
    if not composition_ok(cif, target):
        return 0.0       # ← silent zero
    if not bonds_ok(cif):
        return 0.0
    if not forces_ok(cif):
        return 0.0
    return 1.0

A structurally sound crystal with a minor composition error earns 0. Wasted signal on bonds and forces.

Rubric Reward

def reward(cif, target):
    r = 0.0
    r += 0.10 * format_score(cif)
    r += 0.15 * composition_score(cif, target)
    r += 0.15 * bond_score(cif)
    r += 0.24 * forces_score(cif)
    r += 0.60 * energy_score(cif)
    return r

Same crystal earns partial credit for bonds and forces. Gradient flows through all sub-goals simultaneously.

The deeper problem with gated rewards is that they enforce a strict learning order: the model must master step N before it receives any signal about step N+1. In practice, this means the model can spend many steps stuck on composition while its capacity to learn bond geometry, which it might naturally acquire in parallel, goes entirely unused. Switching to rubric-style scoring accelerated convergence across all sub-goals simultaneously.

The rubric approach does introduce the multi-term complexity from Lesson 3, but the tradeoff is worth it. Dense gradient coverage is more valuable than the simplicity of a clean sequential gate.

Lesson 2: Avoid Negative Rewards

Penalizing a model for an error teaches it to avoid the error, not to fix it. Worse, it can collapse exploration into a local optimum from which the model never escapes.

Early in the project, I ran into a common failure mode: the model was generating text that resembled a CIF file but would fail parsing for superficial reasons: content outside the answer tags, trailing commentary mixed into the structure, or minor syntax errors. My first instinct was to add a negative reward whenever the content inside <|answer_start|>...<|answer_end|> was not parseable by pymatgen's CifParser.

This was a mistake.

The reward landscape at that point in training looked roughly like this:

Model Action Probability Net Expected Reward
Output empty tags (nothing inside) trivially easy +0.10
Include tags with an invalid CIF ~moderate +0.10 − 0.05 = +0.05
Include tags with a valid CIF ~very low (early training) +0.10 or more

The model found the optimal policy immediately: output empty tags and do nothing inside. From its perspective, this was rational. Just using the answer tags earned the format reward (+0.10) regardless of what was inside. Attempting to include a CIF but getting it wrong only netted +0.05, strictly worse than doing nothing. And the probability of generating a valid CIF from scratch was too low early in training to shift the expected value. So the policy quickly converged to: earn the safe 0.10, then stop.

Entropy collapsed almost immediately. The model had found a local optimum, and the very low probability of any individual rollout producing a valid CIF was never high enough to push it out. This is the classic hill-climbing trap: a small guaranteed reward for the safe action dominates when the risky alternative returns less.

The fix was straightforward: remove the negative reward entirely. Now, producing empty tags still earns +0.10, but producing an invalid CIF earns 0.0 instead of being penalized. This keeps exploration alive. The model no longer has a reason to prefer empty tags over a genuine attempt. The rare valid CIF, when it appears, reinforces that behavior without the gradient being suppressed by penalties elsewhere.

The Curiosity Analogy

Think of it this way: punish a child for taking apart their toys to see how they work, and you may kill their curiosity about how things function. The behavior you prevented was actually the behavior you needed. Language models, at training scale, behave in strikingly similar ways. The more I worked on this project, the more I found myself anthropomorphizing the training dynamics, and the more useful that framing turned out to be.

Lesson 3: The More Terms, the More Unexpected the Outcome

Each new reward term doesn't just add one more signal; it multiplies the space of distinct policies that can achieve similar total scores, creating exponentially more surface area for unexpected behavior.

To see why, consider a reward with three independent binary judges (each returning 0 or 1). The set of achievable total rewards is {0, 1, 2, 3}. Now suppose one judge is weighted at 0.5× instead of 1.0×. The achievable set expands to {0, 0.5, 1, 1.5, 2, 2.5}: only three judges, but 50% more distinct reward levels. Crucially, the mapping from policy behavior to reward is now less unique:

Policy Judge 1 (×0.5) Judge 2 (×1.0) Judge 3 (×1.0) Equal Weights Scaled Weights
Policy A ✅ ❌ ✅ 2 1.5
Policy B ✅ ✅ ❌ 2 1.5
Policy C ❌ ✅ ✅ 2 2.0

With equal weights, Policies A, B, and C are indistinguishable (all score 2). Discount Judge 1 to 0.5× and Policy C jumps to the top (2.0) while Policies A and B both drop to 1.5. Policy C never passes Judge 1 at all, yet it wins because it doesn't need to. This is the core mechanic behind composition hacking in the crystal generation task: the composition rubric is Judge 1.

The Crystal Generation Case: Composition Hacking

In my first multi-term iteration, I used four rubrics: format, composition, bond lengths, and forces. Composition was included intentionally. Free generation without a compositional constraint would inadvertently reward simpler crystals, since binary metals and symmetric oxides are far easier to generate than ternary or quaternary systems.

Training proceeded well at first. Then, around step 1000, I noticed the aggregate reward kept climbing while the composition sub-score steadily declined. The model had discovered a simple exploit: ignore the target composition and generate an easier, structurally simpler crystal instead. A binary metallic oxide, for example, is far more likely to achieve low formation energy and reasonable bond lengths than a ternary chalcogenide. Two rubrics improving more than compensated for one rubric getting worse.

Why the math says this will always happen:

Consider the joint distribution of policies that satisfy composition: P(C = 1). Within that subset, the fraction that also achieves good energy and forces, P(E = 1, F = 1 | C = 1), is much smaller than the unconstrained version, P(E = 1, F = 1). The composition constraint shrinks the feasible policy space dramatically.

GRPO optimizes for the highest total reward across rollouts. From the perspective of the policy gradient, the expected return is higher in the unconstrained region, regardless of what weight you assign to composition, as long as that weight is small relative to the combined energy and force weight. Composition becomes an obstacle, not a signal.

The fix, for smaller models where this was most acute, was to temporarily increase the composition weight to force the policy back into the compositionally constrained region, then gradually rebalance as the model became capable of satisfying all rubrics simultaneously. At larger scales (30B and 230B parameters), this problem was much less severe: those models had enough capacity to satisfy composition and stability constraints at the same time, so inflated composition weights were not needed.

Lesson 4: Continuous Rewards Outperform Binary Ones

When the underlying metric is continuous, preserve that structure in the reward. Binary rewards discard ordinal information that could otherwise guide the policy toward incrementally better outputs.

For the formation energy rubric, the materials science community uses a canonical threshold: structures with Ef < 0.1 eV/atom are considered thermodynamically stable and likely synthesizable. The natural instinct is to implement this as a step function: above threshold → 0, below threshold → 1.

The problem is that formation energy is a rich, continuous signal. A structure with Ef = 0.5 eV/atom is meaningfully better than one with Ef = 3.2 eV/atom: it is closer to stability, its bond geometry is more coherent, its forces are lower. But under a step reward, they receive identical scores (0). The policy has no reason to prefer one over the other.

Compare the three available reward strategies:

Discrete (Step Function)

Binary. Provides gradient signal only at the threshold boundary; all off-threshold structures are equivalent.

def formation_energy_discrete(energy, threshold=0.1):
    return 1.0 if energy <= threshold else 0.0
Linear Interpolation
Recommended

Smooth gradient across the sub-threshold region. Every improvement in energy is rewarded proportionally.

def formation_energy_linear(energy, threshold=0.1, max_penalty=2.0):
    if energy <= threshold:
        return 1.0
    elif energy >= threshold + max_penalty:
        return 0.0
    else:
        return 1.0 - (energy - threshold) / max_penalty
Exponential Decay

Aggressive penalty for high-energy structures. Front-loads the gradient near the threshold.

def formation_energy_exponential(energy, threshold=0.1, scale=1.0):
    if energy <= threshold:
        return 1.0
    excess = energy - threshold
    return np.exp(-excess / scale)

The linear and exponential variants both provide useful gradient signal for structures that are not yet stable, guiding the policy to incrementally reduce formation energy rather than treating all sub-threshold structures as equally worthless. In practice, the linear variant is preferable for early training because the flat gradient near the threshold doesn't create a "cliff" that destabilizes learning once the model is close to passing.

There are cases where binary rewards are appropriate: when the task is genuinely all-or-nothing and there is no meaningful partial credit (a correct classification, a flawless backflip). For crystal generation, this is not the case. The metric is continuous, the DFT oracle is noisy, and dense gradient coverage is more important than strict threshold semantics.

Lesson 5: Always Think About the Path of Least Resistance

Before finalizing any reward function, ask: how would a perfectly rational, lazily optimal agent exploit this? The answer will almost always reveal a gap you hadn't considered.

This is less of a standalone lesson and more of a meta-rule that encompasses the others. Every reward hacking failure I encountered was, in retrospect, a failure to predict the path of least resistance in advance.

The empty-tag exploit (Lesson 2) was the path of least resistance when negative rewards made exploration costly. Composition hacking (Lesson 3) was the path of least resistance when simpler crystals were easier to generate and two other rubrics were sufficient to compensate. The gated reward (Lesson 1) was the path of least resistance for the training loop itself; it was easier to implement but wasteful in practice.

A useful exercise before training: for each reward term, write down the policy that maximizes that term in isolation while actively failing all others. Then ask: does the total reward still incentivize that degenerate policy? If yes, rebalance. The composition example from Lesson 3 is a perfect illustration: a model that generates whatever crystal it finds easiest, regardless of the target formula, will consistently outscore a model that tries to honor the composition constraint but generates slightly imperfect structures.

A Pre-Training Reward Audit Checklist

  • □ For each term, what is the trivially optimal policy that maximizes only that term?
  • □ Does that degenerate policy still score well under the full reward? If yes, increase the weight of competing terms.
  • □ Are any terms negative? If yes, are there safe actions that avoid the penalty entirely? (If so, the model will take them.)
  • □ Are any terms gated on others? If a model can score well on three rubrics while a fourth kills all rewards, fix the gating.
  • □ Are any continuous metrics being binarized? Consider whether a smooth version would improve gradient coverage.

How These Lessons Shaped the Final Reward

The reward function that trained the full system incorporates all five lessons. No negative rewards. Independent rubric scoring. Continuous rewards for energy and forces where possible. Weights tuned to close off known exploitation paths.

Rubric Weight Reward Type Key Design Choice
Format 0.10 Binary No penalty for invalid CIF; only positive reward for a valid one. Avoids empty-tag collapse.
Composition 0.15 Binary Low weight prevents hacking via simpler crystals; non-zero weight provides early gradient signal.
Formation Energy 0.60 Continuous (linear) Primary stability signal. Linear decay rewards incremental improvement below and above threshold.
Forces 0.24 Continuous (decay) 1/(1 + decay·F_max). Smoothly penalizes residual forces; even imperfect structures get some credit.
Bond Lengths 0.15 Continuous (raw score) Returns the raw 0–1 reasonableness score from pymatgen. Encourages incremental geometric improvement.

None of these weights emerged from a theoretic argument alone. They were each tested against the pre-training audit: which degenerate policies does this weighting allow, and does the full reward still penalize them adequately? The 60% formation energy weight, in particular, is high enough to make purely compositional hacking non-viable; the model needs to hit near-stable energies, not just the right stoichiometry, to score well.

A Note on Capability

All five lessons above exist, fundamentally, because the models I was working with were not capable enough to learn from a simple binary stability reward. This is not a criticism of the models; it is a constraint of the testing regime. The correct remedy, if compute is unconstrained, is almost always: use a more capable model with a simpler reward. Reward engineering is the substitute you reach for when model scale is the binding constraint. With sufficiently large models, the 230B training runs reported in the main post suggest that much of this engineering becomes unnecessary. The model learns to reason its way to stable structures through chain-of-thought alone. The reward, at that scale, can afford to be simple.

← Back to Environment & Training Train on the Environment →