Chapter 10 of 18

Compile the Program

Explain this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Apply this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Compile the Program

Chapter 9 gave the optimizer something to pursue: a metric. That does not mean we should let an optimizer mutate the accepted program.

Compilation in DSPy is the process of taking a program, optimizer-visible evidence, a metric, and an optimizer, then producing a candidate program state.


1. Baseline and candidate are different objects

Before compiling, name the boundary:

A candidate is not the new production program. It is a proposed program version that must be evaluated.

baseline v0.1
     โ†“
compile on allowed data
     โ†“
candidate v0.2
     โ†“
evaluate untouched cases
     โ†“
accept, reject, or keep investigating

CoCoder’s EngineeringProgram docs make this explicit: ProgramRuntime executes one concrete program version; ProgramOptimizer proposes a new candidate version. DSPy types are isolated inside the optimizer adapter. Holdout cases are persisted on the optimization run but not passed into the optimizer.


2. The minimal compile shape

Current DSPy optimizers expose a compile(...) pattern. For BootstrapFewShot:

Other optimizers use a validation set. For example, current MIPROv2 accepts valset during compile and uses it while searching candidate instruction and demonstration combinations:

optimizer = dspy.MIPROv2(
    metric=editorial_metric,
    auto="light",
    seed=13,
)
candidate = optimizer.compile(
    student=baseline,
    trainset=trainset,
    valset=devset,
)

That distinction matters. Current BootstrapFewShot.compile accepts student, optional teacher, and trainset; it does not take valset. Current MIPROv2.compile accepts both trainset and optional valset.

So dev is an experimental role, not a universal DSPy argument:


3. Audit before and after compile

Compilation should leave a trail. At minimum, record the state of the baseline before optimization, then record the candidate after optimization.

For DSPy modules, dump_state() serializes the state of named parameters. After compilation, that can expose predictor state such as demonstrations and optimized signature/instruction content.

It does not by itself describe the whole executable system. Python architecture, external tools, runtime configuration, LM/provider settings, dataset identity, and optimizer configuration belong in the surrounding manifest.

Current BootstrapFewShot also creates a reset copy of the student before training it, so the returned compiled student is distinct from the baseline state. Recording baseline_before and baseline_after still gives us a useful invariant: compilation must not silently change the accepted baseline object.

The important habit is to never let compile be an unaudited transition.


4. Persist the candidate state

DSPy supports two importantly different persistence modes.

A .json save is state-only:

import hashlib
import json
from pathlib import Path

def write_manifest(path: str, payload: dict) -> str:
    blob = json.dumps(payload, indent=2, sort_keys=True)
    fingerprint = hashlib.sha256(blob.encode("utf-8")).hexdigest()
    manifest = {**payload, "manifest_fingerprint": fingerprint}
    Path(path).write_text(json.dumps(manifest, indent=2, sort_keys=True))
    return fingerprint

The manifest should answer:


5. CoCoder’s stricter protocol

CoCoder’s frozen DSPy experiment protocol is the larger version of this chapter:

freeze manifest
check corpus
evaluate baseline on holdout
run one DSPy optimization on train/development only
persist candidate program
audit candidate integrity
evaluate candidate on same holdout
compare with conservative policy
persist result
stop without promotion

There is a subtle holdout point in this protocol. CoCoder evaluates the baseline on the frozen holdout before optimization, then evaluates the candidate on the same holdout. The holdout remains valid because it is not passed to the optimizer and the frozen protocol does not tune prompts, policy, validation, dataset membership, or model configuration after observing those results.

So “untouched holdout” should mean not used to shape the candidate or protocol, not necessarily “never executed before candidate generation.” If a human inspected baseline holdout failures and then changed the metric or program before compilation, that holdout would have become development evidence.

The central rule is simple:

The optimizer proposes. It does not promote itself.

DSPy can generate a candidate and report optimizer-facing scores. Those scores are evidence about the optimization procedure, not permission to deploy. Promotion belongs to an independent comparison and policy layer.

For the editorial program, the same rule applies. A compiled sentence improver is a candidate. It must still be compared with the frozen baseline under the predeclared evaluation protocol and inspected for failure distribution.


6. What compilation is allowed to change

Before running an optimizer, decide which parts of the system are allowed to change.

Component Change during compile? Reason
program Python architecture No This experiment optimizes DSPy parameter state, not source code
input/output field schema No Changing the task interface changes the experiment
demonstrations in predictor state Yes Few-shot optimizers may select or bootstrap demos
signature instructions/descriptions Depends on optimizer Instruction optimizers such as MIPROv2 may search this state
metric implementation/version No Changing the objective changes what “better” means
metric threshold / optimizer budget No These are frozen optimizer configuration
split membership No Moving cases after search changes the evidence boundary
holdout influence No Holdout must not shape candidate generation or protocol
LM / prompt-model / adapter configuration No Changing execution dependencies confounds attribution

The point is not that every DSPy optimizer changes the same fields. The manifest should declare the optimizer’s allowed mutation surface before the run. Anything outside that surface changing creates a different experiment.


What Usually Goes Wrong

Symptom Likely cause How to diagnose it What to change
Weak traces are accepted during BootstrapFewShot Fractional metric used without an explicit threshold Inspect metric values and metric_threshold Freeze a boolean acceptance rule or numeric threshold
Audit says dev cases were “visible” to BootstrapFewShot Reserved data and optimizer-visible data were conflated Compare recorded IDs with actual compile(...) arguments Record visibility by optimizer phase
Baseline state changes after compile Accepted state and candidate state were not kept separate Compare dump_state() before and after Fail the experiment if the baseline invariant changes
Holdout cases shape optimization Split or feedback boundary was not enforced Audit compile inputs and post-holdout changes Keep holdout outside candidate/protocol adaptation
State-only candidate cannot load Python architecture/version no longer matches the saved state Reconstruct the recorded baseline/candidate code revision Version code and state together; use full-program artifacts only when justified
Candidate cannot be reproduced No manifest beside saved state Inspect artifact directory Save optimizer/data/metric/LM/version fingerprints
Optimizer score is treated as deployment proof Proposal and promotion are fused Look for automatic active-version updates Require independent evaluation and explicit promotion

Conclusion

We gained the compilation boundary. DSPy compilation is now an auditable transition from frozen baseline state to candidate state under an explicit optimizer visibility and mutation policy.

We removed two assumptions: that compile() is simply “better prompting,” and that an optimizer’s returned program or optimizer score should automatically replace the accepted version.

The next question is narrower: BootstrapFewShot changes program behavior by constructing demonstrations. Where do those demonstrations come from, which traces are admitted, and why should we trust them?