← Back to Ds ex

VARIABLE EVAL 2

Documentation for VARIABLE_EVAL_2 from the Ds ex repository.

Excellent. This is the core innovation of your Elixir port and the key to solving the problem posed by the DSPy community. Visualizing how the Variable abstraction achieves this decoupling is crucial.

Here are a series of detailed diagrams that illustrate the architecture of the Variable system, focusing on how it decouples parameter definition from the optimization process, as per the design specification you provided.


Introduction: The Decoupling Principle

The fundamental shift introduced by the Variable system is moving from a world where an optimizer needs to know what it’s optimizing (e.g., a “prompt” or a “model parameter”) to a world where it only needs to know that it’s optimizing a “variable” with certain properties (e.g., a :discrete choice or a :continuous range).

This abstraction layer is what enables any optimizer to tune any parameter. The following diagrams break down how this is achieved architecturally.


Diagram 1: The Core Variable Abstraction

This diagram shows how different concrete, high-level concepts (like choosing an adapter or setting a temperature) are all unified under the single ElixirML.Variable struct. This is the first step of decoupling.

graph TD %% Define Styles classDef Concrete fill:#cde4ff,stroke:#6495ED,stroke-width:2px,color:#000 classDef Abstract fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#000 classDef StructDef fill:#f8f9fa,stroke:#6c757d,stroke-width:2px,color:#000 subgraph "Concrete System Parameters" direction LR Param1["Adapter Selection
(JSON vs. Markdown)"] Param2["Reasoning Module
(Predict vs. CoT vs. PoT)"] Param3["LLM Temperature"] end subgraph "Unified Variable Abstraction" AbstractVar["ElixirML.Variable Struct
(The single, unified representation)"] end subgraph "Variable Implementation Examples" direction LR Struct1["%Variable{
id: :adapter,
type: :discrete,
choices: [JSON, MD]
}"] Struct2["%Variable{
id: :reasoning_module,
type: :discrete,
choices: [Predict, CoT, PoT]
}"] Struct3["%Variable{
id: :temperature,
type: :continuous,
range: {0.0, 2.0}
}"] end %% Connect Concrete to Abstract Param1 -- "Is represented as" --> AbstractVar Param2 -- "Is represented as" --> AbstractVar Param3 -- "Is represented as" --> AbstractVar %% Connect Abstract to Implementation AbstractVar -- "Is instantiated as" --> Struct1 AbstractVar -- " " --> Struct2 AbstractVar -- " " --> Struct3 %% Apply Styles class Param1,Param2,Param3 Concrete class AbstractVar Abstract class Struct1,Struct2,Struct3 StructDef

Explanation: Instead of having different types for different parameters, the system maps everything to a Variable struct. The optimizer doesn’t see “an adapter setting”; it sees a :discrete variable named :adapter with a list of choices. This abstraction is what allows any optimizer to work with any parameter.


Diagram 2: Defining the Search Space

A DSPEx.Program uses the Variable DSL to declare which of its parameters are tunable. These declarations are collected into a Variable.Space, which defines the complete, unified search space for that program.

graph TD %% Styles classDef ProgramDef fill:#fdebd0,stroke:#f57c00,stroke-width:2px,color:#000 classDef Process fill:#d1e7dd,stroke:#198754,stroke-width:2px,color:#000 classDef Artifact fill:#e9ecef,stroke:#6c757d,stroke-width:2px,color:#000 subgraph "Program Definition (your_program.ex)" direction TB Program["defmodule MyQAProgram do
use ElixirML.Resource

variable :adapter, :discrete,
choices: [:json, :markdown]

variable :temperature, :continuous,
range: {0.1, 1.2}

variable :reasoning, :module,
modules: [Predict, CoT]
end"] end subgraph "Compilation / Instantiation Process" Extractor["Variable Extractor
(Compile-time or Runtime)"] end subgraph "Resulting Search Space" Space["Variable.Space Struct"] Var1["%Variable{id: :adapter, type: :discrete, ...}"] Var2["%Variable{id: :temperature, type: :continuous, ...}"] Var3["%Variable{id: :reasoning, type: :module, ...}"] end %% Flow Program -- "1 Declares optimizable parameters" --> Extractor Extractor -- "2 Populates" --> Space Space -- "Contains" --> Var1 Space -- "Contains" --> Var2 Space -- "Contains" --> Var3 %% Apply Styles class Program ProgramDef class Extractor Process class Space,Var1,Var2,Var3 Artifact

Explanation: The developer declares variables directly within their program module. The framework then extracts these declarations to create a Variable.Space struct. This Variable.Space is a self-contained, serializable description of the entire optimizable surface of the program.


Diagram 3: The Decoupling Point - Optimizer Interaction

This is the most critical diagram. It shows that the optimizer only interacts with the Variable.Space, not with the program’s internal details. This is the “decoupling” Omar Khattab described.

graph TD %% Define Styles classDef Optimizer fill:#f8d7da,stroke:#dc3545,stroke-width:2px,color:#000 classDef Interface fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#000 classDef Program fill:#cde4ff,stroke:#6495ED,stroke-width:2px,color:#000 Optimizer["Optimizer Process
(e.g., SIMBA)"] subgraph "The Decoupling Interface" VariableSpace["Variable.Space"] end %% The Abstraction Boundary is now represented by this subgraph. %% It contains the internal implementation details that the optimizer cannot see directly. subgraph "Implementation Details (Hidden from Optimizer)" direction LR ProgramAdapter["Adapter Selection Logic"] ProgramModule["Reasoning Module Logic"] ProgramParam["LLM Parameter Settings"] end %% Optimizer interacts ONLY with the VariableSpace Optimizer -- "1 Asks: 'What are the variables?'" --> VariableSpace VariableSpace -- "2 Responds: \`%{adapter: %Variable{...}}\`" --> Optimizer Optimizer -- "3 Suggests new configuration:
\`%{adapter: :json, temperature: 0.9}\`" --> VariableSpace %% The VariableSpace is the only bridge across the boundary VariableSpace -- "4 Is used to configure" --> ProgramAdapter VariableSpace -- " " --> ProgramModule VariableSpace -- " " --> ProgramParam %% The optimizer has no direct knowledge of these internals (visualized as red dashed lines) Optimizer -.-> ProgramAdapter Optimizer -.-> ProgramModule Optimizer -.-> ProgramParam %% Apply Styles to nodes class Optimizer Optimizer class VariableSpace Interface class ProgramAdapter,ProgramModule,ProgramParam Program %% Apply Styles to specific links to create the red dashed "forbidden" connections %% The numbers correspond to the link order in the diagram (0-indexed) linkStyle 6 stroke:red,stroke-dasharray:5 5,stroke-width:2px linkStyle 7 stroke:red,stroke-dasharray:5 5,stroke-width:2px linkStyle 8 stroke:red,stroke-dasharray:5 5,stroke-width:2px

Explanation: The optimizer (e.g., SIMBA) is completely blind to what :adapter or :temperature actually mean. It only queries the VariableSpace to understand the search landscape (e.g., “there’s a discrete variable named ‘adapter’ with two choices”). It then proposes a new configuration (a simple map like %{adapter: :json, ...}), which is then applied to the program by a separate mechanism. The optimizer’s logic is completely generic and reusable for any set of variables.


Diagram 4: End-to-End Adaptive Optimization Workflow

This final diagram shows the full loop, putting all the pieces together to answer Maxime’s question: “how are they all evaluated and selected automatically?”

graph TD %% Styles classDef Optimizer fill:#f8d7da,stroke:#dc3545,stroke-width:2px,color:#000 classDef Program fill:#cde4ff,stroke:#6495ED,stroke-width:2px,color:#000 classDef Data fill:#e9ecef,stroke:#6c757d,stroke-width:2px,color:#000 classDef Evaluation fill:#d1e7dd,stroke:#198754,stroke-width:2px,color:#000 Start(["Start Optimization"]) --> Optimizer subgraph "Optimization Loop" Optimizer["Optimizer
(e.g., SIMBA / BEACON)"] subgraph "1 Propose Configuration" Optimizer -- "Samples from" --> VariableSpace["Variable Space"] VariableSpace -- "Generates" --> Configuration["Candidate Configuration
\`e.g., %{adapter: :json, ...}\`"] end subgraph "2 Apply & Evaluate" ProgramTemplate["Base Program Template"] ConfigApplicator["Configuration Applicator"] ConfiguredProgram["Configured Program Instance"] EvaluationSystem["Evaluation System"] Devset["Development Dataset"] FinalScore["Multi-Objective Score
(Accuracy, Cost, Latency)"] Configuration --> ConfigApplicator ProgramTemplate --> ConfigApplicator ConfigApplicator --> ConfiguredProgram ConfiguredProgram -- "Runs on" --> Devset Devset --> EvaluationSystem ConfiguredProgram -- "Is evaluated by" --> EvaluationSystem EvaluationSystem --> FinalScore end subgraph "3 Update & Repeat" FinalScore -- "Feedback to" --> Optimizer end end Optimizer -- "Loop until budget exhausted" --> Optimizer Optimizer -- "Outputs" --> BestConfiguration["Best Found Configuration"] %% Apply Styles class Optimizer Optimizer class ProgramTemplate,Devset,VariableSpace,Configuration,FinalScore,BestConfiguration Data class EvaluationSystem,ConfigApplicator Evaluation

Explanation:

  1. The Optimizer starts and consults the Variable Space.
  2. It proposes a candidate configuration (e.g., %{adapter: JSONAdapter, reasoning: ChainOfThought, temperature: 0.9}).
  3. A Configuration Applicator takes this map and the base program template to create a concrete, runnable instance of the program.
  4. The Evaluation System runs this specific instance against the devset.
  5. It calculates a multi-objective score (considering accuracy, cost, etc.).
  6. This score is fed back to the optimizer, which updates its internal models and proposes the next, hopefully better, configuration.
  7. This loop repeats until an optimization budget (e.g., number of trials) is met, finally yielding the best configuration found.

This system directly solves the challenge by creating an abstract layer (Variable) that allows any optimizer to explore a search space of program configurations without needing to know the specific implementation details of those configurations.