Skip to main content
EVOLVE-BLOCK markers let you specify exactly which parts of your code should be mutated by the LLM, while keeping the rest fixed.

Why Use Evolution Blocks?

Preserve Setup Code

Keep imports, constants, and utility functions unchanged

Focus Evolution

LLM only modifies the algorithm, not boilerplate

Maintain Interfaces

Ensure function signatures stay compatible with evaluator

Faster Convergence

Smaller search space = better results

Basic Usage

Wrap the code to evolve with markers:
What happens:
  • LLM sees and can modify everything between EVOLVE-BLOCK-START and EVOLVE-BLOCK-END
  • Everything outside markers is frozen
  • The frozen code is shown to the LLM for context, but never modified

Real Examples

Example 1: Circle Packing

Problem: Pack 26 circles in a unit square.
Why this works:
  • The LLM can evolve the packing algorithm (construct_packing)
  • Helper function compute_max_radii is also evolvable
  • Interface run_packing() stays fixed (evaluator depends on it)
  • Visualization code is preserved
Source: benchmarks/math/circle_packing/initial_program.py:1

Example 2: GPU Load Balancing

Problem: Optimize expert parallelism load balancing.
Why this works:
  • Core algorithm is evolvable
  • Public API rearrange() is fixed
  • License header is preserved
  • Comments and docstrings inside block can be modified
Source: benchmarks/ADRS/eplb/initial_program.py:14

Example 3: Prompt Optimization

EVOLVE-BLOCKs work for prompt evolution too:
The LLM evolves the prompt instructions while keeping the format consistent.

Advanced Patterns

Multiple Evolution Blocks

You can have multiple blocks in one file:
Both blocks are evolved independently — the LLM can modify either or both.

Nested Functions

Everything inside the block is evolvable, including nested definitions:

Imports Inside Blocks

Imports inside blocks can be evolved:

No Markers = Full File Evolves

If you don’t use EVOLVE-BLOCK markers, the entire file is mutable:
This is fine for:
  • Simple problems
  • When you want maximum flexibility
  • Prompt optimization (no boilerplate)
However, it’s risky because:
  • LLM might break interface contracts
  • Boilerplate code gets regenerated every iteration
  • Harder to maintain compatibility with evaluator
If your evaluator imports specific functions from the program, use EVOLVE-BLOCKs to ensure those function signatures don’t change.

Best Practices

✅ Do:

  1. Keep interfaces fixed:
  2. Preserve expensive setup:
  3. Include helper functions in block if they should evolve:
  4. Document the interface:

❌ Don’t:

  1. Don’t put imports outside if they might change:
  2. Don’t split a function across markers:
  3. Don’t put test code inside block:

Omitting Initial Program

You can skip the initial program entirely:
The LLM generates solutions from scratch based on:
  • The evaluator file (shown as context)
  • Problem description in config
  • Example test cases
This works well for:
  • Exploratory discovery
  • Problems where you don’t have a baseline
  • Prompt optimization
When omitting the initial program, provide a detailed problem description in the config:

How It Works Internally

When you provide a program with EVOLVE-BLOCK markers:
1

Parse Markers

SkyDiscover extracts regions between EVOLVE-BLOCK-START and EVOLVE-BLOCK-END
2

Build Prompt

LLM prompt includes:
  • Full file (with markers) as context
  • Only mutable regions in edit mode
3

Generate Mutation

LLM outputs new code for the evolvable regions
4

Reconstruct File

Replace mutable regions with LLM output, keep fixed regions unchanged
5

Evaluate

Write reconstructed file to disk and run evaluator
Code extraction: skydiscover/utils/code_utils.py

Debugging

To see what code is being evolved, enable prompt logging:
Then inspect checkpoints/checkpoint_N/prompts/<program_id>.json to see:
  • Full context shown to LLM
  • Which regions are marked as mutable
  • LLM’s generated replacement code

Evaluators

Write evaluation functions that work with evolved code

Quick Start

See EVOLVE-BLOCKs in action