DSPy Architecture Deep Dive for Elixir/BEAM Port
Executive Summary
DSPy is a sophisticated framework for programming language models declaratively through composable modules, automatic optimization, and structured prompting. A port to Elixir/BEAM would leverage OTP’s supervision trees, lightweight processes, and fault tolerance to create a more robust and scalable version.
Core Architectural Components
1. Signature System - The Foundation
Current Python Architecture:
- Uses Pydantic models with metaclasses to define input/output schemas
- Supports custom types (Image, Audio, Tool, etc.) with serialization
- Field-level validation and type coercion
- Instruction embedding at the type level
BEAM/OTP Translation:
- Replace with Ecto-style schemas or custom structs with behaviours
- Leverage pattern matching for type validation
- Use GenServer registries for signature management
- Custom types as modules implementing a common behaviour
defmodule DSPy.Signature do
@callback fields() :: %{atom() => DSPy.Field.t()}
@callback instructions() :: String.t()
end
2. Module System - Composable Components
Current Python Architecture:
- Base
Module
class withforward()
method - Automatic parameter discovery via
named_parameters()
- Deep copy semantics for optimization
- Callback system for instrumentation
BEAM/OTP Translation:
- Modules as GenServers with standardized
call/3
interface - Process hierarchy mirroring module composition
- Supervision trees for fault tolerance
- ETS tables for parameter storage and sharing
defmodule DSPy.Module do
@callback forward(inputs :: map(), opts :: keyword()) ::
{:ok, outputs :: map()} | {:error, reason :: term()}
# Each module instance runs in its own process
use GenServer
end
3. Language Model Abstraction
Current Python Architecture:
LM
class with provider abstraction- Async/sync calling patterns
- Caching layer with disk/memory tiers
- Token usage tracking
BEAM/OTP Translation:
- LM providers as GenServer pools
- Built-in backpressure via process mailboxes
- Distributed caching with :ets and :mnesia
- Circuit breaker pattern for external API calls
defmodule DSPy.LM.Pool do
use DynamicSupervisor
# Pool of LM workers with load balancing
end
defmodule DSPy.LM.Worker do
use GenServer
# Individual LM instance with circuit breaker
end
4. Adapter Pattern - Format Translation
Current Python Architecture:
ChatAdapter
,JSONAdapter
for different LM interfaces- Message formatting and parsing logic
- Custom type serialization
- Error handling and retries
BEAM/OTP Translation:
- Adapters as stateless modules or GenServers
- Pipeline pattern for message transformation
- Streaming support via GenStage
- Supervisor-based retry mechanisms
5. Optimization/Teleprompting System
Current Python Architecture:
- Multiple optimization strategies (COPRO, Bootstrap, etc.)
- Parallel evaluation with threading
- Complex state management during optimization
- Metric-driven optimization loops
BEAM/OTP Translation:
- Each optimization run as a supervised process tree
- Task distribution via Task.Supervisor
- Persistent state in ETS/Mnesia for large optimizations
- Real-time progress monitoring via Phoenix LiveView
defmodule DSPy.Teleprompt.Supervisor do
use Supervisor
# Supervises optimization process, evaluation workers, and state management
end
Key Architectural Patterns for BEAM
1. Process-Per-Module Architecture
Instead of object instances, each DSPy module would run in its own process:
# Module composition creates process hierarchies
%{
"question_answerer" => #PID<0.123.0>,
"retriever" => #PID<0.124.0>,
"chain_of_thought" => #PID<0.125.0>
}
Benefits:
- Fault isolation (one module failure doesn’t crash others)
- Natural parallelism
- Memory isolation
- Built-in monitoring and restart capabilities
2. Supervision Trees for Fault Tolerance
DSPy.Application.Supervisor
├── DSPy.LM.Supervisor
│ ├── Provider.OpenAI.Pool
│ ├── Provider.Anthropic.Pool
│ └── DSPy.Cache.Manager
├── DSPy.Module.Supervisor (DynamicSupervisor)
│ ├── Module.Instance.{uuid1}
│ ├── Module.Instance.{uuid2}
│ └── ...
└── DSPy.Teleprompt.Supervisor
├── Optimization.{run_id}
└── Evaluation.Pool
3. Event-Driven Architecture
Replace callback system with Phoenix PubSub:
# Module execution events
Phoenix.PubSub.broadcast(DSPy.PubSub, "module:events", {
:module_start,
%{module_id: module_id, inputs: inputs, timestamp: timestamp}
})
4. Streaming and Backpressure
Use GenStage for streaming LM responses:
defmodule DSPy.LM.Stream do
use GenStage
# Handles streaming responses with built-in backpressure
end
Memory and State Management
1. ETS for Fast Local Caching
- Signature registries
- Module parameter storage
- Local LM response cache
2. Mnesia for Distributed State
- Optimization history
- Training data
- Cross-node module sharing
3. Process Dictionary for Request Context
- Trace information
- Request-scoped settings
- Callback state
Concurrency and Parallelism Advantages
1. Natural Parallelism
- Module evaluation across multiple processes
- Parallel optimization candidate testing
- Concurrent LM requests with different providers
2. Backpressure Handling
- Process mailboxes provide natural rate limiting
- GenStage for streaming operations
- Circuit breakers for external API protection
3. Distributed Computing
- Spread optimization across multiple nodes
- Distributed caching with Mnesia
- Node-level fault tolerance
Error Handling and Recovery
1. Let It Crash Philosophy
- Module failures are isolated and recoverable
- Supervisor strategies for different failure modes
- Graceful degradation patterns
2. Circuit Breaker Pattern
- Protect against LM API failures
- Automatic retry with exponential backoff
- Health check monitoring
3. Poison Message Handling
- Dead letter queues for problematic inputs
- Automatic quarantine and analysis
- Human-in-the-loop recovery workflows
Configuration and Settings
Replace global settings with:
1. Application Environment
config :dspy,
default_lm: DSPy.LM.OpenAI,
cache_ttl: :timer.minutes(30),
max_retries: 3
2. Process-Local Configuration
# Per-process overrides
Process.put(:dspy_config, %{temperature: 0.7})
3. Dynamic Configuration
# Runtime configuration changes
DSPy.Config.update(:default_lm, DSPy.LM.Anthropic)
API Design Patterns
1. Pipeline-Based Composition
result =
inputs
|> DSPy.Module.call(retriever)
|> DSPy.Module.call(chain_of_thought)
|> DSPy.Module.call(answer_formatter)
2. Supervision Tree Building
{:ok, program_pid} = DSPy.Program.start_link([
{:retriever, DSPy.Retrieve, [k: 5]},
{:cot, DSPy.ChainOfThought, [signature: "question -> answer"]},
{:formatter, DSPy.Format, [template: "Answer: {{answer}}"]}
])
3. Stream Processing
inputs
|> DSPy.Stream.from_enumerable()
|> DSPy.Stream.through_module(question_processor)
|> DSPy.Stream.batch(50)
|> DSPy.Stream.to_list()
Key Benefits of BEAM Architecture
- Fault Tolerance: Module failures don’t crash the entire system
- Scalability: Lightweight processes enable massive concurrency
- Distribution: Natural support for multi-node deployments
- Monitoring: Built-in process monitoring and health checking
- Hot Code Upgrades: Update modules without system downtime
- Resource Management: Process-level memory and CPU isolation
- Backpressure: Built-in flow control prevents system overload
Implementation Considerations
1. State Persistence
- Use GenServer state for module parameters
- ETS for frequently accessed data
- Mnesia for distributed/persistent state
2. Type System Integration
- Leverage Elixir’s pattern matching
- Consider using TypedStruct for structured data
- Implement runtime type checking where needed
3. Interoperability
- NIFs for performance-critical components
- Ports for Python model integration
- GenStage for streaming data processing
This architecture would make DSPy more robust, scalable, and naturally distributed while maintaining its core declarative programming model for language models.