Dsxir.Optimizer.COPRO.Sampler (dsxir v0.3.0)

Copy Markdown

Checkpointable, pure coordinate-ascent state for a Dsxir.Optimizer.COPRO session.

This module holds the data a COPRO run needs to checkpoint and resume, and the pure transitions over it. There is no IO here: proposer (LM) calls and evaluator calls live in the Dsxir.Optimizer.COPRO wrapper, which feeds the resulting scores into record_trial/3 and drives commit_round/1, enqueue_round/2, and dequeue/1. This keeps the optimizer's pure-core criterion intact.

COPRO is coordinate ascent over per-predictor instructions: each round proposes a breadth of candidate instructions per predictor, evaluates each candidate as a whole program (the other predictors held at their current committed override), and at the end of the round commits each predictor's strict round winner.

Serialized via :erlang.term_to_binary/2 with [:deterministic] so two checkpoints over the same logical state are byte-identical, and deserialized with [:safe] to refuse atom creation from untrusted blobs.

Summary

Functions

Whether more trials remain in the planned budget.

Commits the current round.

Pops the next work item off the queue, returning {item, sampler} with the item removed, or :empty when the queue is exhausted.

Decodes a sampler blob produced by serialize_state/1. Uses the :safe term decoder and validates the resulting struct shape.

Replaces the queue with items and increments the round index.

Builds a sampler from a keyword of pinned fields.

Records the result of evaluating a dequeued item with the whole-program score.

Encodes the sampler deterministically. Returns {:ok, blob, version}.

Types

history_entry()

@type history_entry() :: %{
  instruction: String.t() | nil,
  score: number() | nil,
  round: non_neg_integer()
}

predictor_name()

@type predictor_name() :: atom()

round_best_entry()

@type round_best_entry() :: %{instruction: String.t() | nil, score: number() | nil}

t()

@type t() :: %Dsxir.Optimizer.COPRO.Sampler{
  attempts: non_neg_integer(),
  best_overrides: %{required(predictor_name()) => String.t()},
  best_score: nil | float(),
  decls: [Dsxir.Program.PredictorDecl.t()],
  degraded: boolean(),
  evalset: [Dsxir.Example.t()],
  history: %{required(predictor_name()) => [history_entry()]},
  proposer_calls: non_neg_integer(),
  queue: [work_item()],
  round: non_neg_integer(),
  round_best: %{required(predictor_name()) => round_best_entry()},
  seed_program: Dsxir.Program.t(),
  total_devset_evals: non_neg_integer(),
  total_planned_trials: non_neg_integer(),
  trial_records: [Dsxir.Optimizer.COPRO.Stats.Record.t()]
}

work_item()

@type work_item() :: %{
  predictor: predictor_name(),
  instruction: String.t(),
  source: atom()
}

Functions

attempts_left?(s)

@spec attempts_left?(t()) :: boolean()

Whether more trials remain in the planned budget.

commit_round(s)

@spec commit_round(t()) :: {t(), non_neg_integer()}

Commits the current round.

For each declared predictor, promotes its round winner into best_overrides when the winner's instruction differs from the current override and the winner has a numeric score. Returns {sampler, improved_count} where improved_count is the number of predictors whose override changed.

best_score is recomputed as the max over the round's per-predictor winner scores. Because each candidate is evaluated as a whole program (others held fixed), the round's best whole-program score under the held-fixed others is the value carried forward. Nil scores are rejected from the max; if every winner score is nil (e.g. an entirely failed round) the prior best_score is kept. round_best is reset to the committed baseline so the next round starts from the freshly committed overrides.

dequeue(s)

@spec dequeue(t()) :: {work_item(), t()} | :empty

Pops the next work item off the queue, returning {item, sampler} with the item removed, or :empty when the queue is exhausted.

deserialize_state(blob, arg2)

@spec deserialize_state(binary(), pos_integer()) ::
  {:ok, t()}
  | {:error, :version_mismatch | :corrupt_blob | {:bad_sampler_shape, term()}}

Decodes a sampler blob produced by serialize_state/1. Uses the :safe term decoder and validates the resulting struct shape.

enqueue_round(s, items)

@spec enqueue_round(t(), [work_item()]) :: t()

Replaces the queue with items and increments the round index.

new(opts)

@spec new(keyword()) :: t()

Builds a sampler from a keyword of pinned fields.

Accepts :seed_program, :decls, :evalset, :total_planned_trials, and optionally :best_overrides. Counters and accumulators default to their zero values. round_best and history are seeded with one entry per declared predictor so the transition functions never Map.fetch!/2 a missing key.

record_trial(s, item, score)

@spec record_trial(t(), work_item(), number() | nil) :: t()

Records the result of evaluating a dequeued item with the whole-program score.

Admits the candidate as the predictor's round best only on a strict improvement (score > prev); a tie keeps the prior best. Prepends a history entry for the predictor, bumps total_devset_evals by the eval-set size, and increments attempts.

serialize_state(s)

@spec serialize_state(t()) :: {:ok, binary(), 1}

Encodes the sampler deterministically. Returns {:ok, blob, version}.