packages feed

shikumi-optimize-0.1.0.0: src/Shikumi/Optimize/Instruction.hs

-- | M3 — instruction search (a MIPRO/COPRO-style optimizer). For each node, the
-- /grounded/ proposer (EP-19, 'Shikumi.Optimize.Propose.proposeInstructions') suggests
-- several candidate instruction strings — fed a dataset summary, the program's
-- pseudo-code and description, the node's real input/output field names, the
-- instruction history, and a stylistic tip — and each candidate is scored; the best is
-- kept. Optimization is greedy coordinate ascent — one node at a time, holding the
-- others fixed — so the candidate count is linear in @nodes × proposals@.
--
-- The proposer and its signal-gatherers are themselves ordinary shikumi 'Program's, so
-- they are typed, cached, traced, and testable with the same stub-LM machinery as
-- everything else — the optimizer is written in the framework it optimizes. The
-- /current/ instruction is always retained as a candidate (the proposer guarantees it),
-- so a node can never end up worse than where it started.
--
-- __Budget.__ The grounded proposer makes @4 + proposalsPerNode@ LM calls per node
-- (dataset summary, program describe, module describe, and one generation per proposal);
-- scoring one candidate costs one LM call per dataset example. The search threads a
-- running raw-call count and stops — returning the best found /so far/ — before either
-- bound in the 'Budget' would be exceeded, so the recorded LM-call count never exceeds
-- @maxLmCalls@. When the remaining budget cannot cover a node's full proposal, that node
-- keeps its current instruction (no proposer call) rather than partially proposing.
--
-- This module re-points V1's blind proposer at EP-19's grounded surface; the old
-- @ProposeIn@/@ProposeOut@/@proposeInstruction@ predictor is removed (the grounded
-- @GenerateInstructionIn@/@GenerateInstructionOut@ replaces it, still emitting a
-- @proposedInstruction@ output field).
module Shikumi.Optimize.Instruction
  ( instructionSearch,
  )
where

import Control.Monad (foldM)
import Data.Aeson (ToJSON)
import Data.Maybe (fromMaybe)
import Shikumi.Eval (datasetSize)
import Shikumi.Optimize.Propose
  ( ProposeRequest (..),
    ProposeResult (..),
    proposeInstructions,
  )
import Shikumi.Optimize.Search (freezeProgram, instructionAt, scoreOn, setNodeInstr)
import Shikumi.Optimize.Types (Budget (..), Optimizer (..))
import Shikumi.Program (foldParams)

-- | Search for a better instruction at every node by greedy coordinate ascent under
-- an explicit LM-call budget, using the grounded proposer to generate candidates.
instructionSearch :: (ToJSON i, ToJSON o) => Int -> Budget -> Optimizer i o
instructionSearch proposalsPerNode budget = Optimizer $ \train metric student -> do
  let dsSize = datasetSize train
      nNodes = length (foldParams student)
      -- The grounded proposer's fixed per-node LM-call cost (see module header).
      proposerCost = 4 + max 0 proposalsPerNode

      -- Score candidate instructions for node @idx@, threading the call count and
      -- the best-so-far; stop before the next scoring would exceed the budget.
      scoreCands calls best idx prog cs = case cs of
        [] -> pure (best, calls)
        (c : rest)
          | calls + dsSize > maxLmCalls budget -> pure (best, calls)
          | otherwise -> do
              s <- scoreOn train metric (setNodeInstr idx c prog)
              let best' = case best of
                    Nothing -> Just (c, s)
                    Just (_, bs) -> if s > bs then Just (c, s) else best
              scoreCands (calls + dsSize) best' idx prog rest

      -- Optimize one node, holding the others fixed.
      stepNode (prog, calls) idx = do
        let curInstr = fromMaybe "" (instructionAt idx prog)
        (cands, calls1) <-
          if calls + proposerCost <= maxLmCalls budget
            then do
              ProposeResult cs <-
                proposeInstructions
                  train
                  ProposeRequest
                    { program = prog,
                      targetNode = idx,
                      currentInstruction = curInstr,
                      history = [],
                      bootstrappedDemos = [],
                      numCandidates = proposalsPerNode,
                      tipIndex = 0,
                      viewBatch = 2
                    }
              pure (cs, calls + proposerCost)
            else pure ([curInstr], calls)
        (best, calls2) <- scoreCands calls1 Nothing idx prog cands
        pure (setNodeInstr idx (maybe curInstr fst best) prog, calls2)

  (final, _) <- foldM stepNode (student, 0) [0 .. nNodes - 1]
  pure (freezeProgram final)